前阵子将一个手机APP改为TV应用,由于首次开发TV,故把开发过程中的一些问题记录下来,以备不时之需。电视应用和手机应用开发过程大同小异,电视应用主要注意三个地方:1是清单文件,2是布局文件,3是处理好控件获取焦点时的背景显示,因为对于没有触控功能的电视设备,用户想要点击某个控件时,只能先操作遥控器的方向键将焦点移到该控件上,接着才能按遥控器的确定键执行点击,所以就需要处理好控件获取焦点时的背景显示(区别于没有获取焦点的其他控件),以便用户能一眼看出来现在是哪个控件在获取焦点并可点击。
1.清单文件需要改的地方
1.1 处理电视可能不支持的硬件功能
电视上的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节点下
如上图,应用的启动图除了提供常规的手机APP的logo外,还需要另外提供一张横幅图片放在banner属性下,什么是横幅?请看下图:
横幅图片分辨率一般建议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是会重走生命周期方法的,这是为了不让其重走生命周期方法:
另外,电视应用不支持竖屏显示,所以我们可以直接在清单文件中将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,为了区分当前是哪张图片获得焦点,可以用放大的方式将获取焦点图片区别出来,如下图:
仔细看上图,“拨号”图片当前是获取焦点的,它相对于其他三张图片放大了。要想图片获取焦点时放大,不获取焦点时缩小到正常水平,其实只需要重写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,区分的方式可以包括缩放、文字颜色等,看下图:
上图的“拨号”和“通讯录”分别是两个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。还是看上面那个拨号界面:
当前获取焦点的是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 处理导航
一般不需要处理用户操作遥控器方向键(上、下、左、右)时的导航顺序,如果有这个需求,可以根据下面属性在布局文件中指定你的导航顺序:
如果你想在最后一个控件获取焦点后,继续按右方向键的话导航到第一个控件,那么你只需要将最后一个控件中nextFocusRight的值设为第一个控件的id即可。