HarmonyOS 分布式親子教育
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
1. 項(xiàng)目介紹
遠(yuǎn)程教育,多屏協(xié)同是智慧教育的一個(gè)重要場(chǎng)景。本篇Codelab通過一個(gè)親子早教系統(tǒng),完成了分布式早教算數(shù)題和分布式拼圖游戲兩個(gè)綜合案例,旨在幫助開發(fā)者快速了解HarmonyOS應(yīng)用開發(fā)、多屏互動(dòng)和分布式跨設(shè)備協(xié)同的體驗(yàn)。
本篇Codelab將為您重點(diǎn)介紹Page Ability、Service Ability、Intent以及分布式任務(wù)調(diào)度、公共事件等。同時(shí)我們還將為您介紹多屏互動(dòng),分布式跨設(shè)備協(xié)同、繪圖畫布的使用、經(jīng)典拼圖算法。正式介紹之前,我們先對(duì)親子早教系統(tǒng)進(jìn)行展示,讓您快速了解到本篇Codelab所實(shí)現(xiàn)的功能。
功能1:早教算數(shù)題
點(diǎn)擊早教算數(shù)題,系統(tǒng)會(huì)為您隨機(jī)出一道兩位數(shù)的加法,點(diǎn)擊實(shí)時(shí)輔導(dǎo)會(huì)拉起兩個(gè)畫布,本地端可以用黑色筆跡進(jìn)行草稿運(yùn)算,遠(yuǎn)程端可以用紅色筆跡進(jìn)行實(shí)時(shí)指導(dǎo),操作步驟兩端實(shí)時(shí)同步,效果圖如下所示。

功能2:益智拼圖游戲
點(diǎn)擊益智拼圖游戲會(huì)拉起一個(gè)九宮格的拼圖游戲(圖片會(huì)隨機(jī)亂序),點(diǎn)擊圖片可以進(jìn)行拼圖。在本地端點(diǎn)擊親子協(xié)同,遠(yuǎn)程端會(huì)拉起一個(gè)一模一樣的游戲頁(yè)面,兩個(gè)設(shè)備可以進(jìn)行實(shí)時(shí)互動(dòng),同步對(duì)圖片進(jìn)行拼接,操作步驟亦可實(shí)時(shí)同步,效果圖如下所示。

想知道上述兩個(gè)親子游戲是如何實(shí)現(xiàn)的嗎?快來跟隨我們的Codelab進(jìn)行學(xué)習(xí)吧。
2. 搭建HarmonyOS環(huán)境
搭建HarmonyOS環(huán)境
- 安裝DevEco Studio,詳情請(qǐng)參考DevEco Studio下載。
- 設(shè)置DevEco Studio開發(fā)環(huán)境,DevEco Studio開發(fā)環(huán)境需要依賴于網(wǎng)絡(luò)環(huán)境,需要連接上網(wǎng)絡(luò)才能確保工具的正常使用,可以根據(jù)如下兩種情況來配置開發(fā)環(huán)境
- 如果可以直接訪問Internet,只需進(jìn)行下載HarmonyOS SDK操作
- 如果網(wǎng)絡(luò)不能直接訪問Internet,需要通過代理服務(wù)器才可以訪問,請(qǐng)參考配置開發(fā)環(huán)境
說明:
如需要在手機(jī)中運(yùn)行程序,則需要提前申請(qǐng)證書,如使用模擬器可忽略
準(zhǔn)備密鑰和證書請(qǐng)求文件
申請(qǐng)調(diào)試證書
你可以通過如下設(shè)備完成本CodeLab:
開啟開發(fā)者模式的HarmonyOS真機(jī)
DevEco Studio中的手機(jī)模擬器(模擬器暫不支持分布式調(diào)試)
3. 代碼結(jié)構(gòu)解讀
本篇Codelab我們只是對(duì)核心代碼進(jìn)行講解,您可以在最后的參考中下載完整代碼,首先來介紹下整個(gè)工程的代碼結(jié)構(gòu):

- devices:封裝了選擇設(shè)備的Dialog,您可直接調(diào)用SelectDeviceDialog里面的相關(guān)方法。
- point:封裝了繪圖的相關(guān)功能,您可直接調(diào)用DrawPoint里面的相關(guān)方法。
- slice:MainAbilitySlice為應(yīng)用主頁(yè)面,MathGameAbilitySlice為早教算數(shù)題主頁(yè)面,MathDrawRemSlice為早教算數(shù)題繪圖頁(yè)面,PictureGameAbilitySlice為拼圖游戲主頁(yè)面。
- utils:封裝了公共方法和公共數(shù)據(jù)。
- MathGameServiceAbility、PictureGameServiceAbility:供遠(yuǎn)端連接的Service Ability。
- resources:存放工程使用到的資源文件,其中resources\base\layout下存放xml布局文件;resources\base\media下存放圖片資源。
- config.json:配置文件。
4. 親子早教系統(tǒng)頁(yè)面流轉(zhuǎn)
親子早教系統(tǒng)主頁(yè)面和各個(gè)游戲頁(yè)面的布局如下圖所示,首先給大家介紹一下相關(guān)的布局代碼。

