Android TV开发要点

Android电视应用开发与Android手机应用开发的区别之一就是Android手机是触屏交互而电视是遥控器按键交互。手机应用开发交互关注于MotionEvent的分发机制,而TV开发需要转而关注KeyEvent。KeyEvent与MotionEvent都是InputEvent的子类,它们的分发逻辑相似。

Keycode

Android Keycode(按键代码)是一种描述在安卓设备上触摸屏幕或按下物理按键的操作。在 Android 中, KeyEvent 就是用来描述按键操作的类。KeyEvent 类会保存一个按键动作(如:ACTION_DOWN, ACTION_MULTIPLE, ACTION_UP)和按键码(键值)。按下每个不同的键都会产生不同的KeyCode:

keycode-img.png

有时候没有实体按键(比如电脑没有返回键等),可以直接使用 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不同的是: 1FOCUS_AFTER_DESCENDANTS它可以优先让下层View请求焦点,失败后再自己请求 2FOCUS_BEFORE_DESCENDANTS:可以优先于下层View请求焦点,失败后再下层View请求 3FOCUS_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按键转换原理


image.png

遥控器配置文件获取

adb shell
dumpsys input

然后搜索KeyLayoutFile

keyboard2.png
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

image.png

可以在终端看到遥控器的按键事件,但是不易辨认,使用 getevent -l 。


image.png

格式为 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容