1.概述
目前家庭電視機(jī)主要通過(guò)其自帶的遙控器進(jìn)行操控,實(shí)現(xiàn)的功能較為單一。例如,當(dāng)我們要在TV端搜索節(jié)目時(shí),電視機(jī)在遙控器的操控下往往只能完成一些字母或數(shù)字的輸入,而無(wú)法輸入其他復(fù)雜的內(nèi)容。分布式遙控器將手機(jī)的輸入能力和電視遙控器的遙控能力結(jié)合為一體,從而快速便捷操控電視。
分布式遙控器的實(shí)現(xiàn)基于OpenHarmony的分布式能力和RPC通信能力,UI使用eTS進(jìn)行開(kāi)發(fā)。如下圖所示,分別用兩塊開(kāi)發(fā)板模擬TV端和手機(jī)端。
- 分布式組網(wǎng)后可以通過(guò)TV端界面的Controller按鈕手動(dòng)拉起手機(jī)端的遙控界面,在手機(jī)端輸入時(shí)會(huì)將輸入的內(nèi)容同步顯示在TV端搜索框,點(diǎn)擊搜索按鈕會(huì)根據(jù)輸入的內(nèi)容搜索相關(guān)節(jié)目。
- 還可以通過(guò)點(diǎn)擊方向鍵(上下左右)將焦點(diǎn)移動(dòng)到我們想要的節(jié)目上,再點(diǎn)擊播放按鈕進(jìn)行播放,按返回按鈕返回TV端主界面。
- 同時(shí)還可以通過(guò)手機(jī)遙控端關(guān)機(jī)按鈕同時(shí)關(guān)閉TV端和手機(jī)端界面。
UI效果圖如下:
圖1 TV端主頁(yè)默認(rèn)頁(yè)面
圖2 手機(jī)端遙控頁(yè)面
- 圖3 TV端視頻播放頁(yè)面
說(shuō)明: 本示例涉及使用系統(tǒng)接口,需要手動(dòng)替換Full SDK才能編譯通過(guò),具體操作可參考[替換指南]。
2.搭建OpenHarmony環(huán)境
完成本篇Codelab我們首先要完成開(kāi)發(fā)環(huán)境的搭建,本示例以RK3568開(kāi)發(fā)板為例,參照以下步驟進(jìn)行:
- [獲取OpenHarmony系統(tǒng)版本]:標(biāo)準(zhǔn)系統(tǒng)解決方案(二進(jìn)制)。
以3.1版本為例: - 搭建燒錄環(huán)境。
- 搭建開(kāi)發(fā)環(huán)境。
- 開(kāi)始前請(qǐng)參考[工具準(zhǔn)備],完成DevEco Studio的安裝和開(kāi)發(fā)環(huán)境配置。
- 開(kāi)發(fā)環(huán)境配置完成后,請(qǐng)參考[使用工程向?qū)創(chuàng)建工程(模板選擇“Empty Ability”),選擇JS或者eTS語(yǔ)言開(kāi)發(fā)。
- 工程創(chuàng)建完成后,選擇使用[真機(jī)進(jìn)行調(diào)測(cè)])。
3.分布式組網(wǎng)
本章節(jié)以系統(tǒng)自帶的音樂(lè)播放器為例(具體以實(shí)際的應(yīng)用為準(zhǔn)),介紹如何完成兩臺(tái)設(shè)備的分布式組網(wǎng)。
硬件準(zhǔn)備:準(zhǔn)備兩臺(tái)燒錄相同的版本系統(tǒng)的RK3568開(kāi)發(fā)板A、B。
開(kāi)發(fā)板A、B連接同一個(gè)WiFi網(wǎng)絡(luò)。
打開(kāi)設(shè)置-->WLAN-->點(diǎn)擊右側(cè)WiFi開(kāi)關(guān)-->點(diǎn)擊目標(biāo)WiFi并輸入密碼。將設(shè)備A,B設(shè)置為互相信任的設(shè)備。
- 找到系統(tǒng)應(yīng)用“音樂(lè)”。
- 設(shè)備A打開(kāi)音樂(lè),點(diǎn)擊左下角流轉(zhuǎn)按鈕,彈出列表框,在列表中會(huì)展示遠(yuǎn)端設(shè)備的id。選擇遠(yuǎn)端設(shè)備B的id,另一臺(tái)開(kāi)發(fā)板(設(shè)備B)會(huì)彈出驗(yàn)證的選項(xiàng)框。
- 設(shè)備B點(diǎn)擊允許,設(shè)備B將會(huì)彈出隨機(jī)PIN碼,將設(shè)備B的PIN碼輸入到設(shè)備A的PIN碼填入框中。
配網(wǎng)完畢。
- 找到系統(tǒng)應(yīng)用“音樂(lè)”。
4.代碼結(jié)構(gòu)解讀
本篇Codelab只對(duì)核心代碼進(jìn)行講解,首先來(lái)介紹下整個(gè)工程的代碼結(jié)構(gòu):
- MainAbility:
- PhoneAbility:存放應(yīng)用手機(jī)控制端主頁(yè)面。
- pages/PhoneIndex.ets:手機(jī)控制端主頁(yè)面。
- ServiceAbility:存放ServiceAbility相關(guān)文件。
- service.ts:service服務(wù),用于跨設(shè)備連接后通訊。
- resources :存放工程使用到的資源文件。
- resources/rawfile:存放工程中使用的圖片資源文件。
- config.json:配置文件。
5.實(shí)現(xiàn)TV端界面
在本章節(jié)中,您將學(xué)會(huì)開(kāi)發(fā)TV端默認(rèn)界面和TV端視頻播放界面,示意圖參考第一章圖1和圖3所示。
建立數(shù)據(jù)模型,將圖片ID、圖片源、圖片名稱和視頻源綁定成一個(gè)數(shù)據(jù)模型。詳情代碼可以查看MainAbility/model/PicData.ets和MainAbility/model/PicDataModel.ets兩個(gè)文件。
- 實(shí)現(xiàn)TV端默認(rèn)頁(yè)面布局和樣式。
- 在MainAbility/pages/TVIndex.ets 主界面文件中添加入口組件。頁(yè)面布局代碼如下:
// 入口組件 @Entry @Component struct Index { private letters: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] private source: string @State text: string = '' @State choose: number = -1 build() { Flex({ direction: FlexDirection.Column }) { TextInput({text: this.text, placeholder: 'Search' }) .onChange((value: string) = > { this.text = value }) Row({space: 30}) { Text('Clear') .fontSize(16) .backgroundColor('#ABB0BA') .textAlign(TextAlign.Center) .onClick(() = > { this.text = '' }) .clip(true) .borderRadius(10) Text('Backspace') .fontSize(16) .backgroundColor('#ABB0BA') .textAlign(TextAlign.Center) .onClick(() = > { this.text = this.text.substring(0, this.text.length - 1) }) .clip(true) .borderRadius(10) Text('Controller') .fontSize(16) .backgroundColor('#ABB0BA') .textAlign(TextAlign.Center) .onClick(() = > { ...... }) .clip(true) .borderRadius(10) } Grid() { ForEach(this.letters, (item) = > { GridItem() { Text(item) .fontSize(20) .backgroundColor('#FFFFFF') .textAlign(TextAlign.Center) .onClick(() = > { this.text += item }) .clip(true) .borderRadius(5) } }, item = > item) } .rowsTemplate('1fr 1fr 1fr 1fr') .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr') .columnsGap(8) .rowsGap(8) .width('75%') .height('25%') .margin(5) .backgroundColor('#D2D3D8') .clip(true) .borderRadius(10) Grid() { ForEach(this.picItems, (item: PicData) = > { GridItem() { PicGridItem({ picItem: item }) } }, (item: PicData) = > item.id.toString()) } .rowsTemplate('1fr 1fr 1fr') .columnsTemplate('1fr 1fr') .columnsGap(5) .rowsGap(8) .width('90%') .height('58%') .backgroundColor('#FFFFFF') .margin(5) } .width('98%') .backgroundColor('#FFFFFF') } }
- 其中PicGridItem將PicItem的圖片源和圖片名稱綁定,實(shí)現(xiàn)代碼如下:
// 九宮格拼圖組件 @Component struct PicGridItem { private picItem: PicData build() { Column() { Image(this.picItem.image) .objectFit(ImageFit.Contain) .height('85%') .width('100%') .onClick(() = > { ...... }) }) Text(this.picItem.name) .fontSize(20) .fontColor('#000000') } .height('100%') .width('90%') } }
- 在MainAbility/pages/TVIndex.ets 主界面文件中添加入口組件。頁(yè)面布局代碼如下:
- 實(shí)現(xiàn)TV端視頻播放界面。
- 在MainAbility/pages/VideoPlay.ets 文件中添加組件。頁(yè)面布局代碼如下:
import router from '@system.router' @Entry @Component struct Play { // 取到Index頁(yè)面跳轉(zhuǎn)來(lái)時(shí)攜帶的source對(duì)應(yīng)的數(shù)據(jù)。 private source: string = router.getParams().source build() { Column() { Video({ src: this.source, }) .width('100%') .height('100%') .autoPlay(true) .controls(true) } } }
- 在MainAbility/pages/TVIndex.ets中,給PicGridItem的圖片添加點(diǎn)擊事件,點(diǎn)擊圖片即可播放PicItem的視頻源。實(shí)現(xiàn)代碼如下:
Image(this.picItem.image) ...... .onClick(() = > { router.push({ uri: 'pages/VideoPlay', params: { source: this.picItem.video } }) })
- 在MainAbility/pages/VideoPlay.ets 文件中添加組件。頁(yè)面布局代碼如下:
6.實(shí)現(xiàn)手機(jī)遙控端界面
在本章節(jié)中,您將學(xué)會(huì)開(kāi)發(fā)手機(jī)遙控端默認(rèn)界面,示意圖參考第一章圖2所示。
- PhoneAbility/pages/PhoneIndex.ets 主界面文件中添加入口組件。頁(yè)面布局代碼如下:
@Entry @Component struct Index { build() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) { Row() { Image($rawfile('TV.png')) .width(25) .height(25) Text('華為智慧屏').fontSize(20).margin(10) } // 文字搜索框 TextInput({ placeholder: 'Search' }) .margin(20) .onChange((value: string) = > { if (connectModel.mRemote){ ...... } }) Grid() { GridItem() { // 向上箭頭 Button({ type: ButtonType.Circle, stateEffect: true }) { Image($rawfile('up.png')).width(80).height(80) } .onClick(() = > { ...... }) .width(80) .height(80) .backgroundColor('#FFFFFF') } .columnStart(1) .columnEnd(5) GridItem() { // 向左箭頭 Button({ type: ButtonType.Circle, stateEffect: true }) { Image($rawfile('left.png')).width(80).height(80) } .onClick(() = > { ...... }) .width(80) .height(80) .backgroundColor('#FFFFFF') } GridItem() { // 播放鍵 Button({ type: ButtonType.Circle, stateEffect: true }) { Image($rawfile('play.png')).width(60).height(60) } .onClick(() = > { ...... }) .width(80) .height(80) .backgroundColor('#FFFFFF') } GridItem() { // 向右箭頭 Button({ type: ButtonType.Circle, stateEffect: true }) { Image($rawfile('right.png')).width(70).height(70) } .onClick(() = > { ...... }) .width(80) .height(80) .backgroundColor('#FFFFFF') } GridItem() { // 向下箭頭 Button({ type: ButtonType.Circle, stateEffect: true }) { Image($rawfile('down.png')).width(70).height(70) } .onClick(() = > { ...... }) .width(80) .height(80) .backgroundColor('#FFFFFF') } .columnStart(1) .columnEnd(5) } .rowsTemplate('1fr 1fr 1fr') .columnsTemplate('1fr 1fr 1fr') .backgroundColor('#FFFFFF') .margin(10) .clip(new Circle({ width: 325, height: 325 })) .width(350) .height(350) Row({ space:100 }) { // 返回鍵 Button({ type: ButtonType.Circle, stateEffect: true }) { Image($rawfile('return.png')).width(40).height(40) } .onClick(() = > { ...... }) .width(100) .height(100) .backgroundColor('#FFFFFF') // 關(guān)機(jī)鍵 Button({ type: ButtonType.Circle, stateEffect: true }) { Image($rawfile('off.png')).width(40).height(40) } .onClick(() = > { ...... }) .width(100) .height(100) .backgroundColor('#FFFFFF') // 搜索鍵 Button({ type: ButtonType.Circle, stateEffect: true }) { Image($rawfile('search.png')).width(40).height(40) } .onClick(() = > { ...... }) .width(100) .height(100) .backgroundColor('#FFFFFF') } .padding({ left:100 }) } .backgroundColor('#E3E3E3') } }
7.實(shí)現(xiàn)分布式拉起和RPC通信
在本章節(jié)中,您將學(xué)會(huì)如何拉起在同一組網(wǎng)內(nèi)的設(shè)備上的FA,并且連接遠(yuǎn)端Service服務(wù)。
首先通過(guò)TV端拉起手機(jī)端界面,并將本端的deviceId發(fā)送到手機(jī)端。
- 點(diǎn)擊TV端主頁(yè)上的"Controller"按鈕,增加.onClick()事件。調(diào)用RegisterDeviceListCallback()發(fā)現(xiàn)設(shè)備列表,并彈出設(shè)備列表選擇框CustomDialogExample,選擇設(shè)備后拉起遠(yuǎn)端FA。CustomDialogExample()代碼如下:
// 設(shè)備列表彈出框 @CustomDialog struct CustomDialogExample { @State editFlag: boolean = false controller: CustomDialogController cancel: () = > void confirm: () = > void build() { Column() { List({ space: 10, initialIndex: 0 }) { ForEach(DeviceIdList, (item) = > { ListItem() { Row() { Text(item) .width('87%') .height(50) .fontSize(10) .textAlign(TextAlign.Center) .borderRadius(10) .backgroundColor(0xFFFFFF) .onClick(() = > { onStartRemoteAbility(item); this.controller.close(); }) } }.editable(this.editFlag) }, item = > item) } }.width('100%').height(200).backgroundColor(0xDCDCDC).padding({ top: 5 }) } }
- 點(diǎn)擊設(shè)備彈出框內(nèi)的Text組件會(huì)調(diào)用onStartRemoteAbility()方法拉起遠(yuǎn)端FA(手機(jī)端),將TV端的deviceId傳給手機(jī)端,并連接手機(jī)端的Service。因此在featureAbility.startAbility()成功的回調(diào)中也要調(diào)用onConnectRemoteService()方法。這里將連接遠(yuǎn)端Service和發(fā)送消息抽象為ConnectModel,詳細(xì)代碼可查看MainAbility/model/ConnectModel.ets文件中onConnectRemoteService()方法。onStartRemoteAbility()方法的代碼如下:
function onStartRemoteAbility(deviceId) { AuthDevice(deviceId); let numDevices = remoteDeviceModel.deviceList.length; if (numDevices === 0) { prompt.showToast({ message: "onStartRemoteAbility no device found" }); return; } var params = { remoteDeviceId: localDeviceId } var wantValue = { bundleName: 'com.example.helloworld0218', abilityName: 'com.example.helloworld0218.PhoneAbility', deviceId: deviceId, parameters: params }; featureAbility.startAbility({ want: wantValue }).then((data) = > { // 拉起遠(yuǎn)端后,連接遠(yuǎn)端service connectModel.onConnectRemoteService(deviceId) }); }
- 需要注意的是,配置文件config.json中ServiceAbility的屬性visible要設(shè)置為true,代碼如下:
"abilities": [ ... { "visible": true, "srcPath": "ServiceAbility", "name": ".ServiceAbility", "icon": "$media:icon", "srcLanguage": "ets", "description": "$string:description_serviceability", "type": "service" } ],
- 點(diǎn)擊TV端主頁(yè)上的"Controller"按鈕,增加.onClick()事件。調(diào)用RegisterDeviceListCallback()發(fā)現(xiàn)設(shè)備列表,并彈出設(shè)備列表選擇框CustomDialogExample,選擇設(shè)備后拉起遠(yuǎn)端FA。CustomDialogExample()代碼如下:
成功拉起手機(jī)端界面后,通過(guò)接收TV端傳過(guò)來(lái)的deviceId連接TV端的Service。在手機(jī)端的生命周期內(nèi)增加aboutToAppear()事件,在界面被拉起的時(shí)候讀取對(duì)方的deviceId并調(diào)用onConnectRemoteService()方法,連接對(duì)方的Service,實(shí)現(xiàn)代碼如下:
async aboutToAppear() { await featureAbility.getWant((error, want) = > { // 遠(yuǎn)端被拉起后,連接對(duì)端的service if (want.parameters.remoteDeviceId) { let remoteDeviceId = want.parameters.remoteDeviceId connectModel.onConnectRemoteService(remoteDeviceId) } }); }
建立一個(gè)ServiceAbility處理收到的消息并發(fā)布公共事件,詳細(xì)代碼請(qǐng)看ServiceAbility/service.ts文件。TV端訂閱本端Service的公共事件,并接受和處理消息。
- 創(chuàng)建SubscribeEvent(),實(shí)現(xiàn)代碼如下:
subscribeEvent() { let self = this; // 用于保存創(chuàng)建成功的訂閱者對(duì)象,后續(xù)使用其完成訂閱及退訂的動(dòng)作 var subscriber; // 訂閱者信息 var subscribeInfo = { events: ["publish_change"], priority: 100 }; // 設(shè)置有序公共事件的結(jié)果代碼回調(diào) function SetCodeCallBack() { } // 設(shè)置有序公共事件的結(jié)果數(shù)據(jù)回調(diào) function SetDataCallBack() { } // 完成本次有序公共事件處理回調(diào) function FinishCommonEventCallBack() { } // 訂閱公共事件回調(diào) function SubscribeCallBack(err, data) { let msgData = data.data; let code = data.code; // 設(shè)置有序公共事件的結(jié)果代碼 subscriber.setCode(code, SetCodeCallBack); // 設(shè)置有序公共事件的結(jié)果數(shù)據(jù) subscriber.setData(msgData, SetDataCallBack); // 完成本次有序公共事件處理 subscriber.finishCommonEvent(FinishCommonEventCallBack) // 處理接收到的數(shù)據(jù)data ...... // 創(chuàng)建訂閱者回調(diào) function CreateSubscriberCallBack(err, data) { subscriber = data; // 訂閱公共事件 commonEvent.subscribe(subscriber, SubscribeCallBack); } // 創(chuàng)建訂閱者 commonEvent.createSubscriber(subscribeInfo, CreateSubscriberCallBack); } }
- 在TV端的生命周期內(nèi)增加aboutToAppear()事件,訂閱公共事件,實(shí)現(xiàn)代碼如下:
async aboutToAppear() { this.subscribeEvent(); }
成功連接遠(yuǎn)端Service服務(wù)后,在手機(jī)遙控器端進(jìn)行按鈕或者輸入操作都會(huì)完成一次跨設(shè)備通訊,消息的傳遞是由手機(jī)遙控器端的FA傳遞到TV端的Service服務(wù)。這里將連接遠(yuǎn)端Service和發(fā)送消息抽象為ConnectModel,詳細(xì)代碼可查看MainAbility/model/ConnectModel.ets文件中sendMessageToRemoteService()方法。
8.設(shè)置遙控器遠(yuǎn)端事件
手機(jī)端應(yīng)用對(duì)TV端能做出的控制有:向上移動(dòng)、向下移動(dòng)、向左移動(dòng)、向右移動(dòng)、確定、返回、關(guān)閉。在手機(jī)端按鍵上增加點(diǎn)擊事件,通過(guò)sendMessageToRemoteService()的方法發(fā)送到TV端Service。TV端根據(jù)發(fā)送code以及數(shù)據(jù),進(jìn)行數(shù)據(jù)處理,這里只展示TV端數(shù)據(jù)處理部分的核心代碼:
// code = 1時(shí),將手機(jī)遙控端search框內(nèi)數(shù)據(jù)同步到TV端
if (code == 1) {
self.text = data.parameters.dataList;
}
// code = 2時(shí),增加選中圖片效果
if (code == 2) {
// 如果在圖片序號(hào)范圍內(nèi)就選中圖片,否則不更改
var tmp: number = +data.parameters.dataList;
if ((self.choose + tmp <= 5) && (self.choose + tmp >= 0)) {
self.choose += tmp;
}
}
// code = 3時(shí),播放選中圖片對(duì)應(yīng)的視頻
if (code == 3) {
self.picItems.forEach(function (item) {
if (item.id == self.choose) {
router.push({
uri: 'pages/VideoPlay',
params: { source: item.video }
})
}
})
}
// code = 4時(shí),回到TV端默認(rèn)頁(yè)面
if (code == 4) {
router.push({
uri: 'pages/TVIndex',
})
}
// code = 5時(shí),關(guān)閉程序
if (code == 5) {
featureAbility.terminateSelf()
}
// code = 6時(shí),搜索圖片名稱并增加選中特效
if (code == 6) {
self.picItems.forEach(function (item) {
if (item.name == self.text) {
self.choose = Number(item.id)
}
})
}
審核編輯 黃宇
-
遙控器
+關(guān)注
關(guān)注
18文章
829瀏覽量
65867 -
分布式
+關(guān)注
關(guān)注
1文章
858瀏覽量
74439 -
鴻蒙
+關(guān)注
關(guān)注
57文章
2302瀏覽量
42689 -
HarmonyOS
+關(guān)注
關(guān)注
79文章
1966瀏覽量
29962 -
OpenHarmony
+關(guān)注
關(guān)注
25文章
3635瀏覽量
16061
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論