Android TV 开发一览

9,612 阅读8分钟

最近在做基于Android 的TV开发,刚开始还是有丢丢怕的, 从来没接触过这个。大佬说不要慌,和手机开发基本一样......

看了一些文章,发现确实是基本一样的。 最大的区别在于,两者和用户的交互方式不同。手机通过和用户点击触摸屏给与反馈,而TV通过用户点击遥控器给与反馈。

TV开发相对手机开发而言,最大的区别是响应遥控器的按钮点击, 处理View的焦点跳转。

目前国内TV开发环境, 都是创建一个Android项目,而不是一个Android TV项目。 可以认为, 国内的TV APP,就是一个横版长屏幕的APP, 外加处理了一些焦点问题而已......

phone.png

下面来一起详细看看TV开发和手机开发的区别:

1、连接

TV开发不像手机开发, 通过USB线连接进行调试。 可以在电视的网络设置中找到电视的IP地址,通过以下adb命令进行连接, 连接成功后即可在AS中操作电视设备。

// 连接电视
adb connect 170.2.10.20  

// 断开连接
adb disconnect 170.2.10.20

由于我这边的电视系统也是开发版的系统,所以可以直接连接。如果连接不上的话, 可能要想办法开启电视的开发者选项授权.....

2、键盘输入

和手机的输入方式相比,可以说复杂了很多。电视不是触屏的,每一个字符度需要操作遥控器,通过上下左右找到字符,点击确认输入。

如果发现之前的某个字符输错, 又要返回去删除, 简直是噩梦。

部分遥控器已经有了红外操作装置, 输入字符类似于鼠标点击键盘, 但是相比手机触屏,依旧复杂.......

通过下面的adb命令可以快速将字符串输入到电视的输入框中

adb shell input text "hello,world"

妈妈再也不担心我没耐心砸电视了.....

3、焦点控制

电视的按钮状态,相比手机要稍微复杂一些。

用户使用手机APP是可以随处点击,没有限制。 有点击事件的,没有点击事件的,都想点点试试。

电视用户的话,需要限制用户那块可以点击,那块不可以,这就需要用遥控器的上下左右跳转来限制View能否或得焦点。并需要时时刻刻需要告诉用户目前的焦点处于什么位置,方便进行接下来的操作。

1、设置可获取焦点

布局文件中

android:focusable="true"

代码中

view.setFocusable(true);

2、设置触摸获取焦点

布局文件中

 android:focusableInTouchMode="true"

代码中

view.setFocusableInTouchMode(true);

2、View焦点监听

view.setOnFocusChangeListener(new View.OnFocusChangeListener() {
    @Override
    public void onFocusChange(View v, boolean hasFocus) {
        if (hasFocus) {
            // 获取焦点时操作,常见的有放大、加边框等
        } else {
            // 失去焦点时操作,恢复默认状态
        }
    }
});

3、View获取焦点时, 设置下一个获取焦点的View

布局文件中:

 android:nextFocusDown="@id/button1"
 android:nextFocusUp="@id/button2"
 android:nextFocusLeft="@id/button3"
 android:nextFocusRight="@id/button4"

代码中:

 view.setNextFocusDownId(R.id.button1);
 view.setNextFocusUpId(R.id.button2);
 view.setNextFocusLeftId(R.id.button3);
 view.setNextFocusRightId(R.id.button4);

4、确定焦点的位置

TV开发过程中,最头疼的就是遥控器按着按着就不知道焦点去哪了。 明明所有的的View都限制了能否获取焦点,以及获取焦点的状态。 还是会出现按着按着就不知道焦点去哪了。这个在复杂的自定义View中容易出现。

这个时候就要相办法定位到焦点躲到哪里去了......


        ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver();
        observer.addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
            @Override
            public void onGlobalFocusChanged(View oldFocus, View newFocus) {
                VLog.d(TAG, "oldFocus:    " + oldFocus + "/n" + "newFocus:    " + newFocus);
            }
        });

设置Window的全局焦点监听,将失去焦点和获得焦点的View打印出来。

