1
电子说
目前家庭电视机主要通过其自带的遥控器进行操控,实现的功能较为单一。例如,当我们要在TV端搜索节目时,电视机在遥控器的操控下往往只能完成一些字母或数字的输入,而无法输入其他复杂的内容。分布式遥控器将手机的输入能力和电视遥控器的遥控能力结合为一体,从而快速便捷操控电视。
分布式遥控器的实现基于OpenHarmony的分布式能力和RPC通信能力,UI使用eTS进行开发。如下图所示,分别用两块开发板模拟TV端和手机端。
UI效果图如下:
图1 TV端主页默认页面
图2 手机端遥控页面
说明: 本示例涉及使用系统接口,需要手动替换Full SDK才能编译通过,具体操作可参考[替换指南]。
完成本篇Codelab我们首先要完成开发环境的搭建,本示例以RK3568开发板为例,参照以下步骤进行:
gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md
]本章节以系统自带的音乐播放器为例(具体以实际的应用为准),介绍如何完成两台设备的分布式组网。
硬件准备:准备两台烧录相同的版本系统的RK3568开发板A、B。
开发板A、B连接同一个WiFi网络。
打开设置-->WLAN-->点击右侧WiFi开关-->点击目标WiFi并输入密码。
将设备A,B设置为互相信任的设备。
配网完毕。
本篇Codelab只对核心代码进行讲解,首先来介绍下整个工程的代码结构:
在本章节中,您将学会开发TV端默认界面和TV端视频播放界面,示意图参考第一章图1和图3所示。
建立数据模型,将图片ID、图片源、图片名称和视频源绑定成一个数据模型。详情代码可以查看MainAbility/model/PicData.ets和MainAbility/model/PicDataModel.ets两个文件。
// 入口组件
@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')
}
}
// 九宮格拼图组件
@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%')
}
}
import router from '@system.router'
@Entry
@Component
struct Play {
// 取到Index页面跳转来时携带的source对应的数据。
private source: string = router.getParams().source
build() {
Column() {
Video({
src: this.source,
})
.width('100%')
.height('100%')
.autoPlay(true)
.controls(true)
}
}
}
Image(this.picItem.image)
......
.onClick(() = > {
router.push({
uri: 'pages/VideoPlay',
params: { source: this.picItem.video }
})
})
在本章节中,您将学会开发手机遥控端默认界面,示意图参考第一章图2所示。
@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')
// 关机键
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')
}
}
在本章节中,您将学会如何拉起在同一组网内的设备上的FA,并且连接远端Service服务。
首先通过TV端拉起手机端界面,并将本端的deviceId发送到手机端。
// 设备列表弹出框
@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 })
}
}
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) = > {
// 拉起远端后,连接远端service
connectModel.onConnectRemoteService(deviceId)
});
}
"abilities": [
...
{
"visible": true,
"srcPath": "ServiceAbility",
"name": ".ServiceAbility",
"icon": "$media:icon",
"srcLanguage": "ets",
"description": "$string:description_serviceability",
"type": "service"
}
],
成功拉起手机端界面后,通过接收TV端传过来的deviceId连接TV端的Service。在手机端的生命周期内增加aboutToAppear()事件,在界面被拉起的时候读取对方的deviceId并调用onConnectRemoteService()方法,连接对方的Service,实现代码如下:
async aboutToAppear() {
await featureAbility.getWant((error, want) = > {
// 远端被拉起后,连接对端的service
if (want.parameters.remoteDeviceId) {
let remoteDeviceId = want.parameters.remoteDeviceId
connectModel.onConnectRemoteService(remoteDeviceId)
}
});
}
建立一个ServiceAbility处理收到的消息并发布公共事件,详细代码请看ServiceAbility/service.ts文件。TV端订阅本端Service的公共事件,并接受和处理消息。
subscribeEvent() {
let self = this;
// 用于保存创建成功的订阅者对象,后续使用其完成订阅及退订的动作
var subscriber;
// 订阅者信息
var subscribeInfo = {
events: ["publish_change"],
priority: 100
};
// 设置有序公共事件的结果代码回调
function SetCodeCallBack() {
}
// 设置有序公共事件的结果数据回调
function SetDataCallBack() {
}
// 完成本次有序公共事件处理回调
function FinishCommonEventCallBack() {
}
// 订阅公共事件回调
function SubscribeCallBack(err, data) {
let msgData = data.data;
let code = data.code;
// 设置有序公共事件的结果代码
subscriber.setCode(code, SetCodeCallBack);
// 设置有序公共事件的结果数据
subscriber.setData(msgData, SetDataCallBack);
// 完成本次有序公共事件处理
subscriber.finishCommonEvent(FinishCommonEventCallBack)
// 处理接收到的数据data
......
// 创建订阅者回调
function CreateSubscriberCallBack(err, data) {
subscriber = data;
// 订阅公共事件
commonEvent.subscribe(subscriber, SubscribeCallBack);
}
// 创建订阅者
commonEvent.createSubscriber(subscribeInfo, CreateSubscriberCallBack);
}
}
async aboutToAppear() {
this.subscribeEvent();
}
成功连接远端Service服务后,在手机遥控器端进行按钮或者输入操作都会完成一次跨设备通讯,消息的传递是由手机遥控器端的FA传递到TV端的Service服务。这里将连接远端Service和发送消息抽象为ConnectModel,详细代码可查看MainAbility/model/ConnectModel.ets文件中sendMessageToRemoteService()方法。
手机端应用对TV端能做出的控制有:向上移动、向下移动、向左移动、向右移动、确定、返回、关闭。在手机端按键上增加点击事件,通过sendMessageToRemoteService()的方法发送到TV端Service。TV端根据发送code以及数据,进行数据处理,这里只展示TV端数据处理部分的核心代码:
// code = 1时,将手机遥控端search框内数据同步到TV端
if (code == 1) {
self.text = data.parameters.dataList;
}
// code = 2时,增加选中图片效果
if (code == 2) {
// 如果在图片序号范围内就选中图片,否则不更改
var tmp: number = +data.parameters.dataList;
if ((self.choose + tmp <= 5) && (self.choose + tmp >= 0)) {
self.choose += tmp;
}
}
// code = 3时,播放选中图片对应的视频
if (code == 3) {
self.picItems.forEach(function (item) {
if (item.id == self.choose) {
router.push({
uri: 'pages/VideoPlay',
params: { source: item.video }
})
}
})
}
// code = 4时,回到TV端默认页面
if (code == 4) {
router.push({
uri: 'pages/TVIndex',
})
}
// code = 5时,关闭程序
if (code == 5) {
featureAbility.terminateSelf()
}
// code = 6时,搜索图片名称并增加选中特效
if (code == 6) {
self.picItems.forEach(function (item) {
if (item.name == self.text) {
self.choose = Number(item.id)
}
})
}
审核编辑 黄宇
全部0条评论
快来发表一下你的评论吧 !