前阵子将一个手机APP改为TV应用,由于首次开发TV,故把开发过程中的一些问题记录下来,以备不时之需。电视应用和手机应用开发过程大同小异,电视应用主要注意三个地方:1是清单文件,2是布局文件,3是处理好控件获取焦点时的背景显示,因为对于没有触控功能的电视设备,用户想要点击某个控件时,只能先操作遥控器的方向键将焦点移到该控件上,接着才能按遥控器的确定键执行点击,所以就需要处理好控件获取焦点时的背景显示(区别于没有获取焦点的其他控件),以便用户能一眼看出来现在是哪个控件在获取焦点并可点击。

1.清单文件需要改的地方

1.1 处理电视可能不支持的硬件功能

电视上的android系统一般不支持以下硬件功能:

android电视应用开发 电视应用开发 教程_android


因此,需要在清单文件中将以上特性声明为非必须的,你的应用才能安装在不支持这些特性的电视上,如下:

<uses-feature android:name="android.hardware.touchscreen"
            android:required="false"/>
    <uses-feature android:name="android.hardware.faketouch"
            android:required="false"/>
    <uses-feature android:name="android.hardware.telephony"
            android:required="false"/>
    <uses-feature android:name="android.hardware.camera"
            android:required="false"/>
    <uses-feature android:name="android.hardware.nfc"
            android:required="false"/>
    <uses-feature android:name="android.hardware.location.gps"
            android:required="false"/>
    <uses-feature android:name="android.hardware.microphone"
            android:required="false"/>
    <uses-feature android:name="android.hardware.sensor"
            android:required="false"/>

当然,如果你的应用的某个功能需要用到以上某个特性,你应该在代码中判断设备是否支持改特性,以便做出对应的处理逻辑,怎么判断呢:

是否支持拨打电话:

if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
       //支持电话拨打
    }

是否触摸屏

if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
        //是触摸屏
    }

是否可开启相机

if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
        //可开启相机
    }

其他特性判断就不再赘述。

1.2 application节点下

android电视应用开发 电视应用开发 教程_电视应用_02


如上图,应用的启动图除了提供常规的手机APP的logo外,还需要另外提供一张横幅图片放在banner属性下,什么是横幅?请看下图:

android电视应用开发 电视应用开发 教程_android_03


横幅图片分辨率一般建议320*180的,放在xhdpi的图片资源目录下。

另外,当 TV 应用启动时,系统会显示动画,就像一个不断膨胀的实心圆。要自定义此动画的颜色,请将 TV 应用或 Activity 的 android:colorPrimary 属性设为特定颜色。此外,还应将另外两个过渡重叠属性设为 true,如主题背景资源 XML 文件中的以下代码段所示:

<resources>
        <style ... >
          <item name="android:colorPrimary">@color/primary</item>
          <item name="android:windowAllowReturnTransitionOverlap">true</item>
          <item name="android:windowAllowEnterTransitionOverlap">true</item>
        </style>
    </resources>

1.3 启动页

电视应用的启动页需要将intent-filter的categorycategory值从LAUNCHER改为LEANBACK_LAUNCHER:

<intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
            </intent-filter>

否则,你的应用将不会在电视桌面或横幅列表上显示出来,反正,LEANBACK_LAUNCHER的电视应用也不会在手机桌面显示。

1.4 启动activity的config配置

config属性需要配置navigation值,因为当用户在操作键盘方向键切换导航时,activity是会重走生命周期方法的,这是为了不让其重走生命周期方法:

android电视应用开发 电视应用开发 教程_android_04


另外,电视应用不支持竖屏显示,所以我们可以直接在清单文件中将activity的屏幕方向限定为横屏.

2.布局文件

2.1 由于电视应用都是横屏显示,所以界面布局文件要放在layout-land目录下。

2.2 电视没有状态栏,所以应用主题最好用NoActionBar的。

2.3 对于没有触控功能的电视,fragment的切换最好不要借助Viewpage来管理

2.4 对于界面内容过长时,根布局最好用NestedScrollView

3.处理焦点

3.1 为了控件能响应遥控器的方向键切换时获取焦点,需要将控件的focusable属性设为true,这样设置过后,你会发现当将应用装在具有触控功能的设备时,点击这个控件时,需要点击两次才执行,这是因为控件要先走focused,才走onclick,为了避免这种情况,再将focusable属性设为true后,记得同时显性的将控件的clickable也设为true。

3.2 对于几个同一级别或类型的ImageView,为了区分当前是哪张图片获得焦点,可以用放大的方式将获取焦点图片区别出来,如下图:

android电视应用开发 电视应用开发 教程_android_05


仔细看上图,“拨号”图片当前是获取焦点的,它相对于其他三张图片放大了。要想图片获取焦点时放大,不获取焦点时缩小到正常水平,其实只需要重写AppCompatImageView的onFocusChanged方法即可:

/**
 * @author Administrator
 * @time 2019/9/5 17:12
 * @des ${TODO}
 * @updateAuthor $Author$
 * @updateDate $Date$
 * @updateDes ${TODO}
 * 获取焦点放大的ImageView
 */
public class ScaleWithFocusImageView extends AppCompatImageView {

    private FocusedListener mFocusedListener;

    public ScaleWithFocusImageView(Context context) {
        super(context);
    }

    public ScaleWithFocusImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFocusChanged(boolean gainFocus, int direction,
                                  @Nullable Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);

        ViewCompat.animate(this)
                .scaleX(gainFocus ? 1.3f : 1.0f)//x轴方向的缩放
                .scaleY(gainFocus ? 1.3f : 1.0f)//y轴方向的缩放
                .translationZ(gainFocus ? 1f : 0f)//z轴方向的移动
                .start();

        if (mFocusedListener != null) {
            mFocusedListener.focused(gainFocus);
        }
    }

    public void setFocusedListener(FocusedListener focusedListener) {
        mFocusedListener = focusedListener;
    }

}

3.3 对于几个同一级别或类型的TextView,区分的方式可以包括缩放、文字颜色等,看下图:

android电视应用开发 电视应用开发 教程_android电视应用开发_06


上图的“拨号”和“通讯录”分别是两个Fragment的导航标题,当前获取焦点的是“拨号”的TextView,获取焦点时放大了,并且文字颜色为纯白色,相较于“通讯录”,还是很容易看得出来的。下面是重新改TextView的代码

public class FocuseScaleRadiubutton extends AppCompatRadioButton {

    private int mWhiteColor;
    private int mWhiteColor70Transp;

    public FocuseScaleRadiubutton(Context context) {
        super(context);
        init();
    }

    public FocuseScaleRadiubutton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mWhiteColor = getResources().getColor(R.color.color_white);
        mWhiteColor70Transp = getResources().getColor(R.color.color_70_transp_white);
    }

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
        ViewCompat.animate(this)
                .scaleX(focused ? 1.15f : 1.0f)
                .scaleY(focused ? 1.15f : 1.0f)
                .translationZ(focused ? 1.0f : 0f)
                .start();
        setTextColor(focused ? mWhiteColor : mWhiteColor70Transp);
    }
}

3.4 区别RecyclerView中的哪个Item获取焦点

同样的,为了时recyclerview的itemitem能够获取焦点,需要给item的布局根节点设置focusable为true,clickable为true。还是看上面那个拨号界面:

android电视应用开发 电视应用开发 教程_电视应用_07


当前获取焦点的是RecyclerView中的第二个item,我这里是显示了一个红色的线框来区别,当然,你也可以对这个item放大来区别,或者周边高亮阴影来区别。下面代码是该item中的状态背景:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!--获取焦点时的背景-->
    <item android:drawable="@drawable/shape_rec_red_stroke" android:state_focused="true"/>
    <!--按压时的背景-->
    <item android:drawable="@drawable/shape_rec_red_stroke" android:state_pressed="true"/>
    <!--焦点掠过时的背景-->
    <item android:drawable="@drawable/shape_rec_red_stroke" android:state_hovered="true"/>
    <!--失去焦点时的背景-->
    <item android:drawable="@drawable/item_tv_create_video_meeting"/>
</selector>

3.5 监听用户是否按下了遥控

这个场景主要是用于当监听到用户按下了遥控建时做对应的逻辑,例如视频播放界面,当全屏播放时,用户一段时间不操作遥控后,就隐藏某些按钮,当用户按下遥控时,再显示隐藏的按钮,只需要重写Activity的onKeyDown方法即可:

@Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if ((event.getSource() & InputDevice.SOURCE_DPAD)
                != InputDevice.SOURCE_DPAD) {
            //用户按了遥控
        }
            return super.onKeyDown(keyCode, event);
    }

3.5 处理导航

一般不需要处理用户操作遥控器方向键(上、下、左、右)时的导航顺序,如果有这个需求,可以根据下面属性在布局文件中指定你的导航顺序:

android电视应用开发 电视应用开发 教程_Android_08


如果你想在最后一个控件获取焦点后,继续按右方向键的话导航到第一个控件,那么你只需要将最后一个控件中nextFocusRight的值设为第一个控件的id即可。