0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

OpenHarmony开发案例:【分布式遥控器】

jf_46214456 来源:jf_46214456 作者:jf_46214456 2024-04-16 16:44 次阅读

1.概述

目前家庭电视机主要通过其自带的遥控器进行操控,实现的功能较为单一。例如,当我们要在TV端搜索节目时,电视机在遥控器的操控下往往只能完成一些字母或数字的输入,而无法输入其他复杂的内容。分布式遥控器将手机的输入能力和电视遥控器的遥控能力结合为一体,从而快速便捷操控电视。

分布式遥控器的实现基于OpenHarmony的分布式能力和RPC通信能力,UI使用eTS进行开发。如下图所示,分别用两块开发板模拟TV端和手机端。

  1. 分布式组网后可以通过TV端界面的Controller按钮手动拉起手机端的遥控界面,在手机端输入时会将输入的内容同步显示在TV端搜索框,点击搜索按钮会根据输入的内容搜索相关节目。
  2. 还可以通过点击方向键(上下左右)将焦点移动到我们想要的节目上,再点击播放按钮进行播放,按返回按钮返回TV端主界面。
  3. 同时还可以通过手机遥控端关机按钮同时关闭TV端和手机端界面。

UI效果图如下:

图1 TV端主页默认页面

  • 图2 手机端遥控页面

说明: 本示例涉及使用系统接口,需要手动替换Full SDK才能编译通过,具体操作可参考[替换指南]。

2.搭建OpenHarmony环境

完成本篇Codelab我们首先要完成开发环境的搭建,本示例以RK3568开发板为例,参照以下步骤进行:

  1. [获取OpenHarmony系统版本]:标准系统解决方案(二进制)。
    以3.1版本为例:
  2. 搭建烧录环境。
    1. [完成DevEco Device Tool的安装]
    2. [完成RK3568开发板的烧录]
    3. 鸿蒙开发文档指导:[gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md]
  3. 搭建开发环境。
    1. 开始前请参考[工具准备],完成DevEco Studio的安装和开发环境配置。
    2. 开发环境配置完成后,请参考[使用工程向导]创建工程(模板选择“Empty Ability”),选择JS或者eTS语言开发。
    3. 工程创建完成后,选择使用[真机进行调测])。
      搜狗高速浏览器截图20240326151450.png

3.分布式组网

本章节以系统自带的音乐播放器为例(具体以实际的应用为准),介绍如何完成两台设备的分布式组网。

  1. 硬件准备:准备两台烧录相同的版本系统的RK3568开发板A、B。

  2. 开发板A、B连接同一个WiFi网络
    打开设置-->WLAN-->点击右侧WiFi开关-->点击目标WiFi并输入密码。

  3. 将设备A,B设置为互相信任的设备。

    • 找到系统应用“音乐”。
    • 设备A打开音乐,点击左下角流转按钮,弹出列表框,在列表中会展示远端设备的id。选择远端设备B的id,另一台开发板(设备B)会弹出验证的选项框。
    • 设备B点击允许,设备B将会弹出随机PIN码,将设备B的PIN码输入到设备A的PIN码填入框中。

    配网完毕。

4.代码结构解读

本篇Codelab只对核心代码进行讲解,首先来介绍下整个工程的代码结构:

  • MainAbility:
    • model:数据模型。
      • RemoteDeviceModel.ets:获取组网内的设备列表模型。
      • PicData.ets:图片信息数据。
      • PicDataModel.ets:图片信息模型。
      • ConnectModel.ets:连接远端Service和发送消息模型。
    • pages:存放TV端各个页面。
      • TVindex.ets:TV端主页面。
      • VideoPlay.ets:TV端视频播放页面。
  • PhoneAbility:存放应用手机控制端主页面。
    • pages/PhoneIndex.ets:手机控制端主页面。
  • ServiceAbility:存放ServiceAbility相关文件。
    • service.ts:service服务,用于跨设备连接后通讯。
  • resources :存放工程使用到的资源文件。
    • resources/rawfile:存放工程中使用的图片资源文件。
  • config.json:配置文件。

5.实现TV端界面

在本章节中,您将学会开发TV端默认界面和TV端视频播放界面,示意图参考第一章图1和图3所示。

