介紹
本篇Codelab使用設備管理及分布式鍵值數(shù)據(jù)庫能力,實現(xiàn)多設備之間手寫板應用拉起及同步書寫內容的功能。操作流程:
- 設備連接同一無線網(wǎng)絡,安裝分布式手寫板應用。進入應用,點擊允許使用多設備協(xié)同,點擊主頁上查詢設備按鈕,顯示附近設備。
- 選擇設備確認,若已建立連接,啟動對方設備上的手寫板應用,否則提示建立連接。輸入PIN碼建立連接后再次點擊查詢設備按鈕,選擇設備提交,啟動對方設備應用。
- 建立連接前繪制的內容在啟動對方設備后同步,此時設備上繪制的內容會在另一端同步繪制。
- 點擊撤銷按鈕,兩側設備繪制內容同步撤銷。
相關概念
- [設備管理]:模塊提供分布式設備管理能力。
- [分布式鍵值數(shù)據(jù)庫]:分布式鍵值數(shù)據(jù)庫為應用程序提供不同設備間數(shù)據(jù)庫的分布式協(xié)同能力。
相關權限
本篇Codelab使用了設備管理及分布式鍵值數(shù)據(jù)庫能力,需要手動替換full-SDK,并在配置文件module.json5文件requestPermissions屬性中添加如下權限:
- [分布式設備認證組網(wǎng)權限]:ohos.permission.ACCESS_SERVICE_DM。
- [設備間的數(shù)據(jù)交換權限]:ohos.permission.DISTRIBUTED_DATASYNC。
約束與限制
- 本篇Codelab部分能力依賴于系統(tǒng)API,需下載full-SDK并替換DevEco Studio自動下載的public-SDK。
- 本篇Codelab使用的部分API僅系統(tǒng)應用可用,需要提升應用等級。
環(huán)境搭建
軟件要求
- [DevEco Studio]版本:DevEco Studio 4.0 Beta2。
- OpenHarmony SDK版本:API version 10。
- 鴻蒙指導參考:[
qr23.cn/AKFP8k
]
硬件要求
- 開發(fā)板類型:[潤和RK3568開發(fā)板]。
- OpenHarmony系統(tǒng):4.0 Release。
環(huán)境搭建
完成本篇Codelab我們首先要完成開發(fā)環(huán)境的搭建,本示例以RK3568開發(fā)板為例,參照以下步驟進行:
- [獲取OpenHarmony系統(tǒng)版本]:標準系統(tǒng)解決方案(二進制)。以4.0 Release版本為例:
- 搭建燒錄環(huán)境。
- [完成DevEco Device Tool的安裝]
- [完成RK3568開發(fā)板的燒錄]
- 搭建開發(fā)環(huán)境。
- 開始前請參考[工具準備],完成DevEco Studio的安裝和開發(fā)環(huán)境配置。
- 開發(fā)環(huán)境配置完成后,請參考[使用工程向導]創(chuàng)建工程(模板選擇“Empty Ability”)。
- 工程創(chuàng)建完成后,選擇使用[真機進行調測]。
代碼結構解讀
本篇Codelab只對核心代碼進行講解,對于完整代碼,我們會在gitee中提供。
├──entry/src/main/ets // 代碼區(qū)
│ ├──common
│ │ ├──constants
│ │ │ └──CommonConstants.ets // 公共常量類
│ │ └──utils
│ │ ├──Logger.ets // 日志打印類
│ │ └──RemoteDeviceUtil.ets // 設備管理類
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口類
│ ├──pages
│ │ └──Index.ets // 主界面
│ ├──view
│ │ └──CustomDialogComponent.ets // 自定義彈窗組件類
│ └──viewmodel
│ ├──KvStoreModel.ets // 分布式鍵值數(shù)據(jù)庫管理類
│ └──Position.ets // 繪制位置信息類
└──entry/src/main/resources // 資源文件目錄
界面設計
主界面由導航欄及繪制區(qū)域組成,導航欄包含撤回按鈕及查詢設備按鈕。繪制區(qū)域使用Canvas畫布組件展示繪制效果。Index.ets文件完成界面實現(xiàn),使用Column及Row容器組件進行布局。
// Index.ets
let storage = LocalStorage.getShared();
@Entry(storage)
@Component
struct Index {
...
build() {
Column() {
Row() {
// 撤回按鈕
Image($r('app.media.ic_back'))
.width($r('app.float.ic_back_width'))
.height($r('app.float.ic_back_height'))
...
Blank()
// 查找設備按鈕
Image($r('app.media.ic_hop'))
.width($r('app.float.ic_hop_width'))
.height($r('app.float.ic_hop_height'))
...
}
.width(CommonConstants.FULL_PERCENT)
.height(CommonConstants.TITLE_HEIGHT)
Row() {
// 繪制區(qū)域
Canvas(this.canvasContext)
.width(CommonConstants.FULL_PERCENT)
.height(CommonConstants.FULL_PERCENT)
...
}
...
.width(CommonConstants.FULL_PERCENT)
.layoutWeight(CommonConstants.NUMBER_ONE)
}
.height(CommonConstants.FULL_PERCENT)
.width(CommonConstants.FULL_PERCENT)
}
...
}
分布式組網(wǎng)
準備分布式環(huán)境
創(chuàng)建設備管理器。設備管理器創(chuàng)建完成后注冊設備上線離線監(jiān)聽,信任設備上線離線時觸發(fā)。執(zhí)行獲取本地設備信息,獲取信任設備列表,初始化展示設備列表等方法。其中deviceManager類需使用full-SDK。
// RemoteDeviceUtil.ets
import deviceManager from '@ohos.distributedHardware.deviceManager';
class RemoteDeviceUtil {
...
async createDeviceManager() {
...
await new Promise((resolve: (value: Object | PromiseLike< Object >) = > void, reject: ((reason?: RejectError) = > void)) = > {
try {
// 創(chuàng)建設備管理器
deviceManager.createDeviceManager(CommonConstants.BUNDLE_NAME,
(error, value: deviceManager.DeviceManager) = > {
...
this.myDeviceManager = value;
// 注冊信任設備上線離線監(jiān)聽
this.registerDeviceStateListener();
// 獲取本地設備信息
this.getLocalDeviceInfo();
// 獲取信任設備列表
this.getTrustedDeviceList();
// 初始化展示設備列表
this.initDeviceList();
resolve(value);
});
} catch (error) {
Logger.error('RemoteDeviceModel',
`createDeviceManager failed, error=${JSON.stringify(error)}`);
}
});
}
...
}
注冊設備狀態(tài)監(jiān)聽。已驗證設備上線或有新設備驗證通過時狀態(tài)類型為ONLINE,將設備添加至信任設備列表。設備離線時狀態(tài)類型為OFFLINE,將設備從信任列表中移除。
// RemoteDeviceUtil.ets
class RemoteDeviceUtil {
...
// 注冊設備狀態(tài)改變監(jiān)聽
registerDeviceStateListener(): void {
...
try {
// 注冊監(jiān)聽
this.myDeviceManager.on('deviceStateChange', (data) = > {
...
switch (data.action) {
// 設備上線
case deviceManager.DeviceStateChangeAction.ONLINE: {
this.deviceStateChangeActionOnline(data.device);
break;
}
// 設備離線
case deviceManager.DeviceStateChangeAction.OFFLINE: {
this.deviceStateChangeActionOffline(data.device);
break;
}
...
}
});
} catch (error) {
Logger.error('RemoteDeviceModel',
`registerDeviceStateListener on('deviceStateChange') failed, error=${JSON.stringify(error)}`);
}
}
// 設備上線,加入信任列表及展示列表
deviceStateChangeActionOnline(device: deviceManager.DeviceInfo): void {
this.trustedDeviceList[this.trustedDeviceList.length] = device;
this.addToDeviceList(device);
}
// 設備下線,將設備移出信任列表和展示列表
deviceStateChangeActionOffline(device: deviceManager.DeviceInfo): void {
let list: deviceManager.DeviceInfo[] = [];
for (let i: number = 0; i < this.trustedDeviceList.length; i++) {
if (this.trustedDeviceList[i].networkId !== device.networkId) {
list.push(this.trustedDeviceList[i]);
continue;
}
}
this.deleteFromDeviceList(device);
this.trustedDeviceList = list;
}
...
}
建立分布式連接
點擊主界面的查詢設備按鈕,執(zhí)行發(fā)現(xiàn)設備方法,注冊設備發(fā)現(xiàn)監(jiān)聽任務,同時拉起彈窗展示設備列表。當彈窗關閉時,執(zhí)行停止發(fā)現(xiàn)設備方法,注銷監(jiān)聽任務。
// RemoteDeviceUtil.ets
class RemoteDeviceUtil {
...
// 處理新發(fā)現(xiàn)的設備
deviceFound(data: DeviceInfoInterface): void {
for (let i: number = 0; i < this.discoverList.length; i++) {
if (this.discoverList[i].deviceId === data.device.deviceId) {
Logger.info('RemoteDeviceModel', `deviceFound device exist=${JSON.stringify(data)}`);
return;
}
}
this.discoverList[this.discoverList.length] = data.device;
this.addToDeviceList(data.device);
}
startDeviceDiscovery(): void {
...
try {
// 注冊發(fā)現(xiàn)設備監(jiān)聽
this.myDeviceManager.on('deviceFound', (data) = > {
...
// 處理發(fā)現(xiàn)的設備
this.deviceFound(data);
});
...
let info: deviceManager.SubscribeInfo = {
subscribeId: this.subscribeId,
mode: CommonConstants.SUBSCRIBE_MODE,
medium: CommonConstants.SUBSCRIBE_MEDIUM,
freq: CommonConstants.SUBSCRIBE_FREQ,
isSameAccount: false,
isWakeRemote: true,
capability: CommonConstants.SUBSCRIBE_CAPABILITY
};
// 發(fā)現(xiàn)周邊設備
this.myDeviceManager.startDeviceDiscovery(info);
} catch (error) {
Logger.error('RemoteDeviceModel',
`startDeviceDiscovery failed error=${JSON.stringify(error)}`);
}
}
// 停止發(fā)現(xiàn)設備
stopDeviceDiscovery(): void {
...
try {
// 停止發(fā)現(xiàn)設備
this.myDeviceManager.stopDeviceDiscovery(this.subscribeId);
// 注銷監(jiān)聽任務
this.myDeviceManager.off('deviceFound');
this.myDeviceManager.off('discoverFail');
} catch (error) {
Logger.error('RemoteDeviceModel',
`stopDeviceDiscovery failed error=${JSON.stringify(error)}`);
}
}
...
}
選擇彈窗內的設備項提交后,執(zhí)行設備驗證。
- 若設備在信任設備列表,執(zhí)行startAbility()方法啟動連接設備上的應用,將當前的繪制信息作為參數(shù)發(fā)送至連接設備。
- 若設備不是信任設備,執(zhí)行authenticateDevice()方法啟動驗證。此時連接設備提示是否接受,接收連接后連接設備展示PIN碼,本地設備輸入PIN碼確認后連接成功。再次點擊查詢設備按鈕,選擇已連接設備,點擊確認啟動連接設備上的應用。
// RemoteDeviceUtil.ets
class RemoteDeviceUtil {
...
// 設備驗證
authenticateDevice(
context: common.UIAbilityContext,
device: deviceManager.DeviceInfo,
positionList: Position[]
): void {
// 設備為信任設備,啟動連接設備上的應用
let tmpList = this.trustedDeviceList.filter((item: deviceManager.DeviceInfo) = > device.deviceId === item.deviceId);
if (tmpList.length > 0) {
this.startAbility(context, device, positionList);
return;
}
...
try {
// 執(zhí)行設備認證,啟動驗證相關彈窗,接受信任,顯示PIN碼,輸入PIN碼等
this.myDeviceManager.authenticateDevice(device, authParam, (err) = > {
...
})
} catch (error) {
Logger.error('RemoteDeviceModel',
`authenticateDevice failed error=${JSON.stringify(error)}`);
}
}
// 啟動連接設備上的應用
startAbility(context: common.UIAbilityContext, device: deviceManager.DeviceInfo, positionList: Position[]): void {
...
// 啟動連接設備上的應用
context.startAbility(wantValue).then(() = > {
Logger.info('RemoteDeviceModel', `startAbility finished wantValue=${JSON.stringify(wantValue)}`);
}).catch((error: Error) = > {
Logger.error('RemoteDeviceModel', `startAbility failed, error=${JSON.stringify(error)}`);
})
}
...
}
資源釋放
程序關閉時,注銷設備狀態(tài)監(jiān)聽任務,并釋放DeviceManager實例。
// RemoteDeviceUtil.ets
class RemoteDeviceUtil {
...
// 注銷監(jiān)聽任務
unregisterDeviceListCallback(): void {
...
try {
// 注銷設備狀態(tài)監(jiān)聽
this.myDeviceManager.off('deviceStateChange');
// 釋放DeviceManager實例
this.myDeviceManager.release();
} catch (err) {
Logger.error('RemoteDeviceModel',
`unregisterDeviceListCallback stopDeviceDiscovery failed, error=${JSON.stringify(err)}`);
}
}
...
}
繪制功能
Canvas組件區(qū)域監(jiān)聽觸摸事件,按照按下、移動、抬起等觸摸事件,記錄繪制的起點、中間點以及終點。觸摸事件觸發(fā)時,使用CanvasRenderingContext2D對象的繪制方法根據(jù)位置信息進行繪制。繪制結束后,將當前位置信息列表存入分布式鍵值數(shù)據(jù)庫。
// Index.ets
let storage = LocalStorage.getShared();
@Entry(storage)
@Component
struct Index {
...
build() {
Column() {
...
Row() {
Canvas(this.canvasContext)
...
}
.onTouch((event: TouchEvent) = > {
this.onTouchEvent(event);
})
...
}
...
}
// 繪制事件
onTouchEvent(event: TouchEvent): void {
let positionX: number = event.touches[0].x;
let positionY: number = event.touches[0].y;
switch (event.type) {
// 手指按下
case TouchType.Down: {
this.canvasContext.beginPath();
this.canvasContext.lineWidth = CommonConstants.CANVAS_LINE_WIDTH;
this.canvasContext.lineJoin = CommonConstants.CANVAS_LINE_JOIN;
this.canvasContext.moveTo(positionX, positionY);
this.pushData(true, false, positionX, positionY);
break;
}
// 手指移動
case TouchType.Move: {
this.canvasContext.lineTo(positionX, positionY);
this.pushData(false, false, positionX, positionY);
break;
}
// 手指抬起
case TouchType.Up: {
this.canvasContext.lineTo(positionX, positionY);
this.canvasContext.stroke();
this.pushData(false, true, positionX, positionY);
break;
}
default: {
break;
}
}
}
pushData(isFirstPosition: boolean, isEndPosition: boolean, positionX: number, positionY: number): void {
let position = new Position(isFirstPosition, isEndPosition, positionX, positionY);
// 存入位置信息列表
this.positionList.push(position);
if (position.isEndPosition) {
// 當前位置為終點時,將位置信息列表存入分布式鍵值數(shù)據(jù)庫
this.kvStoreModel.put(CommonConstants.CHANGE_POSITION, JSON.stringify(this.positionList));
}
}
...
}
點擊撤銷按鈕時,從位置列表中后序遍歷移除位置信息,直到找到軌跡的初始位置,完成移除上一次繪制的軌跡。移除完成后將位置信息列表存入分布式鍵值數(shù)據(jù)庫中。執(zhí)行redraw()方法,清空畫板上的內容,遍歷位置信息列表,重新繪制。
// Index.ets
let storage = LocalStorage.getShared();
@Entry(storage)
@Component
struct Index {
...
@LocalStorageProp('positionList') positionList: Position[] = [];
...
build() {
Column() {
Row() {
// 撤銷按鈕
Image($r('app.media.ic_back'))
.width($r('app.float.ic_back_width'))
.height($r('app.float.ic_back_height'))
.margin({ left: CommonConstants.ICON_MARGIN_LEFT })
.onClick(() = > {
this.goBack();
})
...
}
.width(CommonConstants.FULL_PERCENT)
.height(CommonConstants.TITLE_HEIGHT)
...
}
...
redraw(): void {
// 刪除畫布內的繪制內容
this.canvasContext.clearRect(0, 0, this.canvasContext.width, this.canvasContext.height);
// 使用當前記錄的位置信息,重新繪制
this.positionList.forEach((position) = > {
...
if (position.isFirstPosition) {
this.canvasContext.beginPath();
this.canvasContext.lineWidth = CommonConstants.CANVAS_LINE_WIDTH;
this.canvasContext.lineJoin = CommonConstants.CANVAS_LINE_JOIN;
this.canvasContext.moveTo(position.positionX, position.positionY);
} else {
this.canvasContext.lineTo(position.positionX, position.positionY);
if (position.isEndPosition) {
this.canvasContext.stroke();
}
}
});
}
// 撤回上一筆繪制
goBack(): void {
if (this.positionList.length === 0) {
return;
}
// 移除位置信息直到位置起始位置
for (let i: number = this.positionList.length - 1; i >= 0; i--) {
let position: Position | undefined = this.positionList.pop();
if (position !== undefined && position.isFirstPosition) {
break;
}
}
this.redraw();
this.kvStoreModel.put(CommonConstants.CHANGE_POSITION, JSON.stringify(this.positionList));
}
...
}
分布式鍵值數(shù)據(jù)庫
使用分布式鍵值數(shù)據(jù)庫需申請數(shù)據(jù)交換權限:ohos.permission.DISTRIBUTED_DATASYNC。
應用啟動時創(chuàng)建分布式鍵值數(shù)據(jù)庫,設置數(shù)據(jù)庫數(shù)據(jù)改變監(jiān)聽。數(shù)據(jù)改變時執(zhí)行回調,獲取插入或更新數(shù)據(jù)列表,遍歷列表,匹配位置信息列表的設置key,更新位置列表后重新繪制。
// Index.ets
...
import KvStoreModel from '../viewmodel/KvStoreModel';
...
let storage = LocalStorage.getShared();
@Entry(storage)
@Component
struct Index {
...
private kvStoreModel: KvStoreModel = new KvStoreModel();
...
aboutToAppear() {
...
this.createKVStore();
}
...
createKVStore(): void {
// 創(chuàng)建分布式鍵值數(shù)據(jù)庫
this.kvStoreModel.createKvStore(this.context, (data: distributedKVStore.ChangeNotification) = > {
// 使用分布式鍵值數(shù)據(jù)庫內的內容重置位置信息列表
this.positionList = [];
let entries: distributedKVStore.Entry[] = data.insertEntries.length > 0 ? data.insertEntries : data.updateEntries;
entries.forEach((entry: distributedKVStore.Entry) = > {
if (CommonConstants.CHANGE_POSITION === entry.key) {
this.positionList = JSON.parse((entry.value.value) as string);
// 位置信息列表更新后,重新繪制
this.redraw();
}
});
});
}
...
}
創(chuàng)建分布式鍵值數(shù)據(jù)庫。設置數(shù)據(jù)庫類型為KVStoreType.SINGLE_VERSION單版本數(shù)據(jù)庫,其他配置參考[創(chuàng)建數(shù)據(jù)庫配置信息]。創(chuàng)建數(shù)據(jù)庫成功后,調用enableSync()方法開啟同步,調用setDataChangeListener()方法訂閱數(shù)據(jù)變更通知。
// KvStoreModel.ets
export default class KvStoreModel {
...
kvStore?: distributedKVStore.SingleKVStore;
...
createKvStore(
context: common.UIAbilityContext,
callback: (data: distributedKVStore.ChangeNotification) = > void
): void {
...
try {
// 創(chuàng)建一個KVManager對象實例,用于管理數(shù)據(jù)庫對象
this.kvManager = distributedKVStore.createKVManager(config);
} catch (error) {
Logger.error('KvStoreModel',
`createKvStore createKVManager failed, err=${JSON.stringify(error)}`);
return;
}
// 創(chuàng)建數(shù)據(jù)庫的配置信息
let options: distributedKVStore.Options = {
...
kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION
...
};
// 獲取分布式鍵值數(shù)據(jù)庫
this.kvManager.getKVStore(CommonConstants.KVSTORE_ID, options).then((store: distributedKVStore.SingleKVStore) = > {
...
this.kvStore = store;
// 開啟同步
this.kvStore.enableSync(true).then(() = > {
Logger.info('KvStoreModel', 'createKvStore enableSync success');
}).catch((error: Error) = > {
Logger.error('KvStoreModel',
`createKvStore enableSync fail, error=${JSON.stringify(error)}`);
});
this.setDataChangeListener(callback);
}).catch((error: Error) = > {
Logger.error('getKVStore',
`createKvStore getKVStore failed, error=${JSON.stringify(error)}`);
})
}
...
}
訂閱數(shù)據(jù)變更通知。創(chuàng)建分布式鍵值數(shù)據(jù)庫,設置數(shù)據(jù)變更訂閱,訂閱類型為全部,當更新數(shù)據(jù)集或插入數(shù)據(jù)集大于0時,執(zhí)行傳入的callback()方法。
// KvStoreModel.ets
export default class KvStoreModel {
...
kvStore?: distributedKVStore.SingleKVStore;
...
setDataChangeListener(callback: (data: distributedKVStore.ChangeNotification) = > void): void {
...
try {
// 訂閱數(shù)據(jù)變更通知
this.kvStore.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL,
(data: distributedKVStore.ChangeNotification) = > {
if ((data.updateEntries.length > 0) || (data.insertEntries.length > 0)) {
callback(data);
}
});
} catch (error) {
Logger.error('KvStoreModel',
`setDataChangeListener on('dataChange') failed, err=${JSON.stringify(error)}`);
}
}
...
}
應用退出時,分布式鍵值數(shù)據(jù)庫取消數(shù)據(jù)改變監(jiān)聽。
// Index.ets
...
import KvStoreModel from '../viewmodel/KvStoreModel';
...
let storage = LocalStorage.getShared();
@Entry(storage)
@Component
struct Index {
...
private kvStoreModel: KvStoreModel = new KvStoreModel();
...
aboutToDisappear() {
this.kvStoreModel.removeDataChangeListener();
}
...
}
// KvStoreModel.ets
export default class KvStoreModel {
...
kvStore?: distributedKVStore.SingleKVStore;
...
removeDataChangeListener(): void {
...
try {
// 取消數(shù)據(jù)改變監(jiān)聽
this.kvStore.off('dataChange');
} catch (error) {
Logger.error('KvStoreModel',
`removeDataChangeListener off('dataChange') failed, err=${JSON.stringify(error)}`);
}
}
...
}
審核編輯 黃宇
-
分布式
+關注
關注
1文章
858瀏覽量
74439 -
HarmonyOS
+關注
關注
79文章
1966瀏覽量
29962 -
OpenHarmony
+關注
關注
25文章
3635瀏覽量
16061 -
鴻蒙OS
+關注
關注
0文章
188瀏覽量
4359
發(fā)布評論請先 登錄
相關推薦
評論