HarmonyOS 分布式亲子教育

系统 分布式 OpenHarmony
本篇Codelab通过一个亲子早教系统,完整的为您介绍了早教算数题和益智拼图游戏两个综合案例,旨在帮助您快速了解HarmonyOS应用开发、多屏互动、分布式跨设备协同的功能了解。

[[412544]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

1. 项目介绍

远程教育,多屏协同是智慧教育的一个重要场景。本篇Codelab通过一个亲子早教系统,完成了分布式早教算数题和分布式拼图游戏两个综合案例,旨在帮助开发者快速了解HarmonyOS应用开发、多屏互动和分布式跨设备协同的体验。

本篇Codelab将为您重点介绍Page Ability、Service Ability、Intent以及分布式任务调度、公共事件等。同时我们还将为您介绍多屏互动,分布式跨设备协同、绘图画布的使用、经典拼图算法。正式介绍之前,我们先对亲子早教系统进行展示,让您快速了解到本篇Codelab所实现的功能。

功能1:早教算数题

点击早教算数题,系统会为您随机出一道两位数的加法,点击实时辅导会拉起两个画布,本地端可以用黑色笔迹进行草稿运算,远程端可以用红色笔迹进行实时指导,操作步骤两端实时同步,效果图如下所示。

HarmonyOS 分布式亲子教育-鸿蒙HarmonyOS技术社区

功能2:益智拼图游戏

点击益智拼图游戏会拉起一个九宫格的拼图游戏(图片会随机乱序),点击图片可以进行拼图。在本地端点击亲子协同,远程端会拉起一个一模一样的游戏页面,两个设备可以进行实时互动,同步对图片进行拼接,操作步骤亦可实时同步,效果图如下所示。

HarmonyOS 分布式亲子教育-鸿蒙HarmonyOS技术社区

想知道上述两个亲子游戏是如何实现的吗?快来跟随我们的Codelab进行学习吧。

2. 搭建HarmonyOS环境

搭建HarmonyOS环境

  • 安装DevEco Studio,详情请参考DevEco Studio下载。
  • 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境
  1. 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作
  2. 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境

说明:

如需要在手机中运行程序,则需要提前申请证书,如使用模拟器可忽略

准备密钥和证书请求文件

申请调试证书

你可以通过如下设备完成本CodeLab:

开启开发者模式的HarmonyOS真机

DevEco Studio中的手机模拟器(模拟器暂不支持分布式调试)

3. 代码结构解读

本篇Codelab我们只是对核心代码进行讲解,您可以在最后的参考中下载完整代码,首先来介绍下整个工程的代码结构:

HarmonyOS 分布式亲子教育-鸿蒙HarmonyOS技术社区
  • devices:封装了选择设备的Dialog,您可直接调用SelectDeviceDialog里面的相关方法。
  • point:封装了绘图的相关功能,您可直接调用DrawPoint里面的相关方法。
  • slice:MainAbilitySlice为应用主页面,MathGameAbilitySlice为早教算数题主页面,MathDrawRemSlice为早教算数题绘图页面,PictureGameAbilitySlice为拼图游戏主页面。
  • utils:封装了公共方法和公共数据。
  • MathGameServiceAbility、PictureGameServiceAbility:供远端连接的Service Ability。
  • resources:存放工程使用到的资源文件,其中resources\base\layout下存放xml布局文件;resources\base\media下存放图片资源。
  • config.json:配置文件。

4. 亲子早教系统页面流转

亲子早教系统主页面和各个游戏页面的布局如下图所示,首先给大家介绍一下相关的布局代码。

HarmonyOS 分布式亲子教育-鸿蒙HarmonyOS技术社区

首页的布局文件为ability_main.xml、页面控制逻辑MainAbilitySlice;早教系统的布局文件为math_game.xml、页面控制逻辑MathGameAbilitySlice;拼图游戏的布局文件为ability_picture.xml、页面控制逻辑PictureGameAbilitySlice。要实现页面的相互流转首先需要在MainAbility中设置路由,并在MainAbilitySlice中实现按钮的点击事件。

以跳转早教算数题为例,首先需要在MainAbility中设置路由,代码如下所示:

  1. addActionRoute(CommonData.MATH_PAGE, MathGameAbilitySlice.class.getName()); 

 而后,需要在MainAbilitySlice中添加点击事件,代码如下所示:

  1. private void mathGame() {  
  2.     LogUtil.info(TAG, "Click ResourceTable Id_math_game");  
  3.     Intent mathGameIntent = new Intent();  
  4.     Operation operationMath = new Intent.OperationBuilder()  
  5.             .withBundleName(getBundleName())  
  6.             .withAbilityName(CommonData.ABILITY_MAIN)  
  7.             .withAction(CommonData.MATH_PAGE)  
  8.             .build();  
  9.     mathGameIntent.setOperation(operationMath);  
  10.     startAbility(mathGameIntent);  

 至此,您已实现跳转到早教算数题和益智拼图游戏两个页面了:

  • 早教算数题的出题逻辑可以参考setQuestion和checkAnswer两个方法
  • 拼图游戏图片乱序、移动图片、判断游戏结束、重新开始可分别参考pictureRandom、moveFun、gameOverFun、restartFun相关方法

因篇幅有限且相关游戏算法不属于本篇Codelab想为您重点介绍的HarmonyOS特性,因此不再赘述,您可自行调用相关函数、理解相关代码实现游戏功能,附件代码中也已进行了详细的注释。以上即完成了单机版本的HarmonyOS应用开发,接下来我们将为您重点介绍基于HarmonyOS分布式能力的相关系统设计。

5. 发现设备和建立连接

在早教算数题中点击实时辅导或者在拼图游戏中点击亲子协同,会弹出选择设备页面,如下图所示。我们已经在devices目录下为您封装好了选择设备的dialog,具体代码请参考DevicesListAdapter(设备列表适配器)、item_device_list.xml (设备列表item布局)、SelectDeviceDialog(选择设备列表的弹窗)、dialog_select_device.xml(弹窗布局)。

HarmonyOS 分布式亲子教育-鸿蒙HarmonyOS技术社区

SelectDeviceDialog中选择设备弹窗的构造函数如下,需要传入三个参数:上下文、设备列表、选择结果的回调事件。在业务代码中调用如下构造函数即可打开选择设备的弹窗,代码如下所示:

  1. public SelectDeviceDialog(Context context, List<DeviceInfo> devices, SelectResultListener listener) {  
  2.     initView(context, devices, listener);  

 其中List devices可以通过如下函数去获取,代码如下所示:

  1. private void getDevices() {  
  2.     if (devices.size() > 0) {  
  3.         devices.clear();  
  4.     }  
  5.     List<DeviceInfo> deviceInfos =  
  6.             DeviceManager.getDeviceList(ohos.distributedschedule.interwork.DeviceInfo.FLAG_GET_ONLINE_DEVICE);  
  7.     LogUtil.info(TAG, "deviceInfos size is :" + deviceInfos.size());  
  8.     devices.addAll(deviceInfos);  
  9.     showDevicesDialog();  

 选择结果的回调事件可以根据业务的不同传入不同的回调事件,例如早教算数题中选择设备后,需要拉起本地端和远程端两个画布,此订阅事件为startLocalFa和startRemoteFa,代码如下所示:

  1. private void showDevicesDialog() {  
  2.     new SelectDeviceDialog(this, devices, deviceInfo -> {  
  3.         startLocalFa(deviceInfo.getDeviceId());  
  4.         startRemoteFa(deviceInfo.getDeviceId());  
  5.     }).show();  

 而拼图游戏需要和另外一台设备建立连接,拉起另外一台设备的拼图页面,此回调事件为connectRemotePa,代码如下所示:

  1. private void showDevicesDialog() {  
  2.     new SelectDeviceDialog(this, devices, deviceInfo -> {  
  3.         connectRemotePa(deviceInfo.getDeviceId(), PictureRemoteProxy.REQUEST_START_ABILITY);  
  4.     }).show();  

 需要说明的是回调函数中deviceInfo是单击选中的设备信息,具体实现在SelectDeviceDialog的initView方法中,代码如下所示:

  1. DevicesListAdapter devicesListAdapter = new DevicesListAdapter(devices, context);  
  2. devicesListContainer.setItemProvider(devicesListAdapter);  
  3. devicesListContainer.setItemClickedListener((listContainer, component, position, l) -> {  
  4.     listener.callBack(devices.get(position));  
  5.     commonDialog.hide();  
  6. }); 

 至此,您已经完成了多设备协同中发现设备和建立连接这一关键步骤。

说明:

实现远程启动FA,需要至少两个设备处于同一个分布式网络中,可以通过如下操作实现:

所有设备接入同一网络;

所有设备登录相同华为账号;

所有设备上开启"设置->更多连接->多设备协同 "。

6. 早教算数题

点击早教算数题,系统会为您随机出一道两位数的加法,点击实时辅导会拉起两个画布,本地端可以用黑色笔迹进行草稿运算,远程端可以用红色笔迹进行实时指导,操作步骤两端实时同步,效果如下图所示。接下来我们将为您详细介绍如何利用HarmonyOS分布式技术实现两端的同步绘制。

HarmonyOS 分布式亲子教育-鸿蒙HarmonyOS技术社区

Step 1 - 选择设备

详见"5 发现设备和建立连接"。

Step 2 - 建立连接

点击选择设备后会进入绘图页面,在onStart方法中会调用初始化并连接设备的函数initAndConnectDevice,通过intent传参获取远程设备的remoteDeviceId,并调用connectRemotePa进行连接,代码如下所示:

  1. private void initAndConnectDevice(Intent intent) {  
  2.     // 页面初始化  
  3.     this.context = MathDrawRemSlice.this;  
  4.     String remoteDeviceId = intent.getStringParam(CommonData.KEY_REMOTE_DEVICEID);  
  5.     isLocal = intent.getBooleanParam(CommonData.KEY_IS_LOCAL, false);  
  6.     if (findComponentById(ResourceTable.Id_text_title) instanceof Text) {  
  7.         Text textTitle = (Text) findComponentById(ResourceTable.Id_text_title);  
  8.         textTitle.setText(isLocal ? "本地端" : "远程端");  
  9.     }  
  10.   
  11.     // 连接远程服务  
  12.     if (!remoteDeviceId.isEmpty()) {  
  13.         connectRemotePa(remoteDeviceId);  
  14.     } else {  
  15.         LogUtil.info(TAG, "localDeviceId is null");  
  16.     }  

 其中调用connectRemotePa方法后会和MathGameServiceAbility建立服务连接,代码如下所示:

  1. Intent connectPaIntent = new Intent();  
  2. Operation operation = new Intent.OperationBuilder()  
  3.         .withDeviceId(deviceId)  
  4.        .withBundleName(getBundleName())  
  5.        .withAbilityName(CommonData.MATH_GAME_SERVICE_NAME)  
  6.         .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)  
  7.         .build();  
  8. connectPaIntent.setOperation(operation); 

 连接成功后会回调onAbilityConnectDone方法,此时两台设备即建立了连接。而后,可以调用senDataToRemote方法进行数据发送,代码如下所示:

  1. public void onAbilityConnectDone(ElementName elementName, IRemoteObject remote, int resultCode) {  
  2.     LogUtil.info(TAG, "onAbilityConnectDone......");  
  3.     connectAbility(elementName, remote, requestType);  
  4. }  
  5.    
  6. private void connectAbility(ElementName elementName, IRemoteObject remote, int requestType) {  
  7.     proxy = new PictureRemoteProxy(remote);  
  8.     LogUtil.error(TAG, "connectRemoteAbility done");  
  9.     if (proxy != null) {  
  10.         try {  
  11.             proxy.senDataToRemote(requestType);  
  12.         } catch (RemoteException e) {  
  13.             LogUtil.error(TAG, "onAbilityConnectDone RemoteException");  
  14.         }  
  15.     }  

 Step 3 - 绘图操作(具体业务,准备要发送的数据)

DrawPoint是一个绘图的工具类,绘制一个点会记录三个信息:X轴坐标、Y轴坐标、是否是最后一个点。将此信息记录到数组pointsX、pointsY、isLastPointArray中,并封装为List allPoints的数据类型,定义如下:

  1. private float[] pointsX;  
  2. private float[] pointsY;  
  3. private boolean[] isLastPointArray;  
  4. private List<MyPoint> allPoints = new ArrayList<>(); 

 其中,是否为最后一个点是通过TouchEvent.PRIMARY_POINT_UP事件进行区分的。当检测到该事件时,会调用回调函数callBack.callBack(allPoints) ,代码如下所示:

  1. if (touchEvent.getAction() == TouchEvent.PRIMARY_POINT_UP) {  
  2.     point.setLastPoint(true);  
  3.     allPoints.add(point);  
  4.     callBack.callBack(allPoints);  
  5. }  

 此时会向上回调到setOnDrawBack方法,代码如下所示:

  1. public void setOnDrawBack(OnDrawCallBack callBack) {  
  2.     this.callBack = callBack;  

 MathDrawRemSlice中initDraw()会初始化画布,一次绘制完成后会回调setOnDrawBack方法,其中points是上述步骤中绘制的点,将其存入数组pointsX、pointsY、isLastPointArray中,代码如下所示:

  1. drawl.setOnDrawBack(points -> {  
  2.     if (points != null && points.size() > 1) {  
  3.         pointsX = new float[points.size()];  
  4.         pointsY = new float[points.size()];  
  5.         isLastPoint = new boolean[points.size()];  
  6.         for (int i = 0; i < points.size(); i++) {  
  7.            pointsX[i] = points.get(i).getPositionX();  
  8.            pointsY[i] = points.get(i).getPositionY();  
  9.            isLastPoint[i] = points.get(i).isLastPoint();  
  10.         }  
  11.         ...  
  12.     }  
  13. }); 

 此时就完成了本地画布绘制并将绘制的点信息存放到了数组pointsX、pointsY、isLastPointArray中,即已经准备好了要发送的数据。

Step 4 - 发送数据

发送数据涉及到Service Ability、分布式任务调度、公共事件三项HarmonyOS能力,如果您还不熟悉相关基础知识可以先参考官方文档进行学习。一次绘制完成后,会调用senDataToRemote方法,将绘制的点信息(pointsX、pointsY、isLastPointArray)存放到data中并发送出去,代码如下所示:

  1. private void senDataToRemote(int requestType) throws RemoteException {  
  2.     LogUtil.info(TAG, "send data to local draw service");  
  3.     MessageParcel data = MessageParcel.obtain();  
  4.     MessageParcel reply = MessageParcel.obtain();  
  5.     MessageOption option = new MessageOption(MessageOption.TF_SYNC);  
  6.     try {  
  7.         if (pointsX != null && pointsY != null && isLastPoint != null) {  
  8.            data.writeFloatArray(pointsX);  
  9.            data.writeFloatArray(pointsY);  
  10.            data.writeBooleanArray(isLastPoint);  
  11.         }  
  12.         remote.sendRequest(requestType, data, reply, option);  
  13.         int ec = reply.readInt();  
  14.         if (ec != ERR_OK) {  
  15.             LogUtil.error(TAG, "RemoteException:");  
  16.         }  
  17.     } catch (RemoteException e) {  
  18.         LogUtil.error(TAG, "RemoteException:");  
  19.     } finally {  
  20.         data.reclaim();  
  21.         reply.reclaim();  
  22.     }  

 其中,remote.sendRequest是将数据发送到MathGameServiceAbility的服务中(因为步骤2是和MathGameServiceAbility建立服务连接),建立服务连接后会回调onRemoteRequest方法,代码如下所示:

  1. @Override  
  2. public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {  
  3.     LogUtil.info(TAG, "onRemoteRequest......");  
  4.     float[] pointsX = data.readFloatArray();  
  5.     float[] pointsY = data.readFloatArray();  
  6.     boolean[] isLastPointArray = data.readBooleanArray();  
  7.     reply.writeInt(ERR_OK);  
  8.     sendEvent(isLastPointArray, pointsX, pointsY);  
  9.     return true;  

 在onRemoteRequest方法中,会调用sendEvent将数组pointsX、pointsY、isLastPointArray发送出去。sendEvent是通过公共事件的方式进行数据传递的,其注册的公共事件是CommonData.MATH_DRAW_EVENT,代码如下所示:

  1. private void sendEvent(boolean[] isLastPoint, float[] pointsX, float[] pointsY) {  
  2.     LogUtil.info(TAG, "sendEvent......");  
  3.     try {  
  4.         Intent intent = new Intent();  
  5.         Operation operation = new Intent.OperationBuilder()  
  6.                 .withAction(CommonData.MATH_DRAW_EVENT)  
  7.                 .build();  
  8.         intent.setOperation(operation);  
  9.        intent.setParam(CommonData.KEY_POINT_X, pointsX);  
  10.        intent.setParam(CommonData.KEY_POINT_Y, pointsY);  
  11.        intent.setParam(CommonData.KEY_IS_LAST_POINT, isLastPoint);  
  12.         CommonEventData eventData = new CommonEventData(intent);  
  13.         CommonEventManager.publishCommonEvent(eventData);  
  14.     } catch (RemoteException e) {  
  15.         LogUtil.error(TAG, "publishCommonEvent occur exception.");  
  16.     }  

Step 5 - 接收数据

MathDrawRemSlice中会订阅CommonData.MATH_DRAW_EVENT的公共事件,代码如下所示:

  1. private void subscribe() {  
  2.     MatchingSkills matchingSkills = new MatchingSkills();  
  3.     matchingSkills.addEvent(CommonData.MATH_DRAW_EVENT);  
  4.     matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON);  
  5.     CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);  
  6.     subscriber = new MyCommonEventSubscriber(subscribeInfo);  
  7.     try {  
  8.         CommonEventManager.subscribeCommonEvent(subscriber);  
  9.     } catch (RemoteException e) {  
  10.         LogUtil.error("""subscribeCommonEvent occur exception.");  
  11.     }  

当订阅到相关事件时会回调onReceiveEvent方法,此时即可将pointsX、pointsY、isLastPointArray解析出来,然后调用drawl.setDrawParams(isLastPointArray, pointsX, pointsY)方法在远程端进行绘制,代码如下所示:

  1. public void onReceiveEvent(CommonEventData commonEventData) {  
  2.     Intent intent = commonEventData.getIntent();  
  3.     pointsX = intent.getFloatArrayParam(CommonData.KEY_POINT_X);  
  4.     pointsY = intent.getFloatArrayParam(CommonData.KEY_POINT_Y);  
  5.     isLastPoint = intent.getBooleanArrayParam(CommonData.KEY_IS_LAST_POINT);  
  6.     // 接收数据后,对远程端画布进行绘制  
  7.     drawl.setDrawParams(isLastPoint, pointsX, pointsY);  
  8.     LogUtil.info(TAG, "onReceiveEvent.....");  

Step 6 - 数据的双向通信

步骤1-5中为您讲解了数据的单向通信,即本地端向远程端的交互。本例的绘图是双向通信,两端都可以进行绘制,这是怎么做到的呢?原来在步骤1中选择设备时,会调用startLocalFa和startRemoteFa,其中startLocalFa传出的remoteDeviceId是所选择的设备ID,startRemoteFa传出的remoteDeviceId是当前设备的ID,由此实现了双向通信,代码如下所示:

  1. private void showDevicesDialog() {  
  2.     new SelectDeviceDialog(this, devices, deviceInfo -> {  
  3.         startLocalFa(deviceInfo.getDeviceId());  
  4.         startRemoteFa(deviceInfo.getDeviceId());  
  5.     }).show();  
  6. }  
  7. private void startLocalFa(String deviceId) {  
  8.     LogUtil.info(TAG, "startLocalFa......");  
  9.     Intent intent = new Intent();  
  10.     intent.setParam(CommonData.KEY_REMOTE_DEVICEID, deviceId);  
  11.     ...  
  12. }  
  13. private void startRemoteFa(String deviceId) {  
  14.     LogUtil.info(TAG, "startRemoteFa......");  
  15.     String localDeviceId = KvManagerFactory.getInstance()  
  16.             .createKvManager(new KvManagerConfig(this)).getLocalDeviceInfo().getId();  
  17.     Intent intent = new Intent();  
  18.     intent.setParam(CommonData.KEY_REMOTE_DEVICEID, localDeviceId);  
  19.     ...  

通过选择设备、建立连接、发送数据、接收数据这几个关键步骤,您就可以实现两台设备的同步绘图、实时显示的功能。

—-结束

7. 益智拼图游戏

点击益智拼图游戏会拉起一个九宫格的拼图游戏(图片会随机乱序),点击图片可以进行拼图。在本地端中点击亲子协同,远程端会拉起一个一模一样的游戏页面,两个设备可以进行实时互动,同步对图片进行拼接,操作步骤亦可实时同步,效果图如下所示。接下来我们将为您详细介绍如何利用HarmonyOS分布式技术实现一个益智拼图游戏。

HarmonyOS 分布式亲子教育-鸿蒙HarmonyOS技术社区

Step 1 - 选择设备

详见"5 发现设备和建立连接"这一章节。

Step 2 - 建立连接并拉起设备

点击亲子协同后会记录选择设备的deviceId,调用connectRemotePa方法后会和PictureGameServiceAbility建立服务连接,其中标识位传递的是REQUEST_START_ABILITY,代码如下所示:

  1. private void showDevicesDialog() {  
  2.     new SelectDeviceDialog(this, devices, deviceInfo -> {  
  3.         connectRemotePa(deviceInfo.getDeviceId(), PictureRemoteProxy.REQUEST_START_ABILITY);  
  4.     }).show();  
  5. }  
  6. private void connectRemotePa(String deviceId, int requestType) {  
  7.     if (!deviceId.isEmpty()) {  
  8.         Intent connectPaIntent = new Intent();  
  9.         Operation operation = new Intent.OperationBuilder()  
  10.                 .withDeviceId(deviceId)  
  11.                 .withBundleName(getBundleName())  
  12.                 .withAbilityName(CommonData.PICTURE_GAME_SERVICE_NAME)  
  13.                 .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)  
  14.                 .build();  
  15.         connectPaIntent.setOperation(operation);  
  16.     ...  

连接成功后会回调onAbilityConnectDone方法,此时两台设备即建立了连接。然后调用sendDataToRemote方法进行数据发送,其中requestType为REQUEST_START_ABILITY,代码如下所示:

  1. public void onAbilityConnectDone(ElementName elementName, IRemoteObject remote, int resultCode) {  
  2.     LogUtil.info(TAG, "onAbilityConnectDone......");  
  3.     connectAbility(elementName, remote, requestType);  
  4. }  
  5.    
  6. private void connectAbility(ElementName elementName, IRemoteObject remote, int requestType) {  
  7.     proxy = new PictureRemoteProxy(remote);  
  8.     LogUtil.error(TAG, "connectRemoteAbility done");  
  9.     if (proxy != null) {  
  10.         try {  
  11.             proxy.sendDataToRemote(requestType);  
  12.         } catch (RemoteException e) {  
  13.             LogUtil.error(TAG, "onAbilityConnectDone RemoteException");  
  14.         }  
  15.     }  

PictureGameServiceAbility服务中接收到了senDataToRemote的消息后会回调onRemoteRequest,因为此时code为REQUEST_START_ABILITY,所以会拉起远程端的拼图页面,代码如下所示:

  1. public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {  
  2.     ...  
  3.     if (code == REQUEST_START_ABILITY) {  
  4.         LogUtil.error(TAG, "RemoteServiceAbility::isFirstStart:");  
  5.         Intent secondIntent = new Intent();  
  6.         Operation operation = new Intent.OperationBuilder().withDeviceId("")  
  7.                 .withBundleName(getBundleName())  
  8.                 .withAbilityName(CommonData.ABILITY_MAIN)  
  9.                 .withAction(CommonData.PICTURE_PAGE)  
  10.                 .build();  
  11.     ...  
  12.     } else {  
  13.     ...  
  14.     }  
  15.     ...  

以上即完成了本地端到远程端的单向通信。远程端设备拉起后,会在PictureGameAbilitySlice执行onStart方法,进而执行initRemoteView方法,其中会再次调用connectRemotePa方法,此时传递的标识位为REQUEST_SEND_DATA,不会重复拉起本地端设备的页面,只会建立数据连接,代码如下所示:

  1. private void initRemoteView(Intent intent) {  
  2.     if (!isLocal) {  
  3.         remoteDeviceId = intent.getStringParam(CommonData.KEY_REMOTE_DEVICEID);  
  4.         connectRemotePa(remoteDeviceId, PictureRemoteProxy.REQUEST_SEND_DATA);  
  5.         if (imageIndex != null) {  
  6.             updateDataInfo(intent);  
  7.         }  
  8.     }  

如上步骤即完成了本地端和远程端的双向通信,实现了数据互传的功能。

Step 3 - 拼图操作(具体业务,准备要发送的数据)

因篇幅有限且拼图游戏不属于本篇Codelab想为您重点介绍的HarmonyOS特性,因此不再赘述,请读者自行理解。我们告诉您的结论是,完成一次拼图操作需要记录三个关键数据:移动图片的下标moveImageId,移动图片的位置movePosition和最终排列的图片下标imageIndex。点击一次拼图,我们就会记录以上三个数据,代码如下所示:

  1. private class ImageClick implements Component.ClickedListener {  
  2.     @Override  
  3.     public void onClick(Component component) {  
  4.         int imageId = component.getId();  
  5.         for (int position = 0; position < imageIndex.length; position++) {  
  6.             if (imageId == imageResourceTable[position]) {  
  7.                 // 完成图片移动,并记录移动信息  
  8.                 moveFun(imageId, position);  
  9.                 moveImageId = imageId;  
  10.                 movePosition = position;  
  11.             }  
  12.         }  
  13.         // 刷新页面显示  
  14.         setImageAndDecodeBounds(imageIndex);  
  15.         // 发送数据  
  16.         senDataToRemoteFun();  
  17.     }  

Step 4 - 发送数据

发送数据的流程和早教算数题一样,我们再次为您做详细介绍,您可参考早教算数题进行对照学习。一次拼图完成后,会调用senDataToRemote方法,将关键信息(imageIndex、moveImageId、movePosition)存放到data中并发送出去,代码如下所示:

  1. private void senDataToRemote(int requestType) throws RemoteException {  
  2.     MessageParcel data = MessageParcel.obtain();  
  3.      ...  
  4.     try {  
  5.      ...  
  6.         data.writeIntArray(imageIndex);  
  7.         data.writeInt(moveImageId);  
  8.         data.writeInt(movePosition);  
  9.         remote.sendRequest(requestType, data, reply, option);  
  10.      ...  
  11.     } catch (RemoteException e) {  
  12.      ...  
  13.     } finally {  
  14.      ...  
  15.     }  

其中,remote.sendRequest是将数据发送到PictureGameServiceAbility的服务中(因为步骤2是和PictureGameServiceAbility建立服务连接),建立服务连接后会回调onRemoteRequest方法。接收顺序应该和发送顺序一致(imageIndex、moveImageId、movePosition),否则会操作接收错误的情况,代码如下所示:

  1. public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {  
  2.     LogUtil.info(TAG, "onRemoteRequest......");  
  3.     int[] imageIndex = data.readIntArray();  
  4.     int moveImageId = data.readInt();  
  5.     int movePosition = data.readInt();  
  6.      ...  
  7.     LogUtil.info(TAG, "receive number:" + imageIndex.length);  
  8.     reply.writeInt(ERR_OK);  
  9.     if (code == REQUEST_START_ABILITY) {  
  10.      ...  
  11.     } else {  
  12.         sendEvent(imageIndex, moveImageId, movePosition);  
  13.     }  
  14.     return true;  

在onRemoteRequest方法中,会调用sendEvent将imageIndex、moveImageId、movePosition发送出去。sendEvent是通过注册公共事件的方式进行数据传递的,其注册的公共事件是CommonData. PICTURE_GAME_EVENT,代码如下所示:

  1. private void sendEvent(int[] imageIndex, int moveImageId, int movePosition) {  
  2.     try {  
  3.         Intent intent = new Intent();  
  4.         Operation operation = new Intent.OperationBuilder()  
  5.                 .withAction(CommonData.PICTURE_GAME_EVENT)  
  6.                 .build();  
  7.         intent.setOperation(operation);  
  8.         intent.setParam(CommonData.KEY_IMAGE_INDEX, imageIndex);  
  9.         intent.setParam(CommonData.KEY_MOVE_IMAGE_ID, moveImageId);  
  10.         intent.setParam(CommonData.KEY_MOVE_POSITION, movePosition);  
  11.         CommonEventData eventData = new CommonEventData(intent);  
  12.         CommonEventManager.publishCommonEvent(eventData);  
  13.     } catch (RemoteException e) {  
  14.         LogUtil.error(TAG, "publishCommonEvent occur exception.");  
  15.     }  

Step 5 - 接收数据

接收数据的代码流程和早教算数题一样,我们再次为您做详细介绍,您可参考早教算数题进行对照学习。PictureGameAbilitySlice会订阅CommonData. PICTURE_GAME_EVENT的公共事件,代码如下所示:

  1. private void subscribe() {  
  2.     MatchingSkills matchingSkills = new MatchingSkills();  
  3.     matchingSkills.addEvent(CommonData.PICTURE_GAME_EVENT);  
  4.     matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON);  
  5.     CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);  
  6.     subscriber = new MyCommonEventSubscriber(subscribeInfo);  
  7.     try {  
  8.         CommonEventManager.subscribeCommonEvent(subscriber);  
  9.     } catch (RemoteException e) {  
  10.         LogUtil.error("""subscribeCommonEvent occur exception.");  
  11.     }  

当订阅到相关事件时会回调onReceiveEvent方法,此时即可将imageIndex、moveImageId、movePosition解析出来,并调用updateDataInfo更新对端的布局文件,代码如下所示:

  1. @Override  
  2. public void onReceiveEvent(CommonEventData commonEventData) {  
  3.     ...  
  4.     Intent intent = commonEventData.getIntent();  
  5.     updateDataInfo(intent);  
  6. }  
  7.    
  8. private void updateDataInfo(Intent intent) {  
  9.     imageIndex = intent.getIntArrayParam(CommonData.KEY_IMAGE_INDEX);  
  10.     moveImageId = intent.getIntParam(CommonData.KEY_MOVE_IMAGE_ID, -1);  
  11.     movePosition = intent.getIntParam(CommonData.KEY_MOVE_POSITION, -1);  
  12.     getUITaskDispatcher().delayDispatch(() -> setImageAndDecodeBounds(imageIndex), DELAY_TIME);  

通过选择设备、建立连接、发送数据、接收数据这几个关键步骤,您就可以实现两台设备的同步拼图的功能。以上两个案例,我们完整的学习了两台设备之间的数据交互,体验了HarmonyOS分布式特性,相信您一定有所收获。

—-结束

说明:

以上代码仅demo演示参考使用,产品化的代码需要考虑数据校验和国际化。

8. 回顾和总结

本篇Codelab通过一个亲子早教系统,完整的为您介绍了早教算数题和益智拼图游戏两个综合案例,旨在帮助您快速了解HarmonyOS应用开发、多屏互动、分布式跨设备协同的功能了解。您需要重点掌握跨设备协同、分布式任务调度、公共事件三项HarmonyOS能力。特别的,我们通过拆解步骤的方式详细为您介绍了如何在两台设备之间进行数据传递,这是您需要重点学习和掌握的知识点。

另外,本篇Codelab的两个案例还可以通过分布式数据服务和分布式文件服务去优化代码,我们将在后续Codelab中为您介绍这方面的知识点和案例。

9. 恭喜您

  • 目前您已经成功完成了Codelab并且学到了:
  • 常用布局、自定义控件的使用
  • Page Ability、Service Ability、Intent
  • 多屏互动、分布式跨设备协同、分布式任务调度
  • 公共事件

10. 参考

gitee源码

github源码

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-07-23 08:57:32

鸿蒙HarmonyOS应用

2020-09-29 19:20:05

鸿蒙

2020-11-06 12:12:35

HarmonyOS

2021-05-28 09:52:00

鸿蒙HarmonyOS应用

2021-12-13 11:07:10

鸿蒙HarmonyOS应用

2021-11-16 09:38:10

鸿蒙HarmonyOS应用

2021-10-21 10:03:09

鸿蒙HarmonyOS应用

2022-07-15 16:26:58

Java分布式

2021-12-10 15:06:56

鸿蒙HarmonyOS应用

2021-12-02 10:11:44

鸿蒙HarmonyOS应用

2021-08-16 09:55:41

鸿蒙HarmonyOS应用

2019-10-10 09:16:34

Zookeeper架构分布式

2017-09-01 05:35:58

分布式计算存储

2019-06-19 15:40:06

分布式锁RedisJava

2023-05-29 14:07:00

Zuul网关系统

2021-01-21 09:45:36

鸿蒙HarmonyOS分布式

2021-12-14 14:47:18

鸿蒙HarmonyOS应用

2021-08-24 15:13:06

鸿蒙HarmonyOS应用

2023-10-26 18:10:43

分布式并行技术系统

2017-10-27 08:40:44

分布式存储剪枝系统
点赞
收藏

51CTO技术栈公众号