建立数据模型,将图片ID、图片源、图片名称和视频源绑定成一个数据模型。详情代码可以查看MainAbility/model/PicData.ets和MainAbility/model/PicDataModel.ets两个文件。

  1. 实现TV端默认页面布局和样式。
    • 在MainAbility/pages/TVIndex.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')
        }
      }
      
    • 其中PicGridItem将PicItem的图片源和图片名称绑定,实现代码如下:
      // 九宮格拼图组件
      @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%')
        }
      }
      
  2. 实现TV端视频播放界面。
    • 在MainAbility/pages/VideoPlay.ets 文件中添加组件。页面布局代码如下:
      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)
          }
        }
      }
      
    • 在MainAbility/pages/TVIndex.ets中,给PicGridItem的图片添加点击事件,点击图片即可播放PicItem的视频源。实现代码如下:
      Image(this.picItem.image)
              ......
              .onClick(() = > {
                router.push({
                  uri: 'pages/VideoPlay',
                  params: { source: this.picItem.video }
                })
              })
      

6.实现手机遥控端界面

在本章节中,您将学会开发手机遥控端默认界面,示意图参考第一章图2所示。

  • PhoneAbility/pages/PhoneIndex.ets 主界面文件中添加入口组件。页面布局代码如下:
    @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')
      }
    }
    

7.实现分布式拉起和RPC通信

在本章节中,您将学会如何拉起在同一组网内的设备上的FA,并且连接远端Service服务。

  1. 首先通过TV端拉起手机端界面,并将本端的deviceId发送到手机端。

    • 点击TV端主页上的"Controller"按钮,增加.onClick()事件。调用RegisterDeviceListCallback()发现设备列表,并弹出设备列表选择框CustomDialogExample,选择设备后拉起远端FA。CustomDialogExample()代码如下:
      // 设备列表弹出框
      @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 })
        }
      }
      
    • 点击设备弹出框内的Text组件会调用onStartRemoteAbility()方法拉起远端FA(手机端),将TV端的deviceId传给手机端,并连接手机端的Service。因此在featureAbility.startAbility()成功的回调中也要调用onConnectRemoteService()方法。这里将连接远端Service和发送消息抽象为ConnectModel,详细代码可查看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) = > {
          // 拉起远端后,连接远端service
          connectModel.onConnectRemoteService(deviceId)
        });
      }
      
    • 需要注意的是,配置文件config.json中ServiceAbility的属性visible要设置为true,代码如下:
      "abilities": [
            ...
            {
              "visible": true,
              "srcPath": "ServiceAbility",
              "name": ".ServiceAbility",
              "icon": "$media:icon",
              "srcLanguage": "ets",
              "description": "$string:description_serviceability",
              "type": "service"
            }
      ],
      
  2. 成功拉起手机端界面后,通过接收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)
          }
        });
      }
    
  3. 建立一个ServiceAbility处理收到的消息并发布公共事件,详细代码请看ServiceAbility/service.ts文件。TV端订阅本端Service的公共事件,并接受和处理消息。

    • 创建SubscribeEvent(),实现代码如下:
    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);
      }
    }
    
    • 在TV端的生命周期内增加aboutToAppear()事件,订阅公共事件,实现代码如下:
    async aboutToAppear() {
        this.subscribeEvent();
      }
    
  4. 成功连接远端Service服务后,在手机遥控器端进行按钮或者输入操作都会完成一次跨设备通讯,消息的传递是由手机遥控器端的FA传递到TV端的Service服务。这里将连接远端Service和发送消息抽象为ConnectModel,详细代码可查看MainAbility/model/ConnectModel.ets文件中sendMessageToRemoteService()方法。

8.设置遥控器远端事件

手机端应用对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)
    }
  })
}

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 遥控器
    +关注

    关注

    18

    文章

    815

    浏览量

    64255
  • 分布式
    +关注

    关注

    1

    文章

    754

    浏览量

    74096
  • 鸿蒙
    +关注

    关注

    55

    文章

    1641

    浏览量

    42123
  • HarmonyOS
    +关注

    关注

    79

    文章

    1861

    浏览量

    29267
  • OpenHarmony
    +关注

    关注

    23

    文章

    3322

    浏览量

    15161
