• 鸿蒙开发实例 | 分布式涂鸦


    CSDN话题挑战赛第2期
    参赛话题:学习笔记

     本篇文章介绍分布式设备间如何共享涂鸦画板的核心功能。

    01、实现涂鸦作品发送至已连接手机

    在涂鸦画板中有3个核心功能:

        (1) 涂鸦者选择好希望连接的设备后,可以直接把涂鸦成果流转给对应的设备。

        (2) 其他设备接收流转的涂鸦后,可以在涂鸦的基础上添加涂鸦或者修改。

        (3) 修改后的涂鸦可以继续流转给涂鸦者,或者流转到其他设备上,如电视上。

    这3个功能都是在连接好附近的设备后才能实现的。鸿蒙操作系统中对于室内网络的通信提供了对软总线的支持,软总线通过屏蔽设备的连接方式,采用一种最优的方式进行多设备的发现、连接和通信,如图1所示。

    ■ 图1 华为鸿蒙分布式软总线

    JavaScript框架中提供了FeatureAbility.continueAbility 方法,这种方法实现了设备间应用流转的所有功能。在需要流转的时候,只需保持好设备流转前的数据,在设备流转后,在其他设备上即可恢复流转前缓存的数据。

    首先在需要流转的页面添加onStartContinuation和onSaveData这两种方法。流转后还需要用到的方法是onRestoreData和onCompleteContinuation,设备间流转触发的生命周期方法如图2所示,Ability流转方法如代码示例1所示。

    ■ 图2 鸿蒙Ability流转图

    代码示例1 Ability流转方法

    1. transforAbility: async function transfer() {
    2. try {
    3. await FeatureAbility.continueAbility(0,null)
    4. } catch (e) {
    5. console.error("迁移出错:" + JSON.stringify(e))
    6. }
    7. },
    8. onStartContinuation: function onStartContinuation() {
    9. //判断当前的状态是不是适合迁移
    10. console.error("trigger onStartContinuation");
    11. return true;
    12. },
    13. onCompleteContinuation: function onCompleteContinuation(code) {
    14. //迁移操作完成,code返回结果
    15. console.error("trigger onCompleteContinuation: code = " + code);
    16. return true
    17. },
    18. onSaveData: function onSaveData(saveData) {
    19. //数据保存到savedData中进行迁移。
    20. saveData.points = this.sharePoints;
    21. return true
    22. },
    23. onRestoreData: function onRestoreData(restoreData) {
    24. //收到迁移数据,恢复。
    25. this.sharePoints = restoreData.points;
    26. return true
    27. }

    这里需要注意,如果需要实现应用流转,则onStartContinuation、onCompleteContinuation、onSaveData和onRestoreData的返回值都必须为true,否则无法流转和恢复数据。

    Ability在流转后,数据需要恢复过来,所以需要在绘制坐标的时候,把所有的坐标保存到一个数组中,sharePoints是用来保存touchMove中的所有坐标信息的数组,如代码示例2所示。

    代码示例2 实现流转

    1. export default {
    2. data: {
    3. cxt: {},
    4. sharePoints: [],
    5. lineHeight:3
    6. },
    7. onShow() {
    8. this.cxt = this.$element("board").getContext("2d");
    9. //恢复数据后,重新绘制
    10. if (this.sharePoints.length > 0) {
    11. for (let index = 0; index < this.sharePoints.length; index++) {
    12. this.paintCanvas(this.sharePoints[index])
    13. }
    14. }
    15. },
    16. //Touch start事件
    17. painStart(e) {
    18. this.isShow = true;
    19. var p = {
    20. x: e.touches[0].localX,
    21. y: e.touches[0].localY,
    22. c: this.selColor,
    23. flag: "start"
    24. }
    25. this.sharePoints.push(p)
    26. //本地绘制
    27. this.paintCanvas(p)
    28. },
    29. //Touch move事件
    30. paint(e) {
    31. //坐标
    32. var p = {
    33. x: e.touches[0].localX,
    34. y: e.touches[0].localY,
    35. c: this.selColor,
    36. flag: "line"
    37. }
    38. this.sharePoints.push(p)
    39. this.paintCanvas(p)
    40. },
    41. //Touch事件结束
    42. paintEnd(e) {
    43. this.cxt.closePath();
    44. },
    45. //本地绘制自由线条
    46. paintCanvas(point) {
    47. if (point.flag == "start") {
    48. this.cxt.beginPath();
    49. this.cxt.strokeStyle = point.c
    50. this.cxt.lineWidth = this.lineHeight
    51. this.cxt.lineCap = "round"
    52. this.cxt.lineJoin = "round"
    53. this.cxt.moveTo(point.x,point.y);
    54. } else if (point.flag == "line") {
    55. this.cxt.lineTo(point.x,point.y);
    56. this.cxt.stroke()
    57. }
    58. },
    59. //启动流转
    60. setUpRemote: async function transfer() {
    61. try {
    62. await FeatureAbility.continueAbility(0,null)
    63. } catch (e) {
    64. console.error("迁移出错:" + JSON.stringify(e))
    65. }
    66. },
    67. onStartContinuation: function onStartContinuation() {
    68. //判断当前的状态是不是适合迁移
    69. return true;
    70. },
    71. onCompleteContinuation: function onCompleteContinuation(code) {
    72. //迁移操作完成,code返回结果
    73. return true
    74. },
    75. onSaveData: function onSaveData(saveData) {
    76. //数据保存到savedData中进行迁移。
    77. saveData.points = this.sharePoints;
    78. return true
    79. },
    80. onRestoreData: function onRestoreData(restoreData) {
    81. //收到迁移数据,恢复。
    82. this.sharePoints = restoreData.points;
    83. return true
    84. }
    85. }

    2、实现画板实时共享功能

    多设备间实现涂鸦画板数据同步,在鸿蒙操作系统中,需要通过分布式数据库来完成。同时需要Service Ability把分布式数据库中的数据推送给前端的FA(JavaScript页面)。

    下面介绍如何为涂鸦画板添加实时共享功能,步骤如下。

    步骤1: 通过分布式数据库实现数据共享。

    定义一个BoardServiceAbility 的PA,如代码示例3所示,重写onStart方法,在onStart方法中引用和创建分布式数据服务,storeID为数据库名称。

    代码示例3 创建分布式数据服务

    1. public class BoardServiceAbility extends Ability {
    2. private KvManagerConfig config;
    3. private KvManager kvManager;
    4. private String storeID = "board";
    5. private SingleKvStore singleKvStore;
    6. @Override
    7. protected void onStart(Intent intent) {
    8. super.onStart(intent);
    9. //初始化manager
    10. config = new KvManagerConfig(this);
    11. kvManager = KvManagerFactory.getInstance().createKvManager(config);
    12. //创建数据库
    13. Options options = new Options();
    14. options.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION);
    15. singleKvStore = kvManager.getKvStore(options, storeID);
    16. }
    17. }

    步骤2: Service Ability实现页面数据订阅与同步。

    把需要共享的数据保存到鸿蒙分布式数据库中,如果需要在页面中访问这些实时的数据,则需要通过创建Service Ability来推送分布式数据库中的数据。创建推送数据的逻辑如代码示例4所示。

    代码示例4 推送数据

    1. package com.cangjie.jsabilitydemo.services;
    2. import com.cangjie.jsabilitydemo.utils.DeviceUtils;
    3. import ohos.aafwk.ability.Ability;
    4. import ohos.aafwk.content.Intent;
    5. import ohos.data.distributed.common.*;
    6. import ohos.data.distributed.user.SingleKvStore;
    7. import ohos.rpc.*;
    8. import ohos.hiviewdfx.HiLog;
    9. import ohos.hiviewdfx.HiLogLabel;
    10. import ohos.utils.zson.ZSONObject;
    11. import java.util.*;
    12. public class BoardServiceAbility extends Ability {
    13. private KvManagerConfig config;
    14. private KvManager kvManager;
    15. private String storeID = "board";
    16. private SingleKvStore singleKvStore;
    17. @Override
    18. protected void onStart(Intent intent) {
    19. super.onStart(intent);
    20. //初始化manager
    21. config = new KvManagerConfig(this);
    22. kvManager = KvManagerFactory.getInstance().createKvManager(config);
    23. //创建数据库
    24. Options options = new Options();
    25. options.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION);
    26. singleKvStore = kvManager.getKvStore(options, storeID);
    27. }
    28. private static final String TAG = "BoardServiceAbility";
    29. private MyRemote remote = new MyRemote();
    30. @Override
    31. protected IRemoteObject onConnect(Intent intent) {
    32. super.onConnect(intent);
    33. return remote.asObject();
    34. }
    35. }

    步骤3:在多个分布式设备中进行数据同步,创建一个RPC代理类MyRemote,代理类用于远程通信连接。MyRemote类可以是BoardServiceAbility这个PA 的内部类。

    实现远程数据调用,这里最关键的是onRemoteRequest方法,这种方法的code参数是客户端发送过来的编号,通过这个编号便可以处理不同的客户端请求,如代码示例5所示。

    代码示例5 远程代理

    1. class MyRemote extends RemoteObject implements IRemoteBroker {
    2. private static final int ERROR = -1;
    3. private static final int SUCCESS = 0;
    4. private static final int SUBSCRIBE = 3000;
    5. private static final int UNSUBSCRIBE = 3001;
    6. private static final int SAVEPOINTS= 2000; //保存绘制的坐标
    7. private Set<IRemoteObject> remoteObjectHandlers = new HashSet<IRemoteObject>();
    8. MyRemote() {
    9. super("MyService_MyRemote");
    10. }
    11. @Override
    12. public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) throws RemoteException {
    13. switch (code) {
    14. case SAVEPOINTS:{
    15. String zsonStr=data.readString();
    16. BoardRequestParam param= ZSONObject.stringToClass(zsonStr,BoardRequestParam.class);
    17. Map<String, Object> zsonResult = new HashMap<String, Object>();
    18. zsonResult.put("data", param.getPoint());
    19. try {
    20. singleKvStore.putString("point", param.getPoint());
    21. } catch (KvStoreException e) {
    22. e.printStackTrace();
    23. }
    24. zsonResult.put("code", SUCCESS);
    25. reply.writeString(ZSONObject.toZSONString(zsonResult));
    26. break;
    27. }
    28. //开启订阅,保存对端的remoteHandler,用于上报数据
    29. case SUBSCRIBE: {
    30. remoteObjectHandlers.add(data.readRemoteObject());
    31. startNotify();
    32. Map<String, Object> zsonResult = new HashMap<String, Object>();
    33. zsonResult.put("code", SUCCESS);
    34. reply.writeString(ZSONObject.toZSONString(zsonResult));
    35. break;
    36. }
    37. //取消订阅,置空对端的remoteHandler
    38. case UNSUBSCRIBE: {
    39. remoteObjectHandlers.remove(data.readRemoteObject());
    40. Map<String, Object> zsonResult = new HashMap<String, Object>();
    41. zsonResult.put("code", SUCCESS);
    42. reply.writeString(ZSONObject.toZSONString(zsonResult));
    43. break;
    44. }
    45. default: {
    46. reply.writeString("service not defined");
    47. return false;
    48. }
    49. }
    50. return true;
    51. }
    52. public void startNotify() {
    53. new Thread(() -> {
    54. while (true) {
    55. try {
    56. Thread.sleep(30); //30ms发送一次
    57. BoardReportEvent();
    58. } catch (RemoteException | InterruptedException e) {
    59. break;
    60. }
    61. }
    62. }).start();
    63. }
    64. private void BoardReportEvent() throws RemoteException {
    65. String points = "";
    66. try {
    67. points = singleKvStore.getString("point");
    68. singleKvStore.delete("point");
    69. } catch (KvStoreException e) {
    70. e.printStackTrace();
    71. }
    72. MessageParcel data = MessageParcel.obtain();
    73. MessageParcel reply = MessageParcel.obtain();
    74. MessageOption option = new MessageOption();
    75. Map<String, Object> zsonEvent = new HashMap<String, Object>();
    76. zsonEvent.put("point", points);
    77. data.writeString(ZSONObject.toZSONString(zsonEvent));
    78. for (IRemoteObject item : remoteObjectHandlers) {
    79. item.sendRequest(100, data, reply, option);
    80. }
    81. reply.reclaim();
    82. data.reclaim();
    83. }
    84. @Override
    85. public IRemoteObject asObject() {
    86. return this;
    87. }
    88. }

    这里需要向页面订阅的方法提供推送的数据,需要定义startNotify方法启动一个线程来推送数据,推送的数据是在BoardReportEvent中定义的,推送的数据是从分布式数据库中获取的,通过singleKvStore.getString("point")方法获取共享的数据。

    这里还需要创建BoardRequestParam 类,用于序列化页面传过来的字符串,并将字符串转换成对象。Point是坐标信息,如 {x:1,y:2,flag:"start"},是从前端的FA 中传递过来的数据,如代码示例6所示。

    代码示例6 序列化页面传过来的字符串BoardRequestParam.java

    1. package com.cangjie.jsabilitydemo.services;
    2. public class BoardRequestParam {
    3. private int code; //code
    4. private String point; //坐标
    5. public int getCode() {
    6. return code;
    7. }
    8. public void setCode(int code) {
    9. this.code = code;
    10. }
    11. public String getPoint() {
    12. return point;
    13. }
    14. public void setPoint(String point) {
    15. this.point = point;
    16. }
    17. }

    这样就完成了画板的Service Ability的创建,接下来需要改造前面的JavaScript页面代码。

      (1) 首先需要修改页面canvas的touchStart、touchMove和touchEnd事件中的绘制方法。要实现实时同步共享画板,就不能直接在touch事件中绘制线了。

    需要在touchStart中将起点的坐标发送到上面定义的Service Ability中,PA 接收到请求后,把这个坐标点放到分布式数据服务中。同时页面可以订阅PA,以便页面获取推送的坐标信息。

    touchMove将终点的坐标发送到上面定义的Service Ability中,PA 接收到请求后,把这个坐标点放到分布式数据服务中。同时页面可以订阅PA,以便页面获取推送的坐标信息,如代码示例7所示。

    代码示例7 向PA 发送需要同步的Point

    1. //起点
    2. var p = {
    3. x: e.touches[0].localX,
    4. y: e.touches[0].localY,
    5. c: this.selColor,
    6. flag: "start"
    7. }
    8. //终点
    9. var p = {
    10. x: e.touches[0].localX,
    11. y: e.touches[0].localY,
    12. c: this.selColor,
    13. flag: "line"
    14. }
    15. //向PA发送需要同步的Point
    16. sendPoint(p) {
    17. gameAbility.callAbility({
    18. code: 2000,
    19. point: p
    20. }).then(result=>{
    21. })
    22. }

    (2) 实现多设备共享绘制效果,如代码示例8所示,需要通过鸿蒙操作系统的软总线实现,在页面中,需要在onInit中订阅Service Ability中的回调事件以便获取共享的坐标信息。

    代码示例8

    1. onInit() {
    2. //订阅PA事件
    3. this.subscribeRemoteService();
    4. },
    5. //订阅PA
    6. subscribeRemoteService() {
    7. gameAbility.subAbility(3000,(data)=>{
    8. console.error(JSON.stringify(data));
    9. if (data.data.point != "") {
    10. var p = JSON.parse(data.data.point)
    11. this.paintCanvas(p)
    12. prompt.showToast({
    13. message: "POINTS:" + JSON.stringify(p)
    14. })
    15. }
    16. }).then(result=>{
    17. //订阅状态返回
    18. if (result.code != 0)
    19. return console.warn("订阅失败");
    20. console.log("订阅成功");
    21. });
    22. },

    这个订阅方法,只需在页面启动后调用一次就可以了,所以放在onInit方法中进行调用。

    这里通过subscribeRemoteService方法来订阅从Service Ability中推送过来的数据,gameAbility.subAbility方法就是订阅Service Ability推送的方法,3000 是调用ServiceAbility 接收的编号。鸿蒙多设备共享涂鸦画板的效果如图9所示。

     ■ 图9 鸿蒙多设备共享涂鸦画板同步

  • 相关阅读:
    VM下虚拟机连接usb相机后电脑蓝屏重启问题
    文件系统里面没有 /dev/input/mice 文件的解决办法
    数据 | MongoDB Compass 连接远程数据库及 Nest.js 连接 MongoDB
    JS 数组的各个方法汇总
    Win11找不到DNS地址怎么办?Win11找不到DNS无法访问网页解决方法
    HashMap实现原理, 扩容机制,面试题和总结
    Java Reflection操作Classes的简介说明
    el-upload 上传视频时,动态截取视频第一帧画面作为封面展示
    ISP住宅网络的特点是什么
    华为OD机试真题-任务最优调度-2023年OD统一考试(B卷)
  • 原文地址:https://blog.csdn.net/qq_41640218/article/details/127047007