首頁(yè)的布局文件為ability_main.xml、頁(yè)面控制邏輯MainAbilitySlice;早教系統(tǒng)的布局文件為math_game.xml、頁(yè)面控制邏輯MathGameAbilitySlice;拼圖游戲的布局文件為ability_picture.xml、頁(yè)面控制邏輯PictureGameAbilitySlice。要實(shí)現(xiàn)頁(yè)面的相互流轉(zhuǎn)首先需要在MainAbility中設(shè)置路由,并在MainAbilitySlice中實(shí)現(xiàn)按鈕的點(diǎn)擊事件。
以跳轉(zhuǎn)早教算數(shù)題為例,首先需要在MainAbility中設(shè)置路由,代碼如下所示:
- addActionRoute(CommonData.MATH_PAGE, MathGameAbilitySlice.class.getName());
而后,需要在MainAbilitySlice中添加點(diǎn)擊事件,代碼如下所示:
- private void mathGame() {
- LogUtil.info(TAG, "Click ResourceTable Id_math_game");
- Intent mathGameIntent = new Intent();
- Operation operationMath = new Intent.OperationBuilder()
- .withBundleName(getBundleName())
- .withAbilityName(CommonData.ABILITY_MAIN)
- .withAction(CommonData.MATH_PAGE)
- .build();
- mathGameIntent.setOperation(operationMath);
- startAbility(mathGameIntent);
- }
至此,您已實(shí)現(xiàn)跳轉(zhuǎn)到早教算數(shù)題和益智拼圖游戲兩個(gè)頁(yè)面了:
- 早教算數(shù)題的出題邏輯可以參考setQuestion和checkAnswer兩個(gè)方法
- 拼圖游戲圖片亂序、移動(dòng)圖片、判斷游戲結(jié)束、重新開始可分別參考pictureRandom、moveFun、gameOverFun、restartFun相關(guān)方法
因篇幅有限且相關(guān)游戲算法不屬于本篇Codelab想為您重點(diǎn)介紹的HarmonyOS特性,因此不再贅述,您可自行調(diào)用相關(guān)函數(shù)、理解相關(guān)代碼實(shí)現(xiàn)游戲功能,附件代碼中也已進(jìn)行了詳細(xì)的注釋。以上即完成了單機(jī)版本的HarmonyOS應(yīng)用開發(fā),接下來我們將為您重點(diǎn)介紹基于HarmonyOS分布式能力的相關(guān)系統(tǒng)設(shè)計(jì)。
5. 發(fā)現(xiàn)設(shè)備和建立連接
在早教算數(shù)題中點(diǎn)擊實(shí)時(shí)輔導(dǎo)或者在拼圖游戲中點(diǎn)擊親子協(xié)同,會(huì)彈出選擇設(shè)備頁(yè)面,如下圖所示。我們已經(jīng)在devices目錄下為您封裝好了選擇設(shè)備的dialog,具體代碼請(qǐng)參考DevicesListAdapter(設(shè)備列表適配器)、item_device_list.xml (設(shè)備列表item布局)、SelectDeviceDialog(選擇設(shè)備列表的彈窗)、dialog_select_device.xml(彈窗布局)。

