当前位置: 首页 > news >正文

Android软件适配遥控器需求-案例经验分享

不分大屏产品需要有遥控器功能,这里分享部分实战经验

文章目录

  • 前言
  • 一、案例部分效果图
  • 二、项目基础架构
  • 三、焦点基础知识
    • 适配遥控器基础-焦点问题
    • 焦点管理
      • 明确焦点状态
        • 布局实现
        • 硬编码实现
        • 引入第三方自定义组件实现
      • 焦点顺序
        • 作用
      • 初始焦点 requestFocus
    • 按键处理
    • 获取当前焦点
  • 四、实际开发技能分享
    • 处理焦点注意实现
    • RecycleView 案例分析
  • 总结


前言

十多年的Android软件开发中,基本上都是做方案上的软件产品。 对于 电视、投影、闺蜜机 上面的软件 都有遥控器控制的需求,就需要自己的Android App能够受遥控器控制。 这里举一个案例,分享一下开发中的部分经验。 也方便自己下次开发直接复用经验,高效开发。


一、案例部分效果图

当前分享案例中部分效果图如下

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

二、项目基础架构

为什么要简单列举一下架构图,其实不同的UI架构会遇到各种不一样的问题,这里针对性的从列举项目上展示一下架构,方便分析和理解部分阐明的问题

在这里插入图片描述

三、焦点基础知识

适配遥控器基础-焦点问题

焦点管理

软件App 适配遥控器,需要用遥控器的功能,实际上就是处理焦点问题。当UI获取焦点时候、用遥控器上下左右按键移动到某一个UI图标识货,UI图标必须差异化显示出来,表示选中状态,进而遥控器点击ok 等,实际上就是点击这个UI的操作。

明确焦点状态

确保UI元素有清晰的焦点视觉效果(放大、边框、阴影等)
如上描述,其实就是一个UI组件选中的效果。 这里我理解有三种表现形式

布局实现

比如我们开发中常见的,在获取焦点 state_focused = true ,时候给于不同的背景、颜色 等突出显示出来。

<Buttonandroid:id="@+id/button"android:focusable="true"android:background="@drawable/button_background"/><!-- drawable/button_background.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_focused="true" android:drawable="@drawable/button_focused"/><item android:drawable="@drawable/button_normal"/>
</selector>
硬编码实现

这里举个例子如下,设置UI组件的FocusChange 事件,对获取焦点和失去焦点进行UI不同渲染,达到焦点选中效果,无交点正常显示效果。

 binding.selectOk.setOnFocusChangeListener { v, hasFocus ->val roundView: RoundTextView = v as RoundTextViewif (hasFocus) {roundView.setStrokeWidthColor(5f, Color.parseColor("#EEEE00"))} else {roundView.setStrokeWidthColor(5f, Color.parseColor("#00000000"))}}holder.vb.root.setOnFocusChangeListener { v, hasFocus ->val roundView: RoundConstraintLayout = v as RoundConstraintLayoutif(hasFocus){roundView.setStrokeWidthColor(5f, Color.parseColor("#EEEE00"))focusPos=positionLog.d(TAG," focusPos:${focusPos}")RightAppInfoLiveData.value= RightAppInfo(focusPos,mCenterAppDataList.size)}else{roundView.setStrokeWidthColor(5f, Color.parseColor("#00000000"))Log.d(TAG,"no focusPos:${focusPos}")focusPos=-1RightAppInfoLiveData.value= RightAppInfo(focusPos,mCenterAppDataList.size)}}
引入第三方自定义组件实现

只是作为一个UI组件使用,第三方组件和核心功能就是在获取焦点时候突出显示而已,和 布局表现及 硬编码实现方式并无区别。

焦点顺序

设置合理的焦点移动顺序(android:nextFocusUp/Down/Left/Right),为什么要有这个东西呢? 举例在架构图中,分三页面,无论那个页面都有很多UI组件,如何实现遥控器按 上、下、左、右按键时候,UI组件选中按照自己意愿活着业务定义来切换不同UI选中状态呢?

这个时候,焦点移动顺序就起到作用了,下面列举一下实际用法,其实就是在布局文件中设置的。

作用
  • 实现UI焦点移动顺序
  • 对边角的UI组件,指向自己,这样就可以规避焦点不见了的问题,规避反人类的体验。
    实际使用 简单 如下:

在这里插入图片描述

在这里插入图片描述

初始焦点 requestFocus

