转自:OpenAtom OpenHarmony
在9月30日更新的 OpenHarmony3.0 LTS 上,标准系统新增支持了方舟开发框架(ArkUI)、分布式组网和 FA 跨设备迁移能力等新特性,因此我们结合了这三种特性使用 ets 开发了一款如下动图所示传炸弹应用。

打开应用在通过邀请用户进行设备认证后,用户须根据提示完成相应操作,然后通过分布式流转实现随机传递炸弹给下一位用户的效果。那么这样一款传炸弹应用如何进行开发呢?
完整的项目结构目录如下:
├─entry
│ └─src
│ └─main
│ │ config.json // 应用配置
│ │
│ ├─ets
│ │ └─MainAbility
│ │ │ app.ets //ets应用程序主入口
│ │ │
│ │ └─pages
│ │ CommonLog.ets // 日志类
│ │ game.ets // 游戏首页
│ │ RemoteDeviceManager.ets // 设备管理类
│ │
│ └─resources // 静态资源目录
│ ├─base
│ │ ├─element
│ │ │
│ │ ├─graphic
│ │ ├─layout
│ │ ├─media // 存放媒体资源
│ │ │
│ │ └─profile
│ └─rawfile
我们可以分为如下 3 步:编写声明式 UI 界面、添加分布式能力和编写游戏逻辑。
1. 新增工程
在 DevEco Studio 中点击 File -> New Project ->Standard Empty Ability->Next,Language 选择 ETS 语言,最后点击 Finish 即创建成功。

图1 新建工程
2. 编写游戏页面

