Android TV屏 开发、RecyclerView焦点处理等
TV屏使用遥控器控制,通过焦点操作界面,就跟电视投屏类似
一共两个核心,焦点的处理,按键的监听处理
按键原生提供了onKeyDown 来监听,通过不同的 keyCode 区分不同的按键
一般如果没有遥控器,可以通过电脑键盘测试,使用投屏软件投屏后,对键盘按键效果跟遥控器类似
有时候没有实体按键(比如电脑没有返回键等),可以直接使用 adb 命令控制
adb shell input keyevent keyCode
至于长按事件,通过监听的 KeyEvent 参数中 repeatCount 判断,这里最好等于某个数字时触发,防止多次重复触发
主动获取焦点使用方法 requestFocus(),但是可能会失败,所以需要注意等待UI刷新完后在调用
并且在需要获取焦点的view中设置属性,否则没法落焦
android:focusable="true"
android:focusableInTouchMode="true"
如果实在没法控制刷新后的时机,那就只能延迟(postDelayed)获取了,这个万能钥匙,但是尽量少用,影响效率,减低体验
对于焦点,上面的只是基本操作,实际开发中吭比较多,比较如果界面复杂,焦点是很不好控制的,加上列表各种刷新,跨界面恢复焦点等,懂得都懂
所以为了更好的定位问题,需要借助一些系统监听来获取焦点的状态,才能知道问题在哪
比如可以重写 requestChildFocus 方法,每次获取焦点时打印信息,看看是否被其它 view 抢占焦点
override fun requestChildFocus(child: View?, focused: View?) { child?.let { val position = getChildViewHolder(child).absoluteAdapterPosition Logger.d("requestChildFocus $position ") } }
或者使用全局监听 decorView.viewTreeObserver.addOnGlobalFocusChangeListener ,判断哪些 view 获取了焦点
针对获取焦点无效怎么处理?
估计很多时候会发现,调用了 requestFocus 方法没反应,这是因为没有对上个 view 的焦点进行 clear
你需要在监听中,把获取焦点的 view 赋值给你定义的变量 lastFocusView,然后每次调用 requestFocus 前先调用 lastFocusView.clearFocus()
当 tab 或者 列表 切换界面,这个时候操作遥控器子页面自动抢占了焦点,怎么处理?
可以通过对 onKeyDown 事件的拦截(return true),这样子页面是无法响应按键事件的,也就无法获取焦点
针对一些个别 view 可以设置屏蔽焦点的属性
android:focusable="false"
android:focusableInTouchMode="false"
焦点边框样式统一处理
可以通过全局监听 addOnGlobalFocusChangeListener ,对 view 进行统一绘制边框,或者一些逻辑控制等
window.decorView.viewTreeObserver .addOnGlobalFocusChangeListener { oldFocus, newFocus -> (newFocus ?: window.decorView.findFocus())?.let { mainUpView.setFocusView(newFocus, oldFocus, 1.0f) } }
弹框无法获取焦点怎么处理?
一般 dialog 等只要设置了 focus 属性,然后在初始化调用 requestFocus,是没问题的,但是不排除一些个别情况
比如 PopupWindow,因为 window 弹出后,activity 的 onKeyDown 会无法响应,所以需要单独监听,前提是获取到焦点
如果碰到焦点不好处理,或者落焦后绘制边框等不方便,这里建议手动控制,因为落焦绘制是统一在全局监听里处理的,window 上需要额外监听,没有对这块逻辑封装好,就表示需要把整套逻辑搬到 window 上处理,这显然很冗余
所以何不直接监听 onKeyDown ,然后通过代码手动绘制边框焦点
contentView.apply { requestFocus() binding.flMove.setOnKeyListener { _, keyCode, _ -> Logger.d("setOnKeyListener keyCode $keyCode") when (keyCode) { KeyCode.KEYCODE_DPAD_LEFT -> { switchMenu(1) } KeyCode.KEYCODE_DPAD_RIGHT -> { switchMenu(0) } KeyCode.KEYCODE_BACK -> { dismiss() } } false } }
如果不太喜欢这个方案,还有另外一个方案
可以在xml中添加布局,去仿造弹框,这样焦点就可以统一处理,不过需要对这个布局进行一些显示隐藏的操作