收藏 人收藏

    评论

    相关推荐

    OpenHarmony南向开发案例:【分布式画板】

    使用OpenHarmony3.1-Release开发的应用。通过OpenHarmony分布式技术,使多人能够一起画画。
    的头像 发表于 04-12 14:40 519次阅读
    <b class='flag-5'>OpenHarmony</b>南向<b class='flag-5'>开发案</b>例:【<b class='flag-5'>分布式</b>画板】

    HarmonyOS应用开发-分布式设计

    不同终端设备之间的极速连接、硬件协同、资源共享,为用户提供最佳的场景体验。分布式设计指南可以帮助应用开发者了解如何充分发挥“One Super Device”的能力,提供独特的跨设备交互体验。说明:本设计指南后续举例中将包括手机、智慧屏、手表等多种设备,其中手机均指 EM
    发表于 09-22 17:11

    OpenHarmony 2.2 Beta2 版本发布,具备典型的分布式能力和媒体类产品开发能力

    OpenHarmony 具备了典型的分布式能力和媒体类产品开发能力。即日起,全球开发者可通过 Gitee 和镜像站点下载完整代码(https://gitee.com/
    发表于 08-09 15:15

    OpenHarmony分布式软总线流程分析

    OpenHarmony分布式软总线流程分析,大神总结,大家可以下载去学习了~.~
    发表于 11-19 15:56

    基于润和DAYU200开发套件的OpenHarmony分布式音乐播放

    :参考DevEco Studio(OpenHarmony)使用指南搭建OpenHarmony应用开发环境、并导入本工程进行编译、运行。运行结果截图:【分布式流转体验】硬件准备:准备两台
    发表于 03-14 09:07

    OpenHarmony标准设备应用开发(三)——分布式数据管理

    (以下内容来自开发者分享,不代表 OpenHarmony 项目群工作委员会观点)邢碌上一章,我们通过分布式音乐播放分布式***、
    发表于 04-07 18:48

    OpenHarmony3.1分布式技术资料合集

    客户端(ScreenClient):屏幕图像显示代理客户端,用于在设备上显示其他设备投射过来的屏幕图像数据。3、OpenHarmony3.1的分布式手写板1.介绍基于TS扩展的声明开发
    发表于 04-11 11:50

    OpenHarmony开发之Ability架构

    ][分布式手写板(eTS)][分布式鉴权(JS)][分布式游戏手柄(eTS)][分布式邮件(eTS)][分布式亲子早教系统(eTS)][
    发表于 05-12 15:12

    DAYU200 | 分布式遥控器

    遥控器将手机的输入能力和电视遥控器遥控能力结合为一体,从而快速便捷操控电视。分布式遥控器的实现基于Op
    发表于 05-25 15:47

    【学习打卡】OpenHarmony分布式任务调度

    之前我们分享过分布式软总线和分布式数据管理,今天主要说一下OpenHarmony分布式任务调度,分布式任务调度是建立在
    发表于 07-18 17:06

    开发样例】OpenHarmony分布式购物车

    设计OpenHarmony技术特性eTS UI分布式调度分布式数据管理3.支持OpenHarmony版本OpenHarmony 3.0 LT
    发表于 07-29 14:17

    OpenHarmony 分布式硬件关键技术

    的视频会议;在影音娱乐场景下,能够轻松地把手机音视频放到电视和音箱上播放,还可以让家里的灯光自动跟随电影和音乐进行变化,实现非常震撼的家庭影院的效果。 期待越来越多的开发者参与OpenHarmony的生态中来,共同研究和探讨分布式
    发表于 08-24 17:25

    分布式系统硬件资源池原理和接入实践

    提供更好的服务体验。 图 3 鸿蒙硬件资源池支持各类消费者场景 2.2 开发者场景 对于开发者来说,由于分布式硬件资源池将跨设备硬件调用的复杂度都封装在了系统底层,跨设备硬件复用本地硬件的 API
    发表于 12-06 10:02

    基于OpenHarmony分布式应用开发框架使用教程

    电子发烧友网站提供《基于OpenHarmony分布式应用开发框架使用教程.zip》资料免费下载
    发表于 04-12 11:19 6次下载

    OpenHarmony知识赋能No.29-DAYU200分布式应用开发

    OpenHarmony标准系统北向开发高手。   嘉宾介绍: 徐建国 资深技术专家(江苏润开鸿数字科技有限公司)   课程内容: 1.OpenHarmony分布式API介绍 a.
    的头像 发表于 05-04 09:57 633次阅读
    <b class='flag-5'>OpenHarmony</b>知识赋能No.29-DAYU200<b class='flag-5'>分布式</b>应用<b class='flag-5'>开发</b>