5、按键监听

UI可能天马行空的想给某个View的按下操作加个动画, 这个时候就要监听遥控器的按下操作,并开启动画了。

如何监听那?

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;
        }
    });

常用的遥控器按键:

KeyEvent.KEYCODE_BACK // 返回键
KeyEvent.KEYCODE_DPAD_DOWN // 下键
KeyEvent.KEYCODE_DPAD_UP // 上键
KeyEvent.KEYCODE_DPAD_LEFT // 左键
KeyEvent.KEYCODE_DPAD_RIGHT // 右键
KeyEvent.KEYCODE_MENU // 菜单键
KeyEvent.KEYCODE_SETTINGS // 设置键

跟手机开发一样,HOME键监听不到

6、descendantFocusability属性

在复杂的自定义View中, 只有外层的父View能获取到焦点, 子View无论如何也获取不到焦点。
如何让子View也能获取到焦点那?descendantFocusability属性可以帮忙搞定

官方的定义是这样子的:

        <!-- Defines the relationship between the ViewGroup and its descendants
             when looking for a View to take focus. -->
        <attr name="descendantFocusability">
            <!-- The ViewGroup will get focus before any of its descendants. -->
            <enum name="beforeDescendants" value="0" />
            <!-- The ViewGroup will get focus only if none of its descendants want it. -->
            <enum name="afterDescendants" value="1" />
            <!-- The ViewGroup will block its descendants from receiving focus. -->
            <enum name="blocksDescendants" value="2" />
        </attr>

descendantFocusability是View的一个属性。通过这个属性可以指定viewGroup和其子View到底谁获取焦点, 直接在viewGroup的 xml的布局上使用就行。

android:descendantFocusability="afterDescendants"

三种属性值分别为:

  • beforeDescendants :viewGroup会优先其子类控件而获取到焦点
  • afterDescendants :viewGroup只有当其子类控件不需要获取焦点时才获取焦点
  • blocksDescendants :viewGroup会覆盖子类控件而直接获得焦点

5、UI状态

为了方便用户的操作,更好的提示用户。按钮有焦点态, 按下态,点击态等多种状态,这些可能度需要处理。类似于下图这种

企业微信截图_2b0de903-d875-4de1-90d0-60a248adf2b7.png

像这种其实也是比较简单的,用一个SelectDrawable就可以解决

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/bg_focus" android:state_focused="true" />
    <item android:drawable="@drawable/bg_press" android:state_pressed="true" />
    <item android:drawable="@drawable/bg_select" android:state_selected="true" />
    <item android:drawable="@drawable/bg_normal" />
</selector>

6、模拟器模拟电视分辨率

如果电视的屏幕特别大的话, 对UI的开发也是一种挑战。
现在的电视动辄就五十多英寸起步,太大了。 看个UI效果都要退后离电视三五米。这样开发谁受得了。
这个时候就需要模拟器来帮你了, 用模拟器模拟电视的分辨率, 可以在本地的模拟器上直接看效果。 不用再离电视三五米的距离了......

我用的MuMu模拟器设置分辨率, 其他第三方模拟器应该也是支持的

企业微信截图_2d9f8f7f-3a45-486d-8a59-93b9103d7d1c.png

7、Chrome插件模拟遥控器点击

既然使用模拟器来模拟TV, 那就还缺一样东西。 PC的模拟器虽然不是触控的, 需要用鼠标操作。 和电视的遥控器有一定区别。

那有没有一种工具, 像遥控器一样操作模拟器来模拟上下左右的按键嘛?答案是有的。

Chrome插件,ChromeADB可以模拟遥控器对设备的操作 chrome_adb.png

下载地址:chrome.google.com/webstore/de…

不能科学上网的同学,可以去GitHub下载进行离线安装:github.com/importre/ch…

安装完成后, 连接模拟器。右边的Keyboard就可以当做遥控器来操作设备了。 chrome_adb_controller.png

8、实战之RecyclerView的焦点问题处理

