1# 分布式画布流转场景
2
3## 场景说明
4
5两台设备组网,当其中一个设备修改文件时,两个设备可以同步修改的结果。分布式场景可以在协同办公(如多人多设备编辑同一文件),设备文档更新(分布式设备更新文件内容,所有设备同步更新)中发挥重要作用,有助于加快工作效率,减少工作中的冗余。
6
7本示例将为大家介绍如何实现上述功能。
8
9## 效果呈现
10
11本例效果如下:
12
13| 设置分布式权限                         | 进行分布式连接                             | 连接后状态显示                            |
14| -------------------------------------- | ------------------------------------------ | ----------------------------------------- |
15| ![](figures/disributed_permission.png) | ![](figures/disributed_canvas_connect.png) | ![](figures/disributed_canvas-before.png) |
16
17| 点击rect和ellipse按钮后后本机显示        | 另外一台机器分布式应用显示               |
18| ---------------------------------------- | ---------------------------------------- |
19| ![](figures/disributed_canvas-after.png) | ![](figures/disributed_canvas-after.png) |
20
21## 运行环境
22
23本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发。
24
25- IDE:DevEco Studio 4.0.0.201 Beta1
26- SDK:Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1)
27
28## 实现思路
29
30在分布式文件场景中,分布式设备管理包含了分布式设备搜索、分布式设备列表弹窗、远端设备拉起三部分。
31首先在分布式组网内搜索设备,然后把设备展示到分布式设备列表弹窗中,最后根据用户的选择拉起远端设备。
32
33- 分布式设备搜索:通过SUBSCRIBE_ID搜索分布式组网内的设备。
34
35- 分布式设备列表弹窗:使用@CustomDialog装饰器来装饰分布式设备列表弹窗。
36
37- 远端设备拉起:通过startAbility(deviceId)方法拉起远端设备的包。
38
39- 分布式数据管理:(1)管理分布式数据库:创建一个distributedObject分布式数据对象实例,用于管理分布式数据对象。
40
41  ​							   (2)订阅分布式数据变化:通过this.distributedObject.on('status', this.statusCallback)监听分布式数据对象的变更。
42
43## 开发步骤
44
451. 申请所需权限
46
47model.json5中添加以下配置:
48
49   ```json
50   "requestPermissions": [
51         {
52           "name": "ohos.permission.DISTRIBUTED_DATASYNC"//允许不同设备间的数据交换
53         },
54         {
55           "name": "ohos.permission.ACCESS_SERVICE_DM"//允许系统应用获取分布式设备的认证组网能力
56         }
57       ]
58   ```
59
602. 构建UI框架
61
62   indexCanvas页面:
63
64   TitleBar组件呈现标题栏。通过数据懒加载的方式遍历绘制的图形。被划出可视区域外的资源会被回收。
65
66   绘制ellipse图形、rect图形的按钮使用Button组件呈现。
67
68   返回按钮、删除按钮也通过Button组件呈现。
69
70   ```typescript
71   build() {
72       Column() {
73         TitleBar({ rightBtn: $r('app.media.trans'), onRightBtnClicked: this.showDialog })
74   	//自/common/TitleBar.ets中引入标题栏相关。点击标题栏中的右侧按钮会调用showDialog()函数连接组网设备
75         Row() {
76           Text($r('app.string.state'))
77             .fontSize(30)
78           Image(this.isOnline ? $r('app.media.green') : $r('app.media.red'))
79             .size({ width: 30, height: 30 })
80             .objectFit(ImageFit.Contain)
81         }
82         .width('100%')
83         .padding(16)
84   	  //通过懒加载模式遍历绘制的图形,将每个图形绘制在画布上
85         LazyForEach(this.canvasDataSource, (item: CanvasPath, index) => {
86           Canvas(this.context)
87             .width('100%')
88             .height(200)
89             .backgroundColor('#00ffff')
90             .onReady(() => {
91               if (item.path === 'rect') {
92                 this.context.save();
93                 this.path2Df.rect(80, 80, 100, 100);
94                 this.context.stroke(this.path2Df);
95                 this.context.restore();
96               }
97               if (item.path === 'ellipse') {
98                 this.context.restore();
99                 this.path2De.ellipse(100, 100, 50, 100, Math.PI * 0.25, Math.PI * 0.5, Math.PI);
100                 this.context.stroke(this.path2De);
101                 this.context.save();
102               }
103             })
104         }, item => JSON.stringify(item))
105
106         Row() {
107           Button('ellipse')//绘制ellipse图形的按钮
108             .width(130)
109             .height(45)
110             .key('ellipse')
111             .onClick(() => {
112               if (this.globalObject.isContainString('ellipse') === -1) {
113                 this.globalObject.add('ellipse');  //将绘制信息保存在持久全局数据中
114               }
115               this.onPageShow();
116             })
117           Button('rect')//绘制rect图形的按钮
118             .width(130)
119             .height(45)
120             .key('rect')
121             .onClick(() => {
122               if (this.globalObject.isContainString('rect') === -1) {
123                 this.globalObject.add('rect');
124               }
125               this.onPageShow();
126             })
127         }.margin({ top: 10 })
128         .width('100%')
129         .justifyContent(FlexAlign.SpaceAround)
130
131         Row() {
132           Button('back')
133             .width(130)
134             .height(45)
135             .key('back')
136             .backgroundColor(Color.Orange)
137             .onClick(() => {
138               router.back()
139             })
140           Button('delete')//删除图形
141             .width(130)
142             .height(45)
143             .key('delete')
144             .onClick(() => {
145               this.globalObject.clear();
146               this.canvasDataSource['pathArray'] = [];
147               this.canvasDataSource.notifyDataReload();
148               this.context.clearRect(0, 0, 950, 950)
149             })
150         }.margin({ top: 10 })
151         .width('100%')
152         .justifyContent(FlexAlign.SpaceAround)
153       }
154       .width('100%')
155       .height('100%')
156       .justifyContent(FlexAlign.Center)
157       .alignItems(HorizontalAlign.Center)
158     }
159   }
160   ```
161
1623. 数据model
163
164   通过registerDataChangeListener进行对数据变动的监听,数据发生变化时,调用notifyDataReload方法通知数据已经准备就绪。
165
166   ```typescript
167   //BasicDataSource.ets
168   class BasicDataSource implements IDataSource {
169     private listeners: DataChangeListener[] = []
170
171     public totalCount(): number {
172       return 0
173     }
174
175     public getData(index: number): any {
176       return undefined
177     }
178
179     //注册数据变动的监听
180     registerDataChangeListener(listener: DataChangeListener): void {
181       if (this.listeners.indexOf(listener) < 0) {
182         console.info('add listener')
183         this.listeners.push(listener)
184       }
185     }
186
187     unregisterDataChangeListener(listener: DataChangeListener): void {
188       const pos = this.listeners.indexOf(listener);
189       if (pos >= 0) {
190         console.info('remove listener')
191         this.listeners.splice(pos, 1)
192       }
193     }
194
195   //数据reloaded,分布式数据数值变化需要调用这个接口重载下
196     notifyDataReload(): void {
197       this.listeners.forEach(listener => {
198         listener.onDataReloaded()
199       })
200     }
201
202     notifyDataAdd(index: number): void {
203       this.listeners.forEach(listener => {
204         listener.onDataAdd(index)
205       })
206     }
207
208    ....
209
210   export class CanvasDataSource extends BasicDataSource {
211     //监听的数据类型
212     private pathArray: Canvas[] = []
213
214     //重载接口
215     public totalCount(): number {
216       return this.pathArray.length
217     }
218
219     public getData(index: number): any {
220       return this.pathArray[index]
221     }
222
223     public addData(index: number, data: Canvas): void {
224       this.pathArray.splice(index, 0, data)
225       this.notifyDataAdd(index)
226     }
227
228     public pushData(data: Canvas): void {
229       this.pathArray.push(data)
230       this.notifyDataAdd(this.pathArray.length - 1)
231     }
232   }
233   ```
234
2354. 将两台设备组网
236
237   使用自RemoteDeviceModel.ts中引入的类RemoteDeviceModel以扫描获得附近可以连接的设备。
238
239   ```typescript
240   showDialog = () => {
241       //RemoteDeviceModel引入自model/RemoteDeviceModel.ts
242       RemoteDeviceModel.registerDeviceListCallback(() => {
243           //得到附近可信的设备列表
244         Logger.info(TAG, 'registerDeviceListCallback, callback entered')
245         this.devices = []
246         this.devices = RemoteDeviceModel.discoverDevices.length > 0 ? RemoteDeviceModel.discoverDevices : RemoteDeviceModel.devices
247         if (this.dialogController) {
248           this.dialogController.close()
249           this.dialogController = undefined
250         }
251         this.dialogController = new CustomDialogController({
252           builder: DeviceDialog({
253             devices: this.devices,
254             onSelectedIndexChange: this.onSelectedDevice
255           }),
256           autoCancel: true
257         })
258         this.dialogController.open()
259       })
260     }
261   ....................................
262   //model/RemoteDeviceModel.ts
263   import deviceManager from '@ohos.distributedHardware.deviceManager'
264   registerDeviceListCallback(stateChangeCallback: () => void) {
265       if (typeof (this.deviceManager) !== 'undefined') {
266         this.registerDeviceListCallbackImplement(stateChangeCallback)
267         return
268       }
269       Logger.info(TAG, 'deviceManager.createDeviceManager begin')
270       try {
271         deviceManager.createDeviceManager(BUNDLE, (error, value) => {
272           if (error) {
273             Logger.error(TAG, 'createDeviceManager failed.')
274             return
275           }
276           this.deviceManager = value
277           this.registerDeviceListCallbackImplement(stateChangeCallback)
278           Logger.info(TAG, `createDeviceManager callback returned,value=${value}`)
279         })
280       } catch (error) {
281         Logger.error(TAG, `createDeviceManager throw error, code=${error.code} message=${error.message}`)
282       }
283
284       Logger.info(TAG, 'deviceManager.createDeviceManager end')
285     }
286   registerDeviceListCallbackImplement(stateChangeCallback: () => void) {
287       Logger.info(TAG, 'registerDeviceListCallback')
288       this.stateChangeCallback = stateChangeCallback
289       if (this.deviceManager === undefined) {
290         Logger.error(TAG, 'deviceManager has not initialized')
291         this.stateChangeCallback()
292         return
293       }
294       Logger.info(TAG, 'getTrustedDeviceListSync begin')
295       try {
296         let list = this.deviceManager.getTrustedDeviceListSync()//同步获取所有可信设备列表
297         Logger.info(TAG, `getTrustedDeviceListSync end, devices=${JSON.stringify(list)}`)
298         if (typeof (list) !== 'undefined' && typeof (list.length) !== 'undefined') {
299           this.devices = list
300         }
301       } catch (error) {
302         Logger.error(TAG, `getLocalDeviceInfoSync throw error, code=${error.code} message=${error.message}`)
303       }
304       this.stateChangeCallback()
305       Logger.info(TAG, 'callback finished')
306       try {
307         this.deviceManager.on('deviceStateChange', (data) => {
308           if (data === null) {
309             return
310           }
311           Logger.info(TAG, `deviceStateChange data = ${JSON.stringify(data)}`)
312           switch (data.action) {
313             case deviceManager.DeviceStateChangeAction.READY://即设备处于可用状态,表示设备间信息已在分布式数据中同步完成, 可以运行分布式业务
314               this.discoverDevices = []
315               this.devices.push(data.device)
316               this.stateChangeCallback()
317               try {
318                 let list = this.deviceManager.getTrustedDeviceListSync()
319                 if (typeof (list) !== 'undefined' && typeof (list.length) !== 'undefined') {
320                   this.devices = list
321                 }
322               } catch (error) {
323                 Logger.error(TAG, `getTrustedDeviceListSync throw error, code=${error.code} message=${error.message}`)
324               }
325               this.stateChangeCallback()
326               break
327             default:
328               break
329           }
330         })
331         this.deviceManager.on('deviceFound', (data) => {
332           if (data === null) {
333             return
334           }
335           Logger.info(TAG, `deviceFound data=${JSON.stringify(data)}`)
336           this.onDeviceFound(data)
337         })
338         this.deviceManager.on('discoverFail', (data) => {
339           Logger.info(TAG, `discoverFail data=${JSON.stringify(data)}`)
340         })
341         this.deviceManager.on('serviceDie', () => {
342           Logger.info(TAG, 'serviceDie')
343         })
344       } catch (error) {
345         Logger.error(TAG, `on throw error, code=${error.code} message=${error.message}`)
346       }
347       this.startDeviceDiscovery()
348     }
349   startDeviceDiscovery() {
350       SUBSCRIBE_ID = Math.floor(65536 * Math.random())
351       var info = {
352         subscribeId: SUBSCRIBE_ID,
353         mode: 0xAA,
354         medium: 2,
355         freq: 2,//高频率
356         isSameAccount: false,
357         isWakeRemote: true,
358         capability: 0
359       }
360       Logger.info(TAG, `startDeviceDiscovery${SUBSCRIBE_ID}`)
361       try {
362         this.deviceManager.startDeviceDiscovery(info)//开始发现周边设备
363       } catch (error) {
364         Logger.error(TAG, `startDeviceDiscovery throw error, code=${error.code} message=${error.message}`)
365       }
366
367     }
368   ```
369
3705. 实现同步编辑
371
372   通过AppStorage设置持久性数据,然后实现IDataSource接口,通过注册数据监听接口监听数据的变化。
373
374   ```typescript
375   onPageShow() {
376       //每当完成编辑或者新建文件,就会回到主页,此时就会执行onPageShow()
377       //noteDataSource获取globalObject保存的分布式的持久性数据,并进行Reload操作传递。
378       this.noteDataSource['dataArray'] = this.globalObject.distributedObject.documents
379       this.noteDataSource.notifyDataReload()
380       Logger.info(TAG, `this.sessionId = ${this.sessionId}`)
381       Logger.info(TAG, `globalSessionId = ${this.globalSessionId}`)
382       if (this.sessionId !== this.globalSessionId) {
383         this.sessionId = this.globalSessionId
384         this.share()
385       }
386     }
387   share() {
388       //多个设备间的对象如果设置为同一个sessionId,数据自动同步
389       Logger.info(TAG, `sessionId = ${this.sessionId}`)
390       this.globalObject.setChangeCallback(() => {
391         this.noteDataSource['dataArray'] = this.globalObject.distributedObject.documents
392         this.noteDataSource.notifyDataReload()
393       })
394       this.globalObject.setStatusCallback((session, networkId, status) => {
395         Logger.info(TAG, `StatusCallback,${status}`)
396         if (status === 'online') {
397           this.isOnline = true
398         } else {
399           this.isOnline = false
400         }
401       })
402       this.globalObject.distributedObject.setSessionId(this.sessionId)
403       AppStorage.SetOrCreate('objectModel', this.globalObject)
404     }
405   ```
406
407## 全部代码
408
409本例完整代码sample示例链接:[分布式对象](https://gitee.com/openharmony/applications_app_samples/tree/master/code/SuperFeature/DistributedAppDev/DistributedNote)
410
411## 参考
412
413[权限列表](../application-dev/security/AccessToken/permissions-for-all.md#ohospermissiondistributed_datasync)
414
415[Path2D对象](../application-dev/reference/apis-arkui/arkui-ts/ts-components-canvas-path2d.md)
416
417[分布式数据对象](../application-dev/reference/apis-arkdata/js-apis-data-distributedobject.md)
418