美文网首页
Android TV开发要点

Android TV开发要点

作者: 神迹12 | 来源:发表于2023-11-29 17:14 被阅读0次

    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

    相关文章

      网友评论

          本文标题:Android TV开发要点

          本文链接:https://www.haomeiwen.com/subject/gnjfgdtx.html