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情形,焦点很容易没有规律,掌握一些基本的处理方案很重要。 遇到问题针对性解决即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/85331.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/85331.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《HTTP权威指南》 第3章 HTTP报文

报文是如何流动的 HTTP报文是在HTTP程序之间发送的数据块。数据块以一些文本形式的元信息开头。 报文方向有&#xff1a;流入、流出、上游、下游。 流入和流出描述事务处理的方向&#xff0c;流入和流出是基于服务器的描述。 流入&#xff1a;客户端发往服务器的请求报文 流…

Kafka 集群架构与高可用方案设计(二)

Kafka 集群架构与高可用方案的优化策略 合理配置参数 在 Kafka 集群的配置中&#xff0c;参数的合理设置对于系统的高可用性和性能表现起着关键作用。例如&#xff0c;min.insync.replicas参数定义了 ISR&#xff08;In-Sync Replicas&#xff0c;同步副本&#xff09;集合中…

47-Oracle ASH报告解读

上一期生成了ASH报告后&#xff0c;就需要解读报告关键信息。ASH的使用可以快速定位瞬时性能问题。生产环境的场景时间紧、任务重&#xff0c;但是必须要结合具体业务分析&#xff0c;同时借助其他工具做报告做趋势分析。 一、ASH 技术原理​ ​1. 核心机制​ ​采样原理​&a…

“本地化思维+模块化体验”:一款轻量数据中心监控系统的真实测评

“本地化思维模块化体验”&#xff1a;一款轻量数据中心监控系统的真实测评 在数据中心运维逐步精细化的今天&#xff0c;一款真正贴合本地用户习惯、设计有温度的系统并不多见。近期体验了一款功能全面、逻辑清晰的监控平台&#xff0c;给人留下了深刻印象。并不是广。今天就从…

词编码模型有哪些

词编码模型有哪些 词编码模型在高维向量空间的关系解析与实例说明 如Word2Vec、BERT、Qwen等 一、高维向量空间的基础概念 词编码模型(如Word2Vec、BERT、Qwen等)的核心是将自然语言符号映射为稠密的高维向量,使语义相近的词汇在向量空间中位置接近。以Qwen模型为例,其…

elementui el-select 获取value和label 以及 对象的方法

获取 el-select 的 value 和 label 值 在 Element UI 的 el-select 组件中&#xff0c;可以通过以下方法获取选项的 value 和 label 值。 1、绑定 v-model 获取 value el-select 通常通过 v-model 绑定 value 值&#xff0c;直接访问绑定的变量即可获取当前选中的 value。…

树莓派与嵌入式系统实验报告

一、Linux 系统编译工具链实践&#xff1a;mininim 源码编译 虚拟机 Ubuntu 编译流程 环境配置问题 编译时遇到虚拟机无法联网的情况&#xff0c;通过连接个人热点解决&#xff08;校园网限制导致无法访问外部资源&#xff09;。 执行 ./bootstrap 时报错 gnulib-tool: command…

IDEA部署redis测试

新建springboot&#xff0c;项目改为&#xff1a;testredis E:\ideaproject\testredis\src\main\java\org\example\testredis\TestredisApplication.java 代码为&#xff1a; package org.example.testredis;import org.springframework.boot.SpringApplication; import org.…

旅游服务礼仪实训室:从历史演进到未来创新的实践探索

一、旅游服务礼仪实训室的历史演进&#xff1a;从礼制规范到职业化培养 旅游服务礼仪实训室的建设并非一蹴而就&#xff0c;其发展历程与人类对礼仪认知的深化及职业教育体系的完善密切相关。 1. 古代礼仪教育的萌芽 礼仪作为社会行为规范&#xff0c;最早可追溯至中国夏商周…

Could not find a declaration file for module ‘..XX‘.

1. 添加 Vue 声明文件 如果您还没有为 .vue 文件创建类型声明&#xff0c;可以通过创建一个新的类型声明文件来解决该问题。 步骤&#xff1a; 在您的项目根目录下创建一个名为 shims-vue.d.ts 的文件&#xff08;您可以选择其他名称&#xff0c;但建议使用常见名称以便于识…

