1# 分布式画布流转场景 2 3## 场景说明 4 5两台设备组网,当其中一个设备修改文件时,两个设备可以同步修改的结果。分布式场景可以在协同办公(如多人多设备编辑同一文件),设备文档更新(分布式设备更新文件内容,所有设备同步更新)中发挥重要作用,有助于加快工作效率,减少工作中的冗余。 6 7本示例将为大家介绍如何实现上述功能。 8 9## 效果呈现 10 11本例效果如下: 12 13| 设置分布式权限 | 进行分布式连接 | 连接后状态显示 | 14| -------------------------------------- | ------------------------------------------ | ----------------------------------------- | 15|  |  |  | 16 17| 点击rect和ellipse按钮后后本机显示 | 另外一台机器分布式应用显示 | 18| ---------------------------------------- | ---------------------------------------- | 19|  |  | 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 47 在model.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