在开发Android TV应用时, 使用遥控器控制RecyclerView的焦点,向用户展示当前选中的是那个item。会遇到几个头疼的问题:

  • 设置Item获得焦点时的效果
  • RecyclerView第一次获得焦点,默认选中第一项
  • RecyclerView重新获得焦点后,选中上次的item
  • RecyclerView失去焦点后,继续保持item的选中效果

9.1 设置Item获得焦点时的效果

和单个View一样, 给Item设置SelectDrawable即可。

9.2 RecyclerView第一次获得焦点,默认选中第一项

由于Android系统的焦点跳转规则是就近跳转,可能某个离RecyclerView比较近的View,在跳转时, 跳转到了离它比较近的,RecyclerView内部的某个ItemView,而不是RecyclerView内部的第一个ItemView。这显然不符合我们的要求。

那么如何才能让我们在RecyclerView第一次获得焦点时,选中第一项那?

答案是使用: HorizontalGridView或者VerticalGridView。这两个View是leanback仓库里面的两个类, 都是继承自BaseGridView,而BaseGridView继承自RecyclerView。HorizontalGridView是处理横向RecyclerView的焦点问题, VerticalGridView是处理竖向的。

添加依赖:

implementation "androidx.leanback:leanback:1.0.0"

使用

 <androidx.leanback.widget.HorizontalGridView
        android:id="@+id/rvHead"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:nextFocusLeft="@id/rvHead"
        android:nextFocusRight="@id/rvHead"/>

因为HorizontalGridView extends BaseGridView extends RecyclerView,所以之前使用RecyclerView的代码基本不用改变,并且不用调用setLayoutManager。(千万不调用setLayoutManager,调用的话会不生效。BaseGridView的焦点控制完全是由内部设置的LayoutManager来生效的)

9.3 RecyclerView重新获得焦点后,选中上次的item

类似于这样的效果: image

在不做任何处理的情况下,RecyclerView重新获得焦点也是按照最近原则来获得焦点的,而不是上次选中的View获得焦点。

要想让RecyclerView重新获得焦点后,选中上次的item。 使用HorizontalGridView或者VerticalGridView即可。

9.4 RecyclerView失去焦点后,继续保持item的选中效果

我这边想到两种处理方式

1、同一时刻,有且只能有一个View保持选中状态。当一个View选中时, 之前选中的View取消选中。 那就保存上一次选中的View,当有新的View选中时, 上一次选中的View取消选中,新的View失去焦点时, 将其更新为上次选中的View。

            mRootView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
                @Override
                public void onFocusChange(View v, boolean hasFocus) {
                    if (hasFocus) {
                        v.setSelected(true);
                        if (mLastFocusView!= null) {
                            mLastFocusView.setSelected(false);
                        }
                    } else {
                        mLastFocusView = v;
                    }
                }
            });

2、既然每次只能有一个ItemView处于选中状态,那就拿到被选中ItemView 的positon, 遍历RecyclerView的所有ItemView, 只要不是被选中的positon, 均不让其处于选中状态。

第二种方法比较暴力,推荐使用第一种方法。

9、LeanBack项目

仓库地址: github.com/android/tv-…

贴一段官方的解释装逼....

This sample is a Videos By Google app, designed to run on an Android TV device, which demonstrates how to use the Leanback Support library which enables you to easily develop beautiful Android TV apps with a user-friendly UI that complies with the UX guidelines of Android TV.

这个项目是Google官方提供,面向TV设备的一个视频APP。主要目的是教你使用Leanback库,轻松的上手开发对用户友好,规范的Android TV APP。

这个库通过一些界面的实现方式, 帮助开发者快速实现TV的开发。下面是这个库一些界面的截图。 image

感兴趣的同学可以下载到本地,体验一下。

10、推荐

Google TV开发指南: developer.android.com/training/tv…

Android TV--RecyclerView中item焦点实战:juejin.cn/post/687811…

LeanbackShowcase:github.com/android/tv-…

tv-samples: github.com/android/tv-…