OpenCV CUDA模块设备层-----反正切(arctangent)函数atan()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 对输入的 uchar1 像素值&#xff08;范围 [0, 255]&#xff09;&#xff0c;先归一化到 [0.0, 1.0] 浮点区间&#xff0c;然后计算其反正切值 at…

java中常见的排序算法设计介绍

排序算法 复杂度原地排序冒泡排序算法逻辑时间复杂度&#xff1a;最好O(n)&#xff0c;最坏和平均O(n^2)冒泡排序:稳定性算法 选择排序算法逻辑时间复杂度&#xff1a;最好&#xff0c;最坏和平均都是O(n^2)选择排序:不稳定性算法 插入排序算法逻辑时间复杂度&#xff1a;最好O…

深度学习系列81:MCP快速上手

MCP 是一种开放协议&#xff0c;通过标准化的服务器实现&#xff0c;使 AI 模型能够安全地与本地和远程资源进行交互。MCP 可帮助你在 LLM 之上构建智能代理和复杂的工作流。MCP 采用客户端-服务器架构&#xff0c;主机应用程序可以连接到多个服务器。 这里用个demo展示一下如何…

【Python机器学习(一)】NumPy/Pandas手搓决策树+使用Graphviz可视化(以西瓜书数据集为例)

下题来源于笔者学校的《模式识别与机器学习》课程的作业题,本文将通过使用NumPy处理数学运算,Pandas处理数据集,Graphviz实现决策树可视化等Python库来实现决策树算法及其格式化。 导入用到的Python库: import numpy as np import pandas as pd from graphviz import Digr…

react-activation 组件级缓存解决方案

文章目录 一、KeepAlive 组件二、AliveScope 容器三、useAliveController Hook四、生命周期五、完整示例 react-activation 主要解决 React 项目中的「页面缓存」需求(是第三方库&#xff0c;非React 官方)&#xff0c;类似于 Vue 中的 <KeepAlive>&#xff1a; 功能说明…

CentOS 7内核升级方案

关于升级 CentOS 7 系统内核至 4.19 版本的可执行升级方案,可根据实际情况进行调整和完善,希望能对大家有所帮助: 一、升级背景与目的 随着业务的发展和系统稳定性的要求,当前 CentOS 7 系统所使用的内核版本 3.10.0-1160.el7.x86_64 已经无法满足部分新功能需求以及面临…

树莓派实验实践记录与技术分析

一、内核驱动开发&#xff1a;hello 模块实现 驱动程序代码 #include <linux/init.h> #include <linux/module.h> static int __init hello_init(void) { printk(KERN_INFO "hello kernel\n"); return 0; } module_init(hello_init); static void …

【秦九绍算法】小红的 gcd

题目 牛客网&#xff1a;小红的 gcd 题目分析 我们知道&#xff0c;求gcd就用欧几里得算法&#xff08;辗转相除法&#xff09;&#xff1a;gcd(a,b)gcd(b,a mod b)。但是这题的a非常大&#xff0c;最大是一个1e6位数&#xff0c;无法使用任何数据类型存储。如果使用高精度…

AWS服务监控之EC2内存监控

首先在IAM里找到角色&#xff0c;创建角色&#xff0c;选择EC2 然后在被监控的机器上安装cloudwatch-agent 官方链接在本地服务器上安装 CloudWatch 代理 - Amazon CloudWatch wget https://s3.amazonaws.com/amazoncloudwatch-agent/redhat/amd64/latest/amazon-cloudwatch-a…

鸿蒙 ArkWeb 和 H5混编开发

ArkWeb Web 相关标准技术(HTML/CSS/JS)&#xff0c;是业内支持性最广泛的技术&#xff0c;可以在最广泛的平台下实现“一次编写到处运行”&#xff1b;大部分对性能无需极致要求的应用页面&#xff0c;都可以使用 Web 技术来实现。 鸿蒙 ArkWeb Kit&#xff08;方舟 Web&…