图2 游戏界面效果图
效果图如上可以分为两部分:
首先在 @entry 组件入口 build() 中使用 Stack 作为容器,达到图片和文字堆叠的效果;
接着依次写入 Image 包裹的两个 Text 组件;
Stack() {
Image($r(<span class="hljs-string">"app.media.title"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">120</span>)
Column() {
Text(<span class="hljs-keyword">this</span>.duration.toString() + <span class="hljs-string">'ms'</span>).fontColor(Color.White)
Text(<span class="hljs-keyword">this</span>.touchText).fontColor(Color.White)
}
}
使用 Grid 网格容器来编写九宫格区域;
在 GridItem 中 Stack (容器依次添加方块背景图片和炸弹图片;
在 visibility 属性中用 bombIndex 变量值来决定炸弹显示的位置;
通过 onClick 点击事件和 GestureGroup 组合手势加入单击、双击和长按的监听事件;
Stack() {
Image($r(<span class="hljs-string">"app.media.background"</span>)).objectFit(ImageFit.Contain)
Grid() {
ForEach(<span class="hljs-keyword">this</span>.grid, (item) => {
GridItem() {
Stack() {
Image($r(<span class="hljs-string">"app.media.squares"</span>)).objectFit(ImageFit.Contain)
Image($r(<span class="hljs-string">"app.media.bomb"</span>))
.width(<span class="hljs-string">'50%'</span>)
.objectFit(ImageFit.Contain)
.visibility(<span class="hljs-keyword">this</span>.bombIndex == item ? Visibility.Visible : Visibility.Hidden)
<span class="hljs-comment">// 炸弹点击事件</span>
.onClick((event) => {
<span class="hljs-comment">// 单击</span>
<span class="hljs-keyword">this</span>.judgeGame(RuleType.click)
})
.gesture(
GestureGroup(GestureMode.Exclusive,
LongPressGesture({ repeat: <span class="hljs-literal">false</span> })
.onAction((event: GestureEvent) => {
<span class="hljs-comment">// 长按</span>
<span class="hljs-keyword">this</span>.judgeGame(RuleType.longPress)
}),
TapGesture({ count: <span class="hljs-number">2</span> })
.onAction(() => {
<span class="hljs-comment">// 双击</span>
<span class="hljs-keyword">this</span>.judgeGame(RuleType.doubleClick)
})
)
}
}.forceRebuild(<span class="hljs-literal">false</span>)
}, item => item)
}
.columnsTemplate(<span class="hljs-string">'1fr 1fr 1fr'</span>)
.rowsTemplate(<span class="hljs-string">'1fr 1fr 1fr'</span>)
.columnsGap(<span class="hljs-number">10</span>)
.rowsGap(<span class="hljs-number">10</span>)
.width(<span class="hljs-string">'90%'</span>)
.height(<span class="hljs-string">'75%'</span>)
}.width(<span class="hljs-string">'80%'</span>).height(<span class="hljs-string">'70%'</span>)
3. 添加弹窗
1)通过 @CustomDialog 装饰器来创建自定义弹窗,使用方式可参考:
2)规则弹窗效果如下,弹窗组成由两个 Text 和两个 Image 竖向排列组成,所以我们可以在 build()下使用 Column 容器来包裹,组件代码如下;

图3 游戏规则
@CustomDialog
struct RuleDialog {
controller: CustomDialogController
confirm: () => <span class="hljs-keyword">void</span>
invite: () => <span class="hljs-keyword">void</span>
@Consume deviceList: RemoteDevice[]
build() {
Column() {
Text(<span class="hljs-string">'游戏规则'</span>).fontSize(<span class="hljs-number">30</span>).margin(<span class="hljs-number">20</span>)
Text(<span class="hljs-string">'炸弹会随机出现在9个方块内,需要在规定时间内完成指定操作(点击、双击或长按),即可将炸弹传递给下一个人,小心炸弹可是会越来越快的喔!'</span>)
.fontSize(<span class="hljs-number">24</span>).margin({ bottom: <span class="hljs-number">10</span> })
Image($r(<span class="hljs-string">"app.media.btn_start"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">80</span>).margin(<span class="hljs-number">10</span>)
.onClick(() => {
console.info(TAG + <span class="hljs-string">'Click start game'</span>)
<span class="hljs-keyword">if</span> (checkTrustedDevice(<span class="hljs-keyword">this</span>.remoteDeviceModel)) {
<span class="hljs-keyword">this</span>.controller.close()
<span class="hljs-keyword">this</span>.confirm()
}
})
Image($r(<span class="hljs-string">"app.media.btn_Invite"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">80</span>).margin(<span class="hljs-number">10</span>)
.onClick(() => {
<span class="hljs-keyword">this</span>.invite()
})
}.width(<span class="hljs-string">'90%'</span>)
.margin(<span class="hljs-number">20</span>)
.backgroundColor(Color.White)
}
}
3)在 @entry 创建 CustomDialogController 对象并传入弹窗所需参数,后面可通过该对象 open() 和 close() 方法进行打开和关闭弹窗;
@Provide deviceList: RemoteDevice[] = []
private ruleDialog: CustomDialogController = <span class="hljs-keyword">new</span> CustomDialogController({
builder: RuleDialog({
invite: () => <span class="hljs-keyword">this</span>.InvitePlayer(),
confirm: () => <span class="hljs-keyword">this</span>.startGame(),
deviceList: <span class="hljs-keyword">this</span>.deviceList
}),
autoCancel: <span class="hljs-literal">false</span>
})

图4 游戏失败弹窗动画
1)编写弹窗布局:将游戏失败文本、炸弹图片和再来一局按钮图片放置于 Column 容器中;
2)用变量来控制动画起始和结束的位置:用 Flex 容器包裹炸弹图片,并用 @State 装饰变量 toggle,通过变量来动态修改 [Flex]的direction 属性;
@State toggle: boolean = <span class="hljs-literal">true</span>
private controller: CustomDialogController
@Consume deviceList: RemoteDevice[]
private confirm: () => <span class="hljs-keyword">void</span>
private interval = <span class="hljs-literal">null</span>
build() {
Column() {
Text(<span class="hljs-string">'游戏失败'</span>).fontSize(<span class="hljs-number">30</span>).margin(<span class="hljs-number">20</span>)
Flex({
direction: <span class="hljs-keyword">this</span>.toggle ? FlexDirection.Column : FlexDirection.ColumnReverse,
alignItems: ItemAlign.Center
})
{
Image($r(<span class="hljs-string">"app.media.bomb"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">80</span>)
}.height(<span class="hljs-number">200</span>)
Image($r(<span class="hljs-string">"app.media.btn_restart"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">120</span>).margin(<span class="hljs-number">10</span>)
.onClick(() => {
<span class="hljs-keyword">this</span>.controller.close()
<span class="hljs-keyword">this</span>.confirm()
})
}
.width(<span class="hljs-string">'80%'</span>)
.margin(<span class="hljs-number">50</span>)
.backgroundColor(Color.White)
}
3)设置动画效果:使用 animateTo 显式动画接口炸弹位置切换时添加动画,并且设置定时器定时执行动画;
aboutToAppear() {
<span class="hljs-keyword">this</span>.setBombAnimate()
}
setBombAnimate() {
<span class="hljs-keyword">let</span> fun = () => {
<span class="hljs-keyword">this</span>.toggle = !<span class="hljs-keyword">this</span>.toggle;
}
<span class="hljs-keyword">this</span>.interval = setInterval(() => {
animateTo({ duration: <span class="hljs-number">1500</span>, curve: Curve.Sharp }, fun)
}, <span class="hljs-number">1600</span>)
}
分布式流转需要在同一网络下通过 DeviceManager 组件进行设备间发现和认证,获取到可信设备的 deviceId 调用 FeatureAbility.startAbility(parameter),即可把应用程序流转到另一设备。
原本分布式流转应用流程如下:
项目中将上面设备管理封装至 RemoteDeviceManager,通过 RemoteDeviceManager 的四个方法来动态维护 deviceList 设备信息列表。

图5 分布式流转
项目实现分布式流转只需如下流程:
1. 创建RemoteDeviceManager实例
1)导入 RemoteDeviceManager
import {RemoteDeviceManager} from <span class="hljs-string">'./RemoteDeviceManager'</span>
2)声明 @Provide 装饰的设备列表变量 deviceList,和创建 RemoteDeviceManager 实例。
@Provide deviceList: RemoteDevice[] = []
private remoteDm: RemoteDeviceManager = <span class="hljs-keyword">new</span> RemoteDeviceManager(<span class="hljs-keyword">this</span>.deviceList)
2. 刷新设备列表
在生命周期 aboutToAppear 中,调用刷新设备列表和开始发现设备。
aboutToAppear 定义:函数在创建自定义组件的新实例后,在执行其 build 函数之前执行。
aboutToAppear() {
<span class="hljs-keyword">this</span>.remoteDm.refreshRemoteDeviceList() <span class="hljs-comment">// 刷新设备列表</span>
<span class="hljs-keyword">this</span>.remoteDm.startDeviceDiscovery() <span class="hljs-comment">// 开始发现设备</span>
}
3. 设备认证
invitePlayer(remoteDevice:RemoteDevice) {
<span class="hljs-keyword">if</span> (remoteDevice.status == RemoteDeviceStatus.ONLINE) {
prompt.showToast({ message: <span class="hljs-string">"Already invited!"</span> })
<span class="hljs-keyword">return</span>
}
<span class="hljs-keyword">this</span>.remoteDm.authDevice(remoteDevice).then(() => {
prompt.showToast({ message: <span class="hljs-string">"Invite success! deviceName="</span> + remoteDevice.deviceName })
}).catch(() => {
prompt.showToast({ message: <span class="hljs-string">"Invite fail!"</span> })
})
}
4. 跨设备流转
从 deviceList 中获取设备列表在线的设备 Id,通过 featureAbility.startAbility 进行流转。
async startAbilityRandom() {
<span class="hljs-keyword">let</span> deviceId = <span class="hljs-keyword">this</span>.getRandomDeviceId() <span class="hljs-comment">// 随机获取设备id</span>
CommonLog.info(<span class="hljs-string">'featureAbility.startAbility deviceId='</span> + deviceId);
<span class="hljs-keyword">let</span> bundleName = await getBundleName()
<span class="hljs-keyword">let</span> wantValue = {
bundleName: bundleName,
abilityName: <span class="hljs-string">'com.sample.bombgame.MainAbility'</span>,
deviceId: deviceId,
parameters: {
ongoing: <span class="hljs-literal">true</span>,
transferNumber: <span class="hljs-keyword">this</span>.transferNumber + <span class="hljs-number">1</span>
}
};
featureAbility.startAbility({
want: wantValue
}).then((data) => {
CommonLog.info(<span class="hljs-string">' featureAbility.startAbility finished, '</span> + <span class="hljs-built_in">JSON</span>.stringify(data));
featureAbility.terminateSelf((error) => {
CommonLog.info(<span class="hljs-string">'terminateSelf finished, error='</span> + error);
});
});
}
5. 注销监听
在声明周期 aboutToDisappear 进行注销监听。
aboutToDisappear 定义:函数在自定义组件析构消耗之前执行。
aboutToDisappear() {
<span class="hljs-keyword">this</span>.remoteDm.stopDeviceDiscovery() <span class="hljs-comment">// 注销监听</span>
}
1. 开始游戏
startGame() {
CommonLog.info(<span class="hljs-string">'startGame'</span>);
<span class="hljs-keyword">this</span>.randomTouchRule() <span class="hljs-comment">// 随机游戏点击规则</span>
<span class="hljs-keyword">this</span>.setRandomBomb() <span class="hljs-comment">// 随机生成炸弹位置</span>
<span class="hljs-keyword">this</span>.stopCountDown() <span class="hljs-comment">// 停止倒计时</span>
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.transferNumber < <span class="hljs-number">10</span>) {
<span class="hljs-keyword">this</span>.duration = <span class="hljs-number">3000</span> - <span class="hljs-keyword">this</span>.transferNumber * <span class="hljs-number">100</span>
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">this</span>.duration = <span class="hljs-number">2000</span>
}
<span class="hljs-keyword">const</span> interval: number = <span class="hljs-number">500</span>
<span class="hljs-comment">// 开始倒计时</span>
<span class="hljs-keyword">this</span>.timer = setInterval(() => {
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.duration <= interval) {
<span class="hljs-keyword">this</span>.duration = <span class="hljs-number">0</span>
clearInterval(<span class="hljs-keyword">this</span>.timer)
<span class="hljs-keyword">this</span>.timer = <span class="hljs-literal">null</span>
<span class="hljs-keyword">this</span>.gameFail()
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">this</span>.duration -= interval
}
}, interval)
}
2. 判断输赢
编写判断逻辑,用于不同的点击事件中调用。
/**
* 判断游戏输赢
* @param operation 点击类型
*/
judgeGame(operation:RuleType) {
this.stopCountDown()
if (operation != this.ruleText) {
this.gameFail()
} else {
prompt.showToast({ message: "finish" })
this.bombIndex = -1
this.startAbilityRandom()
}
}
3. 游戏失败
游戏失败,弹出游戏失败弹框。
gameFail() {
prompt.showToast({
message: <span class="hljs-string">'Game Fail'</span>
})
CommonLog.info(<span class="hljs-string">'gameFail'</span>);
<span class="hljs-keyword">this</span>.gameFailDialog.open()
}
四、项目下载和导入
项目仓库地址:
https://gitee.com/openharmony-sig/knowledge_demo_temp/tree/master/FA/Entertainment/BombGame
1)git下载
git clone https:<span class="hljs-comment">//gitee.com/openharmony-sig/knowledge_demo_temp.git</span>
2)项目导入
打开 DevEco Studio,点击 File->Open->下载路径/FA/Entertainment/BombGame
1. 设备编译约束
2. 应用编译约束

