Android电视应用开发与Android手机应用开发的区别之一就是Android手机是触屏交互而电视是遥控器按键交互。手机应用开发交互关注于MotionEvent的分发机制,而TV开发需要转而关注KeyEvent。KeyEvent与MotionEvent都是InputEvent的子类,它们的分发逻辑相似。
Keycode
Android Keycode(按键代码)是一种描述在安卓设备上触摸屏幕或按下物理按键的操作。在 Android 中, KeyEvent 就是用来描述按键操作的类。KeyEvent 类会保存一个按键动作(如:ACTION_DOWN, ACTION_MULTIPLE, ACTION_UP)和按键码(键值)。按下每个不同的键都会产生不同的KeyCode:
有时候没有实体按键(比如电脑没有返回键等),可以直接使用 adb 命令控制。
adb shell input keyevent keyCode
通过adb快速输入文字到文本输入框中
adb shell input text "hello,world"
焦点
手机与TV开发的另一个巨大的区别就是焦点。KeyEvent事件最终是要体现在具体的View,只有获取了焦点的View才可以处理KeyEvent。
View用自己的int型的成员变量mPrivateFlags中的第2个bit定义自己是不是有焦点的状态,如果View有焦点这个bit是1,没有是0;同时ViewGroup用成员变量mFocused表示自己的子View中哪个有焦点或者包含焦点View。
焦点模式
触摸模式(TouchMode)与普通模式。在xml中的配置区别:
android:focusableInTouchMode="true" 请求有触摸获取焦点的能力
android:focusable="true" 请求有普通获取焦点的能力(物理键盘)
这两个属性都是表示是否可以获取焦点,focusableInTouchMode是针对触屏的。 android:focusable是针对有物理键下操作的。
如果这个View在xml文件中设置了clickable属性为true那这个View就可以获得焦点,也就可以响应按键事件,如果既没有设置clickable也没有设置focusable为true,那这个View就获取不到焦点。
ViewGroup的descendantFocusability属性
对于焦点请求,ViewGroup与View不同的是: 1)FOCUS_AFTER_DESCENDANTS:它可以优先让下层View请求焦点,失败后再自己请求 2)FOCUS_BEFORE_DESCENDANTS:可以优先于下层View请求焦点,失败后再下层View请求 3)FOCUS_BLOCK_DESCENDANTS:可以屏蔽下层View请求焦点
在xml中可以通过设置descendantFocusability属性来控制ViewGroup的焦点模式,ViewGroup默认在所有子View之前处理焦点。
FocusOutFront与FocusOutEnd
如果标题栏使用 HorizontalGridView 实现,内容区域使用ViewPager +Fragment ,Fragment里是 VerticalGridView ,可能出现标题栏和内容区焦点切换不成功的问题。例如焦点不能从内容区切到标题栏这样的情况。这时使用 focusOutFront 和 focusOutEnd 属性能够解决问题,解决不同容器里焦点切换不成功的问题。
同类型的属性还有focusOutEnd, focusOutSideStart,focusOutSideEnd,说明如下:
属性 | 说明 |
---|---|
focusOutFront | 允许DPAD键在视图的前面向外导航(当位置=0时),默认值为false |
focusOutEnd | 允许DPAD键在视图末尾向外导航,默认值为false |
focusOutSideStart | 允许DPAD键导航出第一行,对于HorizontalGridView,它是顶部边缘,对于VerticalGridView,这是“开始”边缘。默认值为true |
focusOutSideEnd | 允许DPAD键导航出最后一行,对于HorizontalGridView,它是底部边缘,对于VerticalGridView,这是“结束”边缘。默认值为true |
全局焦点变化监听
window.decorView.viewTreeObserver
.addOnGlobalFocusChangeListener { oldFocus, newFocus ->
(newFocus ?: window.decorView.findFocus())?.let {
mainUpView.setFocusView(newFocus, oldFocus, 1.0f)
}
}
表示当视图树中的焦点状态更改时回调,oldFocus 和 newFocus 都有可能为 null。
按键事件分发
当产生了一个KeyEvent,ViewRootImpl就根据mFocused和mPrivateFlags从View树中找出这个焦点View,并把KeyEvent给它处理。
Android TV通过 dispatchKeyEvent 分发事件,返回 false 或者 true 截断本类和子类中的事件分发,截取某个按键可返回 false 和 ture 都能截断这个按键向下分发,对上层没有影响。
ViewGroup.java
{
...
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 1);
}
//自己有焦点则自己处理
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
if (super.dispatchKeyEvent(event)) {
return true;
}
//有获得焦点的子view,则由该子view处理按键事件
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
...
}
当KeyEvent 事件分到具体的子 View 的 dispatchEvent() 里时,View 回去先去看有没有设置 OnKeyListener。有则回调 OnKeyListener.onKey()方法来处理事件。
焦点移动
当我们按下按键的时候会发现如果我们不拦截按键事件,按键事件就会转换成焦点View的切换,现在就开始分析这个转换的过程。
由ViewRootImpl的processKeyEvent进行处理:
1、将按键事件的上下左右转换成焦点移动方向的上下左右 2、找出View树中有焦点的View 3、调用焦点View的focusSearch方法寻找下一个获得焦点的View 4、调用下一个获得焦点的View的requestFocus方法,让它获得焦点
通过前面提到的View的成员变量mPrivateFlags中的第2个bit,以及ViewGroup的成员变量mFocused来判断查找所有有焦点的View。
寻找下一个获得焦点的View,不管是View还是ViewGroup会调用focusSearch()方法,焦点会逐级的交给父ViewGroup的focusSearch方法处理,直到顶层布局。最终Android框架焦点的查找都是通过FocusFinder的findNextFocus()方法去寻找,获取到下一个获得焦点的View。
public class FocusFinder {
private static final ThreadLocal<FocusFinder> tlFocusFinder =
new ThreadLocal<FocusFinder>() {
@Override
protected FocusFinder initialValue() {
return new FocusFinder();
}
};
/**
* Get the focus finder for this thread.
*/
public static FocusFinder getInstance() {
return tlFocusFinder.get();
}
...
}
public class ThreadLocal<T> {
...
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
...
}
同Handler中的Looper一样,每个线程只有唯一的一个FocusFinder。
FocusFinder会将所有View树中所有可能获得焦点的View加到列表中,然后再根据定义的规则去寻找最合适(指定方向上与当前focused view距离最近)的View。
public class FocusFinder {
...
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
if (focused != null) {
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
}
if (next != null) {
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
//
effectiveRoot.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
//从focusable中找到最近的一个
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
...
}
按键监听
view.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
// 这种情况就是当按下遥控器返回键时
return true;
}
return false;
}
});
需要注意的是HOME键监听不到,HOME键需要通过广播进行监听。
Leanback
leanback是google官方开发的支持 Android TV 独有的功能TV 专用库。
这些库包括:
Leanback 库提供界面模板,可简化 Android TV 应用的创建流程。
Leanback Preferences 库提供与平台一致的偏好设置和设置界面,但可以设置与您的应用相符的主题。
Leanback Paging 库支持
ObjectAdapters
的 AndroidX 分页模型,该模型通常与 Leanback 模板一起使用。Leanback Tabs 库支持 Android TV 上的标签页式导航。
谷歌官方TV 开发Demo:https://github.com/android/tv-samples
Leanback中针对TV端做的列表封装包括:HorizontalGridView、VerticalGridView。HorizontalGridView和VerticalGridView都继承自RecyclerView,针对TV的特性,在item排版、焦点流转、上/失焦动画、记住焦点、焦点item对齐位置等方面做了比较好的封装。使用上这几个概念需要理解:
ArrayObjectAdapter:作用类似于 List,可以用于装每一行的数据,也可以用于装一行里的每一个 item 数据。
ListRowPresenter:Leanback 库中的 Presenter 作用都有些类似于 RecyclerView.Adapter,负责数据绑定,UI呈现。
ListRow:可以理解成一个 Mode,也就是把每一行抽象封装成一个 ListRow。
PresenterSelector:根据不同的数据类型选择不同的Presenter,用于多item type列表模型
ItemBridgeAdapter:HorizontalGridView和ObjectAdapter的桥梁,用于解耦双方
FocusHighlightHelper:独立的上焦动画帮助类,内置了两种上焦动画,可以在item选中后进行放大。
按键映射
android按键转换原理
遥控器配置文件获取
adb shell
dumpsys input
然后搜索KeyLayoutFile
adb pull /vendor/usr/keylayout/Generic.kl /Users/hello/Downloads/keyboard
导出后查看设备所受用的kl文件。
key 102 MOVE_HOME
key 103 DPAD_UP
key 104 PAGE_UP
key 105 DPAD_LEFT
key 106 DPAD_RIGHT
key 107 MOVE_END
key 108 DPAD_DOWN
key 109 PAGE_DOWN
key 110 INSERT
key 111 FORWARD_DEL
key后面的数字就是kernel上报的按键码,后面的字符标签就是该按键码对应的android中的按键标签,当用户按下按键后,kernel会上报对应按键的按键码,然后上层根据正确的kl文件中的对应关系,将按键对应到上层的按键标签上来。
通过修改kl文件来修改遥控器键位映射。
getevent/sendevent
getevent: 旨在获取android设备的事件信息,具体参考详细用法(本人亦初学者一枚,无法深入解释)
sendevent: 则可以向设备发送模拟事件,其中包括touch和keypress
sendevent /dev/input/event1 0003 0057 00000000
adb shell
然后输入:
getevent
可以在终端看到遥控器的按键事件,但是不易辨认,使用 getevent -l 。
格式为 device: type code value,即 设备、输入设备类型、按键扫描码、附加码,具体定义可从kernel/include/linux/input.h中获得。
type: 输入设备类型,在手机系统中经常使用的键盘(keyboard)和小键盘(kaypad)属于按键设
备EV_KEY,轨迹球属于相对设备EV_REL,触摸屏属于绝对设备EV_ABS
参考
https://www.jianshu.com/nb/39620330
https://www.python100.com/html/98657.html
https://blog.csdn.net/Z_Dong_/article/details/130061274
https://zhuanlan.zhihu.com/p/34486860?utm_id=0
https://developer.android.google.cn/training/tv/start/libraries?hl=zh-cn
https://blog.csdn.net/weixin_42484608/article/details/91554814
https://www.imooc.com/article/74970/
https://zhuanlan.zhihu.com/p/623207925?utm_id=0 //Chrome插件模拟遥控器
https://zhuanlan.zhihu.com/p/479662451?utm_id=0
https://www.jianshu.com/p/5a4b97d9b963?ivk_sa=1024320u
https://icode.best/i/88550535743279 //adb与遥控器按键相关的指令
https://blog.csdn.net/baidu_37503452/article/details/130335853