为什么会有这个方法, 为什么需要? 比如说 进行界面切换的时候,如架构图中从一个界面切换到另外一个界面、点击一个图标进入另外一个界面。 在新的界面焦点在哪里是不确定的或者说在新的界面,是没有焦点的。 那么最好初始化一个UI具备焦点。 这样遥控器按键时候直接上下左右进行切换,规避没有焦点时候或者焦点不确定时候需要多按好多次 才有UI获取焦点显示出来,体验和业务需要的。

比如,架构图中,第一页切换到第二页、第二页切换到第三页、第三页切换到第二页、第二页切换到第一页,如何实现焦点初始化呢?
在 页面onResume 方法中,让指定的UI初始化一次焦点,去获取焦点一次。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

按键处理

我们为什么需要处理按键,遥控器的本身其实就是KeyEvent 事件,映射的其实就是物理按键的功能。 那么当keyevent 事件通过遥控器触发后,做什么业务逻辑,那就是上层需要处理的事情了。 这就是为什么我们需要按键处理了。
对于很多遥控器,基本功能通用;专用的KeyEvent 是通过定制实现(比如 点击遥控器一个按键就是要直接打开抖音、长按遥控器进行语音控制呀,这些就是定制的功能)

下面代码列举了部分源码,监听左右按键最核心的功能是 需要翻页的功能。 遥控器没有翻页的物理按键,通过当前焦点,是否是边角焦点结合按键监听方向,实现是否翻页、翻到哪一页的功能。

  override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {when (keyCode) {KeyEvent.KEYCODE_DPAD_UP -> {Log.d(TAG," KEYCODE_DPAD_UP")val rootview = window.decorViewval focusView = rootview.findFocus()Log.i(TAG, "===当前获取焦点的View===${focusView}")//return true // 返回true表示事件已被处理val focused = currentFocusfocusView?.let {if(viewPager.currentItem==1&&(focusView.id== R.id.cl_homeleft_second||focusView.id== R.id.cl_homeleft_center||focusView.id== R.id.cl_homeleft_first )){Log.d(TAG,"KEYCODE_DPAD_UP  在vp 第2 页面,但是焦点却在第一页面,那么 request 一次")//homeCenterFragment.binding.clTouping.requestFocus()try{homeCenterFragment.viewBinding?.let { vBing->vBing.clTouping.requestFocus()}}catch (e:Exception){Log.d(TAG," 暂时 未初始化 homeCenterFragment.viewBinding ")e.printStackTrace()}//   viewPager.currentItem=1}}}KeyEvent.KEYCODE_DPAD_DOWN -> {Log.d(TAG," KEYCODE_DPAD_DOWN")val rootview = window.decorViewval focusView = rootview.findFocus()val focused = currentFocusLog.i(TAG, "===当前获取焦点的View===${focusView}")focusView?.let {if(viewPager.currentItem==1&&(focusView.id== R.id.cl_homeleft_second||focusView.id== R.id.cl_homeleft_center||focusView.id== R.id.cl_homeleft_first )){Log.d(TAG,"KEYCODE_DPAD_DOWN 在vp 第2 页面,但是焦点却在第一页面,那么 request 一次")//homeCenterFragment.binding.clTouping.requestFocus()// viewPager.requestFocus()try{homeCenterFragment.viewBinding?.let { vBing->vBing.clTouping.requestFocus()}}catch (e:Exception){Log.d(TAG," 暂时 未初始化 homeCenterFragment.viewBinding ")e.printStackTrace()}//viewPager.currentItem=1}}}KeyEvent.KEYCODE_DPAD_LEFT -> {Log.d(TAG," KEYCODE_DPAD_LEFT")val rootview = window.decorViewval focusView = rootview.findFocus()Log.i(TAG, "===当前获取焦点的View===${focusView}")focusView?.let {if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {if(focusView.id== R.id.cl_clock||focusView.id== R.id.cl_touping||focusView.id== R.id.cl_file){viewPager.currentItem=0}else if(focusView.id==R.id.cl_whiteboard){viewPager.currentItem=1}if(viewPager.currentItem==2){Log.d(TAG,"homeRightFragment.centerAppAdapter.focusPos  focusPos:${homeRightFragment.centerAppAdapter.focusPos}")//  if(homeRightFragment.centerAppAdapter.focusPos%8==0){if((homeRightFragment.centerAppAdapter.focusPos)%8==0){viewPager.currentItem=1}Log.d(TAG," 当前是在 vp currentItem =2 下")try {val rightAppInfo: RightAppInfo? = viewModel.rightAppInfoLiveData.valueLog.d(TAG,"   rightAppInfo:${Gson().toJson(rightAppInfo)}")rightAppInfo?.let { it->if( it.index%8==0){viewPager.currentItem=1}}} catch (e: Exception) {e.printStackTrace()}}} else if(resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {if(focusView.id== R.id.cl_clock||focusView.id== R.id.cl_touping||focusView.id== R.id.cl_bizhi||focusView.id== R.id.cl_googleplay||focusView.id== R.id.cl_file){viewPager.currentItem=0}else if(focusView.id==R.id.cl_whiteboard){viewPager.currentItem=1}if(viewPager.currentItem==2){Log.d(TAG,"homeRightFragment.centerAppAdapter.focusPos  focusPos:${homeRightFragment.centerAppAdapter.focusPos}")//  if(homeRightFragment.centerAppAdapter.focusPos%4==0){if((homeRightFragment.centerAppAdapter.focusPos)%4==0){viewPager.currentItem=1}Log.d(TAG," 当前是在 vp currentItem =2 下")try {val rightAppInfo: RightAppInfo? = viewModel.rightAppInfoLiveData.valueLog.d(TAG,"   rightAppInfo:${Gson().toJson(rightAppInfo)}")rightAppInfo?.let { it->if( it.index%4==0){viewPager.currentItem=1}}} catch (e: Exception) {e.printStackTrace()}}}}}KeyEvent.KEYCODE_DPAD_RIGHT -> {Log.d(TAG," KEYCODE_DPAD_RIGHT  ")val rootview = window.decorViewval focusView = rootview.findFocus()Log.i(TAG, "===当前获取焦点的View===${focusView}")focusView?.let {if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {if(focusView.id== R.id.cl_homeleft_second){viewPager.currentItem=1}else if(focusView.id== R.id.cl_home_listapp||focusView.id== R.id.cl_touping||focusView.id== R.id.cl_music){viewPager.currentItem=2}} else if(resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {if(focusView.id== R.id.cl_homeleft_second||focusView.id== R.id.cl_homeleft_center||focusView.id== R.id.cl_homeleft_first ){viewPager.currentItem=1}else if(focusView.id== R.id.cl_home_listapp||focusView.id== R.id.cl_music||focusView.id== R.id.cl_touping||focusView.id== R.id.cl_home_healsound){viewPager.currentItem=2}}if(viewPager.currentItem==1){Log.d(TAG," 当前是在 vp currentItem =1 下")try {val centerShutIndexInfo: CenterShutIndexInfo? = viewModel.centerShutIndexInfoLiveData.valueLog.d(TAG,"   centerShutIndexInfo:${Gson().toJson(centerShutIndexInfo)}")centerShutIndexInfo?.let { it->if((it.index+1)==it.totalNum){viewPager.currentItem=2}}} catch (e: Exception) {e.printStackTrace()}}}}}return super.onKeyDown(keyCode, event)}

获取当前焦点

在开发遥控器控制过程中,最重要的就是知道当前焦点是哪里,这样才能分析各种不可变的bug,只有找到了焦点的位置,针对性解决焦点问题。

这里监听窗体的焦点事件,在监听KeyEvent 事件响应地方也有相关代码的。

     window.decorView.findFocus()?.let { focusedView ->Log.d(TAG, "decorView 焦点 View 信息: 类名: ${focusedView.javaClass.name}")}

四、实际开发技能分享

假使你已经具备了上面的基础知识,实际在项目项目中还是会被焦点问题搞得焦头烂额、无从下手,遇到问题针对性解决。 这里给出自己的部分经验。

处理焦点注意实现

  • 需要获取焦点UI组件,设置为android:focusable=“true” , 不需要获取焦点的组件设置为 false
  • 给每个界面显示的时候设置初始化焦点,如上 onResume 方法中,给对应的UI组件 requestFocus() 一次
  • 给组件设置holder.vb.root.setOnFocusChangeListener 监听事件,有焦点和无焦点情绪下显示不同效果。
  • 监听onKeyDown 事件,结合自己的软件业务,实现不同的业务需求。
  • 注意在边角的UI组件,RecycleView 的部分情形下,针对UI指定 上下左右焦点,保持焦点不外溢、丢失。
  • 对Banner 类型UI组件,自己根据实际问题来解决,因为Banner 会轮训图片、视频 等导致焦点错乱丢失情况,可以具体问题具体分析
  • RecycleView 对于边角问题处理,对于行位、行首、竖方向收尾、竖方向最后一位的焦点处理。 下面会具体分析。

RecycleView 案例分析

RecycleView 会有两种情况特别注意

  • 比如你的RecycleView 焦点在四周,恰好是左边、右边、上边、下边 需要拦截,如果不拦截的话焦点丢失不见了
  • 需要判断RecycleView 焦点是否在四周哪个方向,做对应的业务逻辑处理。比如网格布局情况下,在最左边情形下需要翻页、在最右要拦截、最下边要拦截。

情形一:网格布局情况下,横屏竖屏显示情况下,判断焦点是否在最左边,如果最左边就用viewPager 翻页处理
在这里插入图片描述
在这里插入图片描述

情形而:如下判断是否边角焦点,处理对应业务,比如底部不让事件传递,焦点定在那里,不然会失去焦点。

   private fun handleLightViewTopBoundary(): Boolean {// 可以转移到其他View或保持焦点val first: View = viewBinding.lightRyView.getChildAt(0)if (first != null) {first.requestFocus()return true}return false}private fun handleLightViewRightBoundary(position: Int): Boolean {// 类似处理右边边界//  val lastPos: Int = viewBinding.lightRyView.getAdapter()!!.getItemCount() - 1val last: View =viewBinding.lightRyView.findViewHolderForAdapterPosition(position)!!.itemViewif (last != null) {last.requestFocus()return true}return false}private fun handleLightViewLeftBoundary(position: Int): Boolean {// 类似处理左边边界// val lastPos: Int = viewBinding.lightRyView.getAdapter()!!.getItemCount() - 1val last: View =viewBinding.lightRyView.findViewHolderForAdapterPosition(position)!!.itemViewif (last != null) {last.requestFocus()return true}return false}private fun handleLightViewBottomBoundary(): Boolean {// 类似处理底部边界val lastPos: Int = viewBinding.lightRyView.getAdapter()!!.getItemCount() - 1val last: View =viewBinding.lightRyView.findViewHolderForAdapterPosition(lastPos)!!.itemViewif (last != null) {last.requestFocus()return true}return false}

如下:如果是底部最后一行了,那么就不让事件传递,这样就不会丢失焦点。 在上下左右焦点都是这么处理的,就是判断 或者 在边焦点时候做其他业务处理。

在这里插入图片描述

总结

  • 遥控器功能开发,本身就是处理焦点的问题,这里简要描述了焦点基本知识、实际开发案例、注意事项。
  • 简单的UI焦点处理事件很简单的,默认就支持,可能需要定制焦点选中UI
  • 对于RecycleView 、banner 嵌套在Fragmetn / Dialog 或者 自嵌套 的复杂UI情形,焦点很容易没有规律,掌握一些基本的处理方案很重要。 遇到问题针对性解决即可。

相关文章:

  • opencv的setDefaultAllocator使用
  • MySQL 数据处理函数全面详解
  • PCB设计实践(三十六)PCB设计新手系统性注意事项总结
  • 自演进多智能体在医疗临床诊疗动态场景中的应用
  • ATX电源
  • 关于Github可连接时长问题的实验
  • html中的盒子标签div标签,有序列表,无序列表
  • Nginx转发中相对路径资源302问题的分析与解决
  • Keepalived+LVS高可用集群
  • 基于双目视觉的厂房车间立体空间匹配算法的研究与实现
  • ResourceDictionary和ResourceDictionary.MergedDictionaries区别
  • 如何从网页源码中批量提取关键信息,一种实用方案
  • Qt信号和槽机制详解
  • 显卡、CUDA、cuDNN及PyTorch-GPU安装使用全指南
  • C++ 对象特性
  • 80Qt窗口_对话框
  • Java-49 深入浅出 Tomcat 手写 Tomcat 实现【02】HttpServlet Request RequestProcessor
  • 持续集成 CI/CD-Jenkins持续集成GitLab项目打包docker镜像推送k8s集群并部署至rancher
  • 【AI Study】第三天,NumPy(4)- 核心功能
  • 每日一篇博客:理解Linux动静态库
  • 佛山企业网站推广/品牌传播推广方案
  • 公众号链接网站都是怎么做的/google adwords关键词工具
  • 酒庄企业网站/搜狗seo优化
  • 网上购物网站设计/2021年热门关键词
  • 网站语言切换前端可以做么/搜索引擎优化的方法与技巧
  • 北京网站优化专家/百度app打开