扫码添加开发者小助手微信
获取更多HarmonyOS开发资源和开发者活动资讯
有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
我有一个涉及多台机器、消息队列和事务的问题。因此,例如用户点击网页,点击将消息发送到另一台机器,该机器将付款添加到用户的帐户。每秒可能有数千次点击。事务的所有方面都应该是容错的。我以前从未遇到过这样的事情,但一些阅读表明这是一个众所周知的问题。所以我的问题。我假设安全的方法是使用两阶段提交,但协议(protocol)是阻塞的,所以我不会获得所需的性能,我是否正确?我通常写Ruby,但似乎Redis之类的数据库和Rescue、RabbitMQ等消息队列系统对我的帮助不大——即使我实现某种两阶段提交,如果Redis崩溃,数据也会丢失,因为它本质上只是内存。所有这些让我开始关注erlang和
我的工作要求我为某些测试自动生成电子邮件。我一直在四处寻找,但未能找到可以快速实现的合理解决方案。它需要在outlook而不是其他邮件服务器中,因为我们有一些奇怪的身份验证规则,我们需要保存草稿而不是仅仅发送邮件的选项。显然win32ole可以做到这一点,但我找不到任何相当简单的例子。 最佳答案 假设存储了Outlook凭据并且您设置为自动登录到Outlook,WIN32OLE可以很好地完成此操作:require'win32ole'outlook=WIN32OLE.new('Outlook.Application')message=
//1.验证返回状态码是否是200pm.test("Statuscodeis200",function(){pm.response.to.have.status(200);});//2.验证返回body内是否含有某个值pm.test("Bodymatchesstring",function(){pm.expect(pm.response.text()).to.include("string_you_want_to_search");});//3.验证某个返回值是否是100pm.test("Yourtestname",function(){varjsonData=pm.response.json
在前面两节的例子中,主界面窗口的尺寸和标签控件显示的矩形区域等,都是用C++代码编写的。窗口和控件的尺寸都是预估的,控件如果多起来,那就不好估计每个控件合适的位置和大小了。用C++代码编写图形界面的问题就是不直观,因此Qt项目开发了专门的可视化图形界面编辑器——QtDesigner(Qt设计师)。通过QtDesigner就可以很方便地创建图形界面文件*.ui,然后将ui文件应用到源代码里面,做到“所见即所得”,大大方便了图形界面的设计。本节就演示一下QtDesigner的简单使用,学习拖拽控件和设置控件属性,并将ui文件应用到Qt程序代码里。使用QtDesigner设计界面在开始菜单中找到「Q
给定一个nxmbool数组:[[true,true,false],[false,true,true],[false,true,true]]有什么简单的方法可以返回“该列中有多少个true?”结果应该是[1,3,2] 最佳答案 使用转置得到一个数组,其中每个子数组代表一列,然后将每一列映射到其中的true数:arr.transpose.map{|subarr|subarr.count(true)}这是一个带有inject的版本,应该在1.8.6上运行,没有任何依赖:arr.transpose.map{|subarr|subarr.in
修改(澄清问题)我已经花了几天时间试图弄清楚如何从Facebook游戏中抓取特定信息;但是,我遇到了一堵又一堵砖墙。据我所知,主要问题如下。我可以使用Chrome的检查元素工具手动查找我需要的html-它似乎位于iframe中。但是,当我尝试抓取该iframe时,它是空的(属性除外):如果我使用浏览器的“查看页面源代码”工具,这与我看到的输出相同。我不明白为什么我看不到iframe中的数据。答案不是它是由AJAX之后添加的。(我知道这既是因为“查看页面源代码”可以读取Ajax添加的数据,也是因为我有b/c我一直等到我可以看到数据页面之后才抓取它,但它仍然不存在)。发生这种情况是因为
我正在编写一个简单的日志嗅探器,它将在日志中搜索表明我支持的软件存在问题的特定错误。它允许用户指定日志路径并指定他们想要搜索多少天前。如果用户关闭日志滚动,日志文件有时会变得非常大。目前我正在做以下事情(虽然还没有完成):File.open(@log_file,"r")do|file_handle|file_handle.eachdo|line|ifline.match(/\d+++-\d+-\d+/)etc...line.match显然会查找我们在日志中使用的日期格式,其余逻辑将在下面。但是,有没有更好的方法来搜索没有.each_line的文件?如果没有,我完全同意。我只是想确保我使