本篇文章介绍分布式设备间如何共享涂鸦画板的核心功能。
01、实现涂鸦作品发送至已连接手机
在涂鸦画板中有3个核心功能:
(1) 涂鸦者选择好希望连接的设备后,可以直接把涂鸦成果流转给对应的设备。
(2) 其他设备接收流转的涂鸦后,可以在涂鸦的基础上添加涂鸦或者修改。
(3) 修改后的涂鸦可以继续流转给涂鸦者,或者流转到其他设备上,如电视上。
这3个功能都是在连接好附近的设备后才能实现的。鸿蒙操作系统中对于室内网络的通信提供了对软总线的支持,软总线通过屏蔽设备的连接方式,采用一种最优的方式进行多设备的发现、连接和通信,如图1所示。
■ 图1 华为鸿蒙分布式软总线
在JavaScript框架中提供了FeatureAbility.continueAbility 方法,这种方法实现了设备间应用流转的所有功能。在需要流转的时候,只需保持好设备流转前的数据,在设备流转后,在其他设备上即可恢复流转前缓存的数据。
首先在需要流转的页面添加onStartContinuation和onSaveData这两种方法。流转后还需要用到的方法是onRestoreData和onCompleteContinuation,设备间流转触发的生命周期方法如图2所示,Ability流转方法如代码示例1所示。
■ 图2 鸿蒙Ability流转图
代码示例1 Ability流转方法
transforAbility: async function transfer() { try { await FeatureAbility.continueAbility(0,null) } catch (e) { console.error("迁移出错:" + JSON.stringify(e)) } }, onStartContinuation: function onStartContinuation() { //判断当前的状态是不是适合迁移 console.error("trigger onStartContinuation"); return true; }, onCompleteContinuation: function onCompleteContinuation(code) { //迁移操作完成,code返回结果 console.error("trigger onCompleteContinuation: code = " + code); return true }, onSaveData: function onSaveData(saveData) { //数据保存到savedData中进行迁移。 saveData.points = this.sharePoints; return true }, onRestoreData: function onRestoreData(restoreData) { //收到迁移数据,恢复。 this.sharePoints = restoreData.points; return true }
这里需要注意,如果需要实现应用流转,则onStartContinuation、onCompleteContinuation、onSaveData和onRestoreData的返回值都必须为true,否则无法流转和恢复数据。
Ability在流转后,数据需要恢复过来,所以需要在绘制坐标的时候,把所有的坐标保存到一个数组中,sharePoints是用来保存touchMove中的所有坐标信息的数组,如代码示例2所示。
代码示例2 实现流转
export default { data: { cxt: {}, sharePoints: [], lineHeight:3 }, onShow() { this.cxt = this.$element("board").getContext("2d"); //恢复数据后,重新绘制 if (this.sharePoints.length > 0) { for (let index = 0; index < this.sharePoints.length; index++) { this.paintCanvas(this.sharePoints[index]) } } }, //Touch start事件 painStart(e) { this.isShow = true; var p = { x: e.touches[0].localX, y: e.touches[0].localY, c: this.selColor, flag: "start" } this.sharePoints.push(p) //本地绘制 this.paintCanvas(p) }, //Touch move事件 paint(e) { //坐标 var p = { x: e.touches[0].localX, y: e.touches[0].localY, c: this.selColor, flag: "line" } this.sharePoints.push(p) this.paintCanvas(p) }, //Touch事件结束 paintEnd(e) { this.cxt.closePath(); }, //本地绘制自由线条 paintCanvas(point) { if (point.flag == "start") { this.cxt.beginPath(); this.cxt.strokeStyle = point.c this.cxt.lineWidth = this.lineHeight this.cxt.lineCap = "round" this.cxt.lineJoin = "round" this.cxt.moveTo(point.x,point.y); } else if (point.flag == "line") { this.cxt.lineTo(point.x,point.y); this.cxt.stroke() } }, //启动流转 setUpRemote: async function transfer() { try { await FeatureAbility.continueAbility(0,null) } catch (e) { console.error("迁移出错:" + JSON.stringify(e)) } }, onStartContinuation: function onStartContinuation() { //判断当前的状态是不是适合迁移 return true; }, onCompleteContinuation: function onCompleteContinuation(code) { //迁移操作完成,code返回结果 return true }, onSaveData: function onSaveData(saveData) { //数据保存到savedData中进行迁移。 saveData.points = this.sharePoints; return true }, onRestoreData: function onRestoreData(restoreData) { //收到迁移数据,恢复。 this.sharePoints = restoreData.points; return true } }
2、实现画板实时共享功能
多设备间实现涂鸦画板数据同步,在鸿蒙操作系统中,需要通过分布式数据库来完成。同时需要Service Ability把分布式数据库中的数据推送给前端的FA(JavaScript页面)。
下面介绍如何为涂鸦画板添加实时共享功能,步骤如下。
步骤1: 通过分布式数据库实现数据共享。
定义一个BoardServiceAbility 的PA,如代码示例3所示,重写onStart方法,在onStart方法中引用和创建分布式数据服务,storeID为数据库名称。
代码示例3 创建分布式数据服务
public class BoardServiceAbility extends Ability { private KvManagerConfig config; private KvManager kvManager; private String storeID = "board"; private SingleKvStore singleKvStore; @Override protected void onStart(Intent intent) { super.onStart(intent); //初始化manager config = new KvManagerConfig(this); kvManager = KvManagerFactory.getInstance().createKvManager(config); //创建数据库 Options options = new Options(); options.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION); singleKvStore = kvManager.getKvStore(options, storeID); } }
步骤2: Service Ability实现页面数据订阅与同步。
把需要共享的数据保存到鸿蒙分布式数据库中,如果需要在页面中访问这些实时的数据,则需要通过创建Service Ability来推送分布式数据库中的数据。创建推送数据的逻辑如代码示例4所示。
代码示例4 推送数据
package com.cangjie.jsabilitydemo.services; import com.cangjie.jsabilitydemo.utils.DeviceUtils; import ohos.aafwk.ability.Ability; import ohos.aafwk.content.Intent; import ohos.data.distributed.common.*; import ohos.data.distributed.user.SingleKvStore; import ohos.rpc.*; import ohos.hiviewdfx.HiLog; import ohos.hiviewdfx.HiLogLabel; import ohos.utils.zson.ZSONObject; import java.util.*; public class BoardServiceAbility extends Ability { private KvManagerConfig config; private KvManager kvManager; private String storeID = "board"; private SingleKvStore singleKvStore; @Override protected void onStart(Intent intent) { super.onStart(intent); //初始化manager config = new KvManagerConfig(this); kvManager = KvManagerFactory.getInstance().createKvManager(config); //创建数据库 Options options = new Options(); options.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION); singleKvStore = kvManager.getKvStore(options, storeID); } private static final String TAG = "BoardServiceAbility"; private MyRemote remote = new MyRemote(); @Override protected IRemoteObject onConnect(Intent intent) { super.onConnect(intent); return remote.asObject(); } }
步骤3:在多个分布式设备中进行数据同步,创建一个RPC代理类MyRemote,代理类用于远程通信连接。MyRemote类可以是BoardServiceAbility这个PA 的内部类。
实现远程数据调用,这里最关键的是onRemoteRequest方法,这种方法的code参数是客户端发送过来的编号,通过这个编号便可以处理不同的客户端请求,如代码示例5所示。
代码示例5 远程代理
class MyRemote extends RemoteObject implements IRemoteBroker { private static final int ERROR = -1; private static final int SUCCESS = 0; private static final int SUBSCRIBE = 3000; private static final int UNSUBSCRIBE = 3001; private static final int SAVEPOINTS= 2000; //保存绘制的坐标 private Set<IRemoteObject> remoteObjectHandlers = new HashSet<IRemoteObject>(); MyRemote() { super("MyService_MyRemote"); } @Override public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) throws RemoteException { switch (code) { case SAVEPOINTS:{ String zsonStr=data.readString(); BoardRequestParam param= ZSONObject.stringToClass(zsonStr,BoardRequestParam.class); Map<String, Object> zsonResult = new HashMap<String, Object>(); zsonResult.put("data", param.getPoint()); try { singleKvStore.putString("point", param.getPoint()); } catch (KvStoreException e) { e.printStackTrace(); } zsonResult.put("code", SUCCESS); reply.writeString(ZSONObject.toZSONString(zsonResult)); break; } //开启订阅,保存对端的remoteHandler,用于上报数据 case SUBSCRIBE: { remoteObjectHandlers.add(data.readRemoteObject()); startNotify(); Map<String, Object> zsonResult = new HashMap<String, Object>(); zsonResult.put("code", SUCCESS); reply.writeString(ZSONObject.toZSONString(zsonResult)); break; } //取消订阅,置空对端的remoteHandler case UNSUBSCRIBE: { remoteObjectHandlers.remove(data.readRemoteObject()); Map<String, Object> zsonResult = new HashMap<String, Object>(); zsonResult.put("code", SUCCESS); reply.writeString(ZSONObject.toZSONString(zsonResult)); break; } default: { reply.writeString("service not defined"); return false; } } return true; } public void startNotify() { new Thread(() -> { while (true) { try { Thread.sleep(30); //每30ms发送一次 BoardReportEvent(); } catch (RemoteException | InterruptedException e) { break; } } }).start(); } private void BoardReportEvent() throws RemoteException { String points = ""; try { points = singleKvStore.getString("point"); singleKvStore.delete("point"); } catch (KvStoreException e) { e.printStackTrace(); } MessageParcel data = MessageParcel.obtain(); MessageParcel reply = MessageParcel.obtain(); MessageOption option = new MessageOption(); Map<String, Object> zsonEvent = new HashMap<String, Object>(); zsonEvent.put("point", points); data.writeString(ZSONObject.toZSONString(zsonEvent)); for (IRemoteObject item : remoteObjectHandlers) { item.sendRequest(100, data, reply, option); } reply.reclaim(); data.reclaim(); } @Override public IRemoteObject asObject() { return this; } }
这里需要向页面订阅的方法提供推送的数据,需要定义startNotify方法启动一个线程来推送数据,推送的数据是在BoardReportEvent中定义的,推送的数据是从分布式数据库中获取的,通过singleKvStore.getString("point")方法获取共享的数据。
这里还需要创建BoardRequestParam 类,用于序列化页面传过来的字符串,并将字符串转换成对象。Point是坐标信息,如 {x:1,y:2,flag:"start"},是从前端的FA 中传递过来的数据,如代码示例6所示。
代码示例6 序列化页面传过来的字符串BoardRequestParam.java
package com.cangjie.jsabilitydemo.services; public class BoardRequestParam { private int code; //code private String point; //坐标 public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getPoint() { return point; } public void setPoint(String point) { this.point = point; } }
这样就完成了画板的Service Ability的创建,接下来需要改造前面的JavaScript页面代码。
(1) 首先需要修改页面canvas的touchStart、touchMove和touchEnd事件中的绘制方法。要实现实时同步共享画板,就不能直接在touch事件中绘制线了。
需要在touchStart中将起点的坐标发送到上面定义的Service Ability中,PA 接收到请求后,把这个坐标点放到分布式数据服务中。同时页面可以订阅PA,以便页面获取推送的坐标信息。
touchMove将终点的坐标发送到上面定义的Service Ability中,PA 接收到请求后,把这个坐标点放到分布式数据服务中。同时页面可以订阅PA,以便页面获取推送的坐标信息,如代码示例7所示。
代码示例7 向PA 发送需要同步的Point
//起点 var p = { x: e.touches[0].localX, y: e.touches[0].localY, c: this.selColor, flag: "start" } //终点 var p = { x: e.touches[0].localX, y: e.touches[0].localY, c: this.selColor, flag: "line" } //向PA发送需要同步的Point sendPoint(p) { gameAbility.callAbility({ code: 2000, point: p }).then(result=>{ }) }
(2) 实现多设备共享绘制效果,如代码示例8所示,需要通过鸿蒙操作系统的软总线实现,在页面中,需要在onInit中订阅Service Ability中的回调事件以便获取共享的坐标信息。
代码示例8
onInit() { //订阅PA事件 this.subscribeRemoteService(); }, //订阅PA subscribeRemoteService() { gameAbility.subAbility(3000,(data)=>{ console.error(JSON.stringify(data)); if (data.data.point != "") { var p = JSON.parse(data.data.point) this.paintCanvas(p) prompt.showToast({ message: "POINTS:" + JSON.stringify(p) }) } }).then(result=>{ //订阅状态返回 if (result.code != 0) return console.warn("订阅失败"); console.log("订阅成功"); }); },
这个订阅方法,只需在页面启动后调用一次就可以了,所以放在onInit方法中进行调用。
这里通过subscribeRemoteService方法来订阅从Service Ability中推送过来的数据,gameAbility.subAbility方法就是订阅Service Ability推送的方法,3000 是调用ServiceAbility 接收的编号。鸿蒙多设备共享涂鸦画板的效果如图9所示。
■ 图9 鸿蒙多设备共享涂鸦画板同步