SelectDeviceDialog中選擇設(shè)備彈窗的構(gòu)造函數(shù)如下,需要傳入三個(gè)參數(shù):上下文、設(shè)備列表、選擇結(jié)果的回調(diào)事件。在業(yè)務(wù)代碼中調(diào)用如下構(gòu)造函數(shù)即可打開選擇設(shè)備的彈窗,代碼如下所示:
- public SelectDeviceDialog(Context context, List<DeviceInfo> devices, SelectResultListener listener) {
- initView(context, devices, listener);
- }
其中List
- private void getDevices() {
- if (devices.size() > 0) {
- devices.clear();
- }
- List<DeviceInfo> deviceInfos =
- DeviceManager.getDeviceList(ohos.distributedschedule.interwork.DeviceInfo.FLAG_GET_ONLINE_DEVICE);
- LogUtil.info(TAG, "deviceInfos size is :" + deviceInfos.size());
- devices.addAll(deviceInfos);
- showDevicesDialog();
- }
選擇結(jié)果的回調(diào)事件可以根據(jù)業(yè)務(wù)的不同傳入不同的回調(diào)事件,例如早教算數(shù)題中選擇設(shè)備后,需要拉起本地端和遠(yuǎn)程端兩個(gè)畫布,此訂閱事件為startLocalFa和startRemoteFa,代碼如下所示:
- private void showDevicesDialog() {
- new SelectDeviceDialog(this, devices, deviceInfo -> {
- startLocalFa(deviceInfo.getDeviceId());
- startRemoteFa(deviceInfo.getDeviceId());
- }).show();
- }
而拼圖游戲需要和另外一臺(tái)設(shè)備建立連接,拉起另外一臺(tái)設(shè)備的拼圖頁(yè)面,此回調(diào)事件為connectRemotePa,代碼如下所示:
- private void showDevicesDialog() {
- new SelectDeviceDialog(this, devices, deviceInfo -> {
- connectRemotePa(deviceInfo.getDeviceId(), PictureRemoteProxy.REQUEST_START_ABILITY);
- }).show();
- }
需要說明的是回調(diào)函數(shù)中deviceInfo是單擊選中的設(shè)備信息,具體實(shí)現(xiàn)在SelectDeviceDialog的initView方法中,代碼如下所示:
- DevicesListAdapter devicesListAdapter = new DevicesListAdapter(devices, context);
- devicesListContainer.setItemProvider(devicesListAdapter);
- devicesListContainer.setItemClickedListener((listContainer, component, position, l) -> {
- listener.callBack(devices.get(position));
- commonDialog.hide();
- });
至此,您已經(jīng)完成了多設(shè)備協(xié)同中發(fā)現(xiàn)設(shè)備和建立連接這一關(guān)鍵步驟。
說明:
實(shí)現(xiàn)遠(yuǎn)程啟動(dòng)FA,需要至少兩個(gè)設(shè)備處于同一個(gè)分布式網(wǎng)絡(luò)中,可以通過如下操作實(shí)現(xiàn):
所有設(shè)備接入同一網(wǎng)絡(luò);
所有設(shè)備登錄相同華為賬號(hào);
所有設(shè)備上開啟"設(shè)置->更多連接->多設(shè)備協(xié)同 "。
6. 早教算數(shù)題
點(diǎn)擊早教算數(shù)題,系統(tǒng)會(huì)為您隨機(jī)出一道兩位數(shù)的加法,點(diǎn)擊實(shí)時(shí)輔導(dǎo)會(huì)拉起兩個(gè)畫布,本地端可以用黑色筆跡進(jìn)行草稿運(yùn)算,遠(yuǎn)程端可以用紅色筆跡進(jìn)行實(shí)時(shí)指導(dǎo),操作步驟兩端實(shí)時(shí)同步,效果如下圖所示。接下來我們將為您詳細(xì)介紹如何利用HarmonyOS分布式技術(shù)實(shí)現(xiàn)兩端的同步繪制。

Step 1 - 選擇設(shè)備
詳見"5 發(fā)現(xiàn)設(shè)備和建立連接"。
Step 2 - 建立連接
點(diǎn)擊選擇設(shè)備后會(huì)進(jìn)入繪圖頁(yè)面,在onStart方法中會(huì)調(diào)用初始化并連接設(shè)備的函數(shù)initAndConnectDevice,通過intent傳參獲取遠(yuǎn)程設(shè)備的remoteDeviceId,并調(diào)用connectRemotePa進(jìn)行連接,代碼如下所示:
- private void initAndConnectDevice(Intent intent) {
- // 頁(yè)面初始化
- this.context = MathDrawRemSlice.this;
- String remoteDeviceId = intent.getStringParam(CommonData.KEY_REMOTE_DEVICEID);
- isLocal = intent.getBooleanParam(CommonData.KEY_IS_LOCAL, false);
- if (findComponentById(ResourceTable.Id_text_title) instanceof Text) {
- Text textTitle = (Text) findComponentById(ResourceTable.Id_text_title);
- textTitle.setText(isLocal ? "本地端" : "遠(yuǎn)程端");
- }
- // 連接遠(yuǎn)程服務(wù)
- if (!remoteDeviceId.isEmpty()) {
- connectRemotePa(remoteDeviceId);
- } else {
- LogUtil.info(TAG, "localDeviceId is null");
- }
- }
其中調(diào)用connectRemotePa方法后會(huì)和MathGameServiceAbility建立服務(wù)連接,代碼如下所示:
- Intent connectPaIntent = new Intent();
- Operation operation = new Intent.OperationBuilder()
- .withDeviceId(deviceId)
- .withBundleName(getBundleName())
- .withAbilityName(CommonData.MATH_GAME_SERVICE_NAME)
- .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
- .build();
- connectPaIntent.setOperation(operation);
連接成功后會(huì)回調(diào)onAbilityConnectDone方法,此時(shí)兩臺(tái)設(shè)備即建立了連接。而后,可以調(diào)用senDataToRemote方法進(jìn)行數(shù)據(jù)發(fā)送,代碼如下所示:
- public void onAbilityConnectDone(ElementName elementName, IRemoteObject remote, int resultCode) {
- LogUtil.info(TAG, "onAbilityConnectDone......");
- connectAbility(elementName, remote, requestType);
- }
- private void connectAbility(ElementName elementName, IRemoteObject remote, int requestType) {
- proxy = new PictureRemoteProxy(remote);
- LogUtil.error(TAG, "connectRemoteAbility done");
- if (proxy != null) {
- try {
- proxy.senDataToRemote(requestType);
- } catch (RemoteException e) {
- LogUtil.error(TAG, "onAbilityConnectDone RemoteException");
- }
- }
- }
Step 3 - 繪圖操作(具體業(yè)務(wù),準(zhǔn)備要發(fā)送的數(shù)據(jù))
DrawPoint是一個(gè)繪圖的工具類,繪制一個(gè)點(diǎn)會(huì)記錄三個(gè)信息:X軸坐標(biāo)、Y軸坐標(biāo)、是否是最后一個(gè)點(diǎn)。將此信息記錄到數(shù)組pointsX、pointsY、isLastPointArray中,并封裝為L(zhǎng)ist
- private float[] pointsX;
- private float[] pointsY;
- private boolean[] isLastPointArray;
- private List<MyPoint> allPoints = new ArrayList<>();
其中,是否為最后一個(gè)點(diǎn)是通過TouchEvent.PRIMARY_POINT_UP事件進(jìn)行區(qū)分的。當(dāng)檢測(cè)到該事件時(shí),會(huì)調(diào)用回調(diào)函數(shù)callBack.callBack(allPoints) ,代碼如下所示:
- if (touchEvent.getAction() == TouchEvent.PRIMARY_POINT_UP) {
- point.setLastPoint(true);
- allPoints.add(point);
- callBack.callBack(allPoints);
- }
此時(shí)會(huì)向上回調(diào)到setOnDrawBack方法,代碼如下所示:
- public void setOnDrawBack(OnDrawCallBack callBack) {
- this.callBack = callBack;
- }
MathDrawRemSlice中initDraw()會(huì)初始化畫布,一次繪制完成后會(huì)回調(diào)setOnDrawBack方法,其中points是上述步驟中繪制的點(diǎn),將其存入數(shù)組pointsX、pointsY、isLastPointArray中,代碼如下所示:
- drawl.setOnDrawBack(points -> {
- if (points != null && points.size() > 1) {
- pointsX = new float[points.size()];
- pointsY = new float[points.size()];
- isLastPoint = new boolean[points.size()];
- for (int i = 0; i < points.size(); i++) {
- pointsX[i] = points.get(i).getPositionX();
- pointsY[i] = points.get(i).getPositionY();
- isLastPoint[i] = points.get(i).isLastPoint();
- }
- ...
- }
- });
此時(shí)就完成了本地畫布繪制并將繪制的點(diǎn)信息存放到了數(shù)組pointsX、pointsY、isLastPointArray中,即已經(jīng)準(zhǔn)備好了要發(fā)送的數(shù)據(jù)。
Step 4 - 發(fā)送數(shù)據(jù)
發(fā)送數(shù)據(jù)涉及到Service Ability、分布式任務(wù)調(diào)度、公共事件三項(xiàng)HarmonyOS能力,如果您還不熟悉相關(guān)基礎(chǔ)知識(shí)可以先參考官方文檔進(jìn)行學(xué)習(xí)。一次繪制完成后,會(huì)調(diào)用senDataToRemote方法,將繪制的點(diǎn)信息(pointsX、pointsY、isLastPointArray)存放到data中并發(fā)送出去,代碼如下所示:
- private void senDataToRemote(int requestType) throws RemoteException {
- LogUtil.info(TAG, "send data to local draw service");
- MessageParcel data = MessageParcel.obtain();
- MessageParcel reply = MessageParcel.obtain();
- MessageOption option = new MessageOption(MessageOption.TF_SYNC);
- try {
- if (pointsX != null && pointsY != null && isLastPoint != null) {
- data.writeFloatArray(pointsX);
- data.writeFloatArray(pointsY);
- data.writeBooleanArray(isLastPoint);
- }
- remote.sendRequest(requestType, data, reply, option);
- int ec = reply.readInt();
- if (ec != ERR_OK) {
- LogUtil.error(TAG, "RemoteException:");
- }
- } catch (RemoteException e) {
- LogUtil.error(TAG, "RemoteException:");
- } finally {
- data.reclaim();
- reply.reclaim();
- }
- }
其中,remote.sendRequest是將數(shù)據(jù)發(fā)送到MathGameServiceAbility的服務(wù)中(因?yàn)椴襟E2是和MathGameServiceAbility建立服務(wù)連接),建立服務(wù)連接后會(huì)回調(diào)onRemoteRequest方法,代碼如下所示:
- @Override
- public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
- LogUtil.info(TAG, "onRemoteRequest......");
- float[] pointsX = data.readFloatArray();
- float[] pointsY = data.readFloatArray();
- boolean[] isLastPointArray = data.readBooleanArray();
- reply.writeInt(ERR_OK);
- sendEvent(isLastPointArray, pointsX, pointsY);
- return true;
- }
在onRemoteRequest方法中,會(huì)調(diào)用sendEvent將數(shù)組pointsX、pointsY、isLastPointArray發(fā)送出去。sendEvent是通過公共事件的方式進(jìn)行數(shù)據(jù)傳遞的,其注冊(cè)的公共事件是CommonData.MATH_DRAW_EVENT,代碼如下所示:
- private void sendEvent(boolean[] isLastPoint, float[] pointsX, float[] pointsY) {
- LogUtil.info(TAG, "sendEvent......");
- try {
- Intent intent = new Intent();
- Operation operation = new Intent.OperationBuilder()
- .withAction(CommonData.MATH_DRAW_EVENT)
- .build();
- intent.setOperation(operation);
- intent.setParam(CommonData.KEY_POINT_X, pointsX);
- intent.setParam(CommonData.KEY_POINT_Y, pointsY);
- intent.setParam(CommonData.KEY_IS_LAST_POINT, isLastPoint);
- CommonEventData eventData = new CommonEventData(intent);
- CommonEventManager.publishCommonEvent(eventData);
- } catch (RemoteException e) {
- LogUtil.error(TAG, "publishCommonEvent occur exception.");
- }
- }
Step 5 - 接收數(shù)據(jù)
MathDrawRemSlice中會(huì)訂閱CommonData.MATH_DRAW_EVENT的公共事件,代碼如下所示:
- private void subscribe() {
- MatchingSkills matchingSkills = new MatchingSkills();
- matchingSkills.addEvent(CommonData.MATH_DRAW_EVENT);
- matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON);
- CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
- subscriber = new MyCommonEventSubscriber(subscribeInfo);
- try {
- CommonEventManager.subscribeCommonEvent(subscriber);
- } catch (RemoteException e) {
- LogUtil.error("", "subscribeCommonEvent occur exception.");
- }
- }
當(dāng)訂閱到相關(guān)事件時(shí)會(huì)回調(diào)onReceiveEvent方法,此時(shí)即可將pointsX、pointsY、isLastPointArray解析出來,然后調(diào)用drawl.setDrawParams(isLastPointArray, pointsX, pointsY)方法在遠(yuǎn)程端進(jìn)行繪制,代碼如下所示:
- public void onReceiveEvent(CommonEventData commonEventData) {
- Intent intent = commonEventData.getIntent();
- pointsX = intent.getFloatArrayParam(CommonData.KEY_POINT_X);
- pointsY = intent.getFloatArrayParam(CommonData.KEY_POINT_Y);
- isLastPoint = intent.getBooleanArrayParam(CommonData.KEY_IS_LAST_POINT);
- // 接收數(shù)據(jù)后,對(duì)遠(yuǎn)程端畫布進(jìn)行繪制
- drawl.setDrawParams(isLastPoint, pointsX, pointsY);
- LogUtil.info(TAG, "onReceiveEvent.....");
- }
Step 6 - 數(shù)據(jù)的雙向通信
步驟1-5中為您講解了數(shù)據(jù)的單向通信,即本地端向遠(yuǎn)程端的交互。本例的繪圖是雙向通信,兩端都可以進(jìn)行繪制,這是怎么做到的呢?原來在步驟1中選擇設(shè)備時(shí),會(huì)調(diào)用startLocalFa和startRemoteFa,其中startLocalFa傳出的remoteDeviceId是所選擇的設(shè)備ID,startRemoteFa傳出的remoteDeviceId是當(dāng)前設(shè)備的ID,由此實(shí)現(xiàn)了雙向通信,代碼如下所示:
- private void showDevicesDialog() {
- new SelectDeviceDialog(this, devices, deviceInfo -> {
- startLocalFa(deviceInfo.getDeviceId());
- startRemoteFa(deviceInfo.getDeviceId());
- }).show();
- }
- private void startLocalFa(String deviceId) {
- LogUtil.info(TAG, "startLocalFa......");
- Intent intent = new Intent();
- intent.setParam(CommonData.KEY_REMOTE_DEVICEID, deviceId);
- ...
- }
- private void startRemoteFa(String deviceId) {
- LogUtil.info(TAG, "startRemoteFa......");
- String localDeviceId = KvManagerFactory.getInstance()
- .createKvManager(new KvManagerConfig(this)).getLocalDeviceInfo().getId();
- Intent intent = new Intent();
- intent.setParam(CommonData.KEY_REMOTE_DEVICEID, localDeviceId);
- ...
- }
通過選擇設(shè)備、建立連接、發(fā)送數(shù)據(jù)、接收數(shù)據(jù)這幾個(gè)關(guān)鍵步驟,您就可以實(shí)現(xiàn)兩臺(tái)設(shè)備的同步繪圖、實(shí)時(shí)顯示的功能。
—-結(jié)束
7. 益智拼圖游戲
點(diǎn)擊益智拼圖游戲會(huì)拉起一個(gè)九宮格的拼圖游戲(圖片會(huì)隨機(jī)亂序),點(diǎn)擊圖片可以進(jìn)行拼圖。在本地端中點(diǎn)擊親子協(xié)同,遠(yuǎn)程端會(huì)拉起一個(gè)一模一樣的游戲頁(yè)面,兩個(gè)設(shè)備可以進(jìn)行實(shí)時(shí)互動(dòng),同步對(duì)圖片進(jìn)行拼接,操作步驟亦可實(shí)時(shí)同步,效果圖如下所示。接下來我們將為您詳細(xì)介紹如何利用HarmonyOS分布式技術(shù)實(shí)現(xiàn)一個(gè)益智拼圖游戲。

Step 1 - 選擇設(shè)備
詳見"5 發(fā)現(xiàn)設(shè)備和建立連接"這一章節(jié)。
Step 2 - 建立連接并拉起設(shè)備
點(diǎn)擊親子協(xié)同后會(huì)記錄選擇設(shè)備的deviceId,調(diào)用connectRemotePa方法后會(huì)和PictureGameServiceAbility建立服務(wù)連接,其中標(biāo)識(shí)位傳遞的是REQUEST_START_ABILITY,代碼如下所示:
- private void showDevicesDialog() {
- new SelectDeviceDialog(this, devices, deviceInfo -> {
- connectRemotePa(deviceInfo.getDeviceId(), PictureRemoteProxy.REQUEST_START_ABILITY);
- }).show();
- }
- private void connectRemotePa(String deviceId, int requestType) {
- if (!deviceId.isEmpty()) {
- Intent connectPaIntent = new Intent();
- Operation operation = new Intent.OperationBuilder()
- .withDeviceId(deviceId)
- .withBundleName(getBundleName())
- .withAbilityName(CommonData.PICTURE_GAME_SERVICE_NAME)
- .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
- .build();
- connectPaIntent.setOperation(operation);
- ...
- }
連接成功后會(huì)回調(diào)onAbilityConnectDone方法,此時(shí)兩臺(tái)設(shè)備即建立了連接。然后調(diào)用sendDataToRemote方法進(jìn)行數(shù)據(jù)發(fā)送,其中requestType為REQUEST_START_ABILITY,代碼如下所示:
- public void onAbilityConnectDone(ElementName elementName, IRemoteObject remote, int resultCode) {
- LogUtil.info(TAG, "onAbilityConnectDone......");
- connectAbility(elementName, remote, requestType);
- }
- private void connectAbility(ElementName elementName, IRemoteObject remote, int requestType) {
- proxy = new PictureRemoteProxy(remote);
- LogUtil.error(TAG, "connectRemoteAbility done");
- if (proxy != null) {
- try {
- proxy.sendDataToRemote(requestType);
- } catch (RemoteException e) {
- LogUtil.error(TAG, "onAbilityConnectDone RemoteException");
- }
- }
- }
PictureGameServiceAbility服務(wù)中接收到了senDataToRemote的消息后會(huì)回調(diào)onRemoteRequest,因?yàn)榇藭r(shí)code為REQUEST_START_ABILITY,所以會(huì)拉起遠(yuǎn)程端的拼圖頁(yè)面,代碼如下所示:
- public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
- ...
- if (code == REQUEST_START_ABILITY) {
- LogUtil.error(TAG, "RemoteServiceAbility::isFirstStart:");
- Intent secondIntent = new Intent();
- Operation operation = new Intent.OperationBuilder().withDeviceId("")
- .withBundleName(getBundleName())
- .withAbilityName(CommonData.ABILITY_MAIN)
- .withAction(CommonData.PICTURE_PAGE)
- .build();
- ...
- } else {
- ...
- }
- ...
- }
以上即完成了本地端到遠(yuǎn)程端的單向通信。遠(yuǎn)程端設(shè)備拉起后,會(huì)在PictureGameAbilitySlice執(zhí)行onStart方法,進(jìn)而執(zhí)行initRemoteView方法,其中會(huì)再次調(diào)用connectRemotePa方法,此時(shí)傳遞的標(biāo)識(shí)位為REQUEST_SEND_DATA,不會(huì)重復(fù)拉起本地端設(shè)備的頁(yè)面,只會(huì)建立數(shù)據(jù)連接,代碼如下所示:
- private void initRemoteView(Intent intent) {
- if (!isLocal) {
- remoteDeviceId = intent.getStringParam(CommonData.KEY_REMOTE_DEVICEID);
- connectRemotePa(remoteDeviceId, PictureRemoteProxy.REQUEST_SEND_DATA);
- if (imageIndex != null) {
- updateDataInfo(intent);
- }
- }
- }
如上步驟即完成了本地端和遠(yuǎn)程端的雙向通信,實(shí)現(xiàn)了數(shù)據(jù)互傳的功能。
Step 3 - 拼圖操作(具體業(yè)務(wù),準(zhǔn)備要發(fā)送的數(shù)據(jù))
因篇幅有限且拼圖游戲不屬于本篇Codelab想為您重點(diǎn)介紹的HarmonyOS特性,因此不再贅述,請(qǐng)讀者自行理解。我們告訴您的結(jié)論是,完成一次拼圖操作需要記錄三個(gè)關(guān)鍵數(shù)據(jù):移動(dòng)圖片的下標(biāo)moveImageId,移動(dòng)圖片的位置movePosition和最終排列的圖片下標(biāo)imageIndex。點(diǎn)擊一次拼圖,我們就會(huì)記錄以上三個(gè)數(shù)據(jù),代碼如下所示:
- private class ImageClick implements Component.ClickedListener {
- @Override
- public void onClick(Component component) {
- int imageId = component.getId();
- for (int position = 0; position < imageIndex.length; position++) {
- if (imageId == imageResourceTable[position]) {
- // 完成圖片移動(dòng),并記錄移動(dòng)信息
- moveFun(imageId, position);
- moveImageId = imageId;
- movePosition = position;
- }
- }
- // 刷新頁(yè)面顯示
- setImageAndDecodeBounds(imageIndex);
- // 發(fā)送數(shù)據(jù)
- senDataToRemoteFun();
- }
- }
Step 4 - 發(fā)送數(shù)據(jù)
發(fā)送數(shù)據(jù)的流程和早教算數(shù)題一樣,我們?cè)俅螢槟鲈敿?xì)介紹,您可參考早教算數(shù)題進(jìn)行對(duì)照學(xué)習(xí)。一次拼圖完成后,會(huì)調(diào)用senDataToRemote方法,將關(guān)鍵信息(imageIndex、moveImageId、movePosition)存放到data中并發(fā)送出去,代碼如下所示:
- private void senDataToRemote(int requestType) throws RemoteException {
- MessageParcel data = MessageParcel.obtain();
- ...
- try {
- ...
- data.writeIntArray(imageIndex);
- data.writeInt(moveImageId);
- data.writeInt(movePosition);
- remote.sendRequest(requestType, data, reply, option);
- ...
- } catch (RemoteException e) {
- ...
- } finally {
- ...
- }
- }
其中,remote.sendRequest是將數(shù)據(jù)發(fā)送到PictureGameServiceAbility的服務(wù)中(因?yàn)椴襟E2是和PictureGameServiceAbility建立服務(wù)連接),建立服務(wù)連接后會(huì)回調(diào)onRemoteRequest方法。接收順序應(yīng)該和發(fā)送順序一致(imageIndex、moveImageId、movePosition),否則會(huì)操作接收錯(cuò)誤的情況,代碼如下所示:
- public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
- LogUtil.info(TAG, "onRemoteRequest......");
- int[] imageIndex = data.readIntArray();
- int moveImageId = data.readInt();
- int movePosition = data.readInt();
- ...
- LogUtil.info(TAG, "receive number:" + imageIndex.length);
- reply.writeInt(ERR_OK);
- if (code == REQUEST_START_ABILITY) {
- ...
- } else {
- sendEvent(imageIndex, moveImageId, movePosition);
- }
- return true;
- }
在onRemoteRequest方法中,會(huì)調(diào)用sendEvent將imageIndex、moveImageId、movePosition發(fā)送出去。sendEvent是通過注冊(cè)公共事件的方式進(jìn)行數(shù)據(jù)傳遞的,其注冊(cè)的公共事件是CommonData. PICTURE_GAME_EVENT,代碼如下所示:
- private void sendEvent(int[] imageIndex, int moveImageId, int movePosition) {
- try {
- Intent intent = new Intent();
- Operation operation = new Intent.OperationBuilder()
- .withAction(CommonData.PICTURE_GAME_EVENT)
- .build();
- intent.setOperation(operation);
- intent.setParam(CommonData.KEY_IMAGE_INDEX, imageIndex);
- intent.setParam(CommonData.KEY_MOVE_IMAGE_ID, moveImageId);
- intent.setParam(CommonData.KEY_MOVE_POSITION, movePosition);
- CommonEventData eventData = new CommonEventData(intent);
- CommonEventManager.publishCommonEvent(eventData);
- } catch (RemoteException e) {
- LogUtil.error(TAG, "publishCommonEvent occur exception.");
- }
- }
Step 5 - 接收數(shù)據(jù)
接收數(shù)據(jù)的代碼流程和早教算數(shù)題一樣,我們?cè)俅螢槟鲈敿?xì)介紹,您可參考早教算數(shù)題進(jìn)行對(duì)照學(xué)習(xí)。PictureGameAbilitySlice會(huì)訂閱CommonData. PICTURE_GAME_EVENT的公共事件,代碼如下所示:
- private void subscribe() {
- MatchingSkills matchingSkills = new MatchingSkills();
- matchingSkills.addEvent(CommonData.PICTURE_GAME_EVENT);
- matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON);
- CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
- subscriber = new MyCommonEventSubscriber(subscribeInfo);
- try {
- CommonEventManager.subscribeCommonEvent(subscriber);
- } catch (RemoteException e) {
- LogUtil.error("", "subscribeCommonEvent occur exception.");
- }
- }
當(dāng)訂閱到相關(guān)事件時(shí)會(huì)回調(diào)onReceiveEvent方法,此時(shí)即可將imageIndex、moveImageId、movePosition解析出來,并調(diào)用updateDataInfo更新對(duì)端的布局文件,代碼如下所示:
- @Override
- public void onReceiveEvent(CommonEventData commonEventData) {
- ...
- Intent intent = commonEventData.getIntent();
- updateDataInfo(intent);
- }
- private void updateDataInfo(Intent intent) {
- imageIndex = intent.getIntArrayParam(CommonData.KEY_IMAGE_INDEX);
- moveImageId = intent.getIntParam(CommonData.KEY_MOVE_IMAGE_ID, -1);
- movePosition = intent.getIntParam(CommonData.KEY_MOVE_POSITION, -1);
- getUITaskDispatcher().delayDispatch(() -> setImageAndDecodeBounds(imageIndex), DELAY_TIME);
- }
通過選擇設(shè)備、建立連接、發(fā)送數(shù)據(jù)、接收數(shù)據(jù)這幾個(gè)關(guān)鍵步驟,您就可以實(shí)現(xiàn)兩臺(tái)設(shè)備的同步拼圖的功能。以上兩個(gè)案例,我們完整的學(xué)習(xí)了兩臺(tái)設(shè)備之間的數(shù)據(jù)交互,體驗(yàn)了HarmonyOS分布式特性,相信您一定有所收獲。
—-結(jié)束
說明:
以上代碼僅demo演示參考使用,產(chǎn)品化的代碼需要考慮數(shù)據(jù)校驗(yàn)和國(guó)際化。
8. 回顧和總結(jié)
本篇Codelab通過一個(gè)親子早教系統(tǒng),完整的為您介紹了早教算數(shù)題和益智拼圖游戲兩個(gè)綜合案例,旨在幫助您快速了解HarmonyOS應(yīng)用開發(fā)、多屏互動(dòng)、分布式跨設(shè)備協(xié)同的功能了解。您需要重點(diǎn)掌握跨設(shè)備協(xié)同、分布式任務(wù)調(diào)度、公共事件三項(xiàng)HarmonyOS能力。特別的,我們通過拆解步驟的方式詳細(xì)為您介紹了如何在兩臺(tái)設(shè)備之間進(jìn)行數(shù)據(jù)傳遞,這是您需要重點(diǎn)學(xué)習(xí)和掌握的知識(shí)點(diǎn)。
另外,本篇Codelab的兩個(gè)案例還可以通過分布式數(shù)據(jù)服務(wù)和分布式文件服務(wù)去優(yōu)化代碼,我們將在后續(xù)Codelab中為您介紹這方面的知識(shí)點(diǎn)和案例。
9. 恭喜您
- 目前您已經(jīng)成功完成了Codelab并且學(xué)到了:
- 常用布局、自定義控件的使用
- Page Ability、Service Ability、Intent
- 多屏互動(dòng)、分布式跨設(shè)備協(xié)同、分布式任務(wù)調(diào)度
- 公共事件
10. 參考
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)