最近公司需要开发一个TV的luancher,就是那种纯物理按键的遥控,没有触摸屏,现在说说我踩得那些坑。(其实布局和代码逻辑和正常的安卓应用差不多)
1.焦点 焦点 焦点,重要的事情说三遍,安卓TV由于没有触摸屏所以需要手动设置可以获取焦点的控件。
2.设置获取到状态也就是常用的select。
3.各种事件冲突。
4.按键事件 通过重写onKeyDown(),onKeyUp()方法中监听keyCode的值,来判断用户按下的是哪个键,比如OK键 其他特殊的键值,上下左右键的话,系统会自动处理的。
其实遇到最烦的也就是焦点了,下面列举一下遇到的几件坑爹事情。
1.viewpager嵌套fragment中嵌套gridview,onkeyDown事件中的OK键的键值回调不出来,经过一系列的排查发现是系统的gridview中把以下四个键值给处理掉了。
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_SPACE:
case KeyEvent.KEYCODE_NUMPAD_ENTER:
跟着源码查看:GridView.java中查看onKeyDown()方法:
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return commonKey(keyCode, 1, event);
}
继续往下跟commonKey()方法:
private boolean commonKey(int keyCode, int count, KeyEvent event) {
if (mAdapter == null) {
return false;
}
if (mDataChanged) {
layoutChildren();
}
boolean handled = false;
int action = event.getAction();
if (KeyEvent.isConfirmKey(keyCode)
&& event.hasNoModifiers() && action != KeyEvent.ACTION_UP) {
handled = resurrectSelectionIfNeeded();
if (!handled && event.getRepeatCount() == 0 && getChildCount() > 0) {
keyPressed();
handled = true;
}
}
if (!handled && action != KeyEvent.ACTION_UP) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (event.hasNoModifiers()) {
handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_LEFT);
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_RIGHT);
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (event.hasNoModifiers()) {
handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP);
} else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.hasNoModifiers()) {
handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN);
} else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
}
break;
case KeyEvent.KEYCODE_PAGE_UP:
if (event.hasNoModifiers()) {
handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP);
} else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
}
break;
case KeyEvent.KEYCODE_PAGE_DOWN:
if (event.hasNoModifiers()) {
handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN);
} else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
}
break;
case KeyEvent.KEYCODE_MOVE_HOME:
if (event.hasNoModifiers()) {
handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
}
break;
case KeyEvent.KEYCODE_MOVE_END:
if (event.hasNoModifiers()) {
handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
}
break;
case KeyEvent.KEYCODE_TAB:
// TODO: Sometimes it is useful to be able to TAB through the items in
// a GridView sequentially. Unfortunately this can create an
// asymmetry in TAB navigation order unless the list selection
// always reverts to the top or bottom when receiving TAB focus from
// another widget.
if (event.hasNoModifiers()) {
handled = resurrectSelectionIfNeeded()
|| sequenceScroll(FOCUS_FORWARD);
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
handled = resurrectSelectionIfNeeded()
|| sequenceScroll(FOCUS_BACKWARD);
}
break;
}
}
if (handled) {
return true;
}
if (sendToTextFilter(keyCode, count, event)) {
return true;
}
switch (action) {
case KeyEvent.ACTION_DOWN:
return super.onKeyDown(keyCode, event);
case KeyEvent.ACTION_UP:
return super.onKeyUp(keyCode, event);
case KeyEvent.ACTION_MULTIPLE:
return super.onKeyMultiple(keyCode, count, event);
default:
return false;
}
}
往下跟可以看到KeyEvent.isConfirmKey()方法:
@UnsupportedAppUsage
public static final boolean isConfirmKey(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_SPACE:
case KeyEvent.KEYCODE_NUMPAD_ENTER:
return true;
default:
return false;
}
}
这里把以上4个键值给消费掉了
所以就会出现之前说的那种情况。
解决方案,重写自己的GridView继承自系统的GridView,然后重写对应方法:
public class CustomGridView extends GridView {
public CustomGridView(Context context) {
super(context);
}
public CustomGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomGridView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_SPACE:
case KeyEvent.KEYCODE_NUMPAD_ENTER:
return false;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_SPACE:
case KeyEvent.KEYCODE_NUMPAD_ENTER:
return false;}
return super.onKeyMultiple(keyCode, repeatCount, event);
}
}
使用以上自定义的GridView就能解决gridview中获取不到OK键的监听。
2.ScrollView中的子view获取不到焦点。因为不是触摸屏,所以需要获取焦点的view在xml中设置android:focusable="true" 属性。还有个很重要的属性,也是平时触摸屏基本上用不到的属性android:descendantFocusability="afterDescendants" 百度一下这个属性的3种值代表什么意思。
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="afterDescendants"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_ly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector1"
android:focusable="true"
android:onClick="@{activity::onClick}"
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/x19"
android:layout_height="@dimen/x19"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/x4"
android:src="@mipmap/icon_ly" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/x24"
android:layout_marginLeft="@dimen/x3"
android:gravity="center_vertical"
android:text="蓝牙"
android:textColor="#FFFFFF"
android:textSize="@dimen/x11" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_pmxm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector1"
android:focusable="true"
android:onClick="@{activity::onClick}"
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/x19"
android:layout_height="@dimen/x19"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/x4"
android:src="@mipmap/icon_pmxm" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/x24"
android:layout_marginLeft="@dimen/x3"
android:gravity="center_vertical"
android:text="屏幕休眠"
android:textColor="#FFFFFF"
android:textSize="@dimen/x11" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_ld"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector1"
android:focusable="true"
android:onClick="@{activity::onClick}"
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/x19"
android:layout_height="@dimen/x19"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/x4"
android:src="@mipmap/icon_ld" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/x24"
android:layout_marginLeft="@dimen/x3"
android:gravity="center_vertical"
android:text="亮度"
android:textColor="#FFFFFF"
android:textSize="@dimen/x11" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_cckj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector1"
android:focusable="true"
android:onClick="@{activity::onClick}"
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/x19"
android:layout_height="@dimen/x19"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/x4"
android:src="@mipmap/icon_cckj" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/x24"
android:layout_marginLeft="@dimen/x3"
android:gravity="center_vertical"
android:text="存储空间"
android:textColor="#FFFFFF"
android:textSize="@dimen/x11" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_rq"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector1"
android:focusable="true"
android:onClick="@{activity::onClick}"
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/x19"
android:layout_height="@dimen/x19"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/x4"
android:src="@mipmap/icon_rq" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/x24"
android:layout_marginLeft="@dimen/x3"
android:gravity="center_vertical"
android:text="日期"
android:textColor="#FFFFFF"
android:textSize="@dimen/x11" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_gybj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector1"
android:focusable="true"
android:onClick="@{activity::onClick}"
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/x19"
android:layout_height="@dimen/x19"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/x4"
android:src="@mipmap/icon_gybj" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/x24"
android:layout_marginLeft="@dimen/x3"
android:gravity="center_vertical"
android:text="关于本机"
android:textColor="#FFFFFF"
android:textSize="@dimen/x11" />
</LinearLayout>
</LinearLayout>
</ScrollView>
3.获取到焦点和没获取到焦点时显示的背景色不一样,这个功能在触摸屏中也经常用到的select就出来了。
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/black" android:state_focused="false" />
<item android:drawable="@color/white1" android:state_focused="true" />
</selector>
4.RecyclerView中的item获取不到焦点的问题,或者当RecyclerView滚动时,子item的焦点就丢失了。
4.1先重写LinearLayoutManager:
public class FocusLinearLayoutManager extends LinearLayoutManager {
public FocusLinearLayoutManager(Context context) {
super(context);
}
public FocusLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public FocusLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public View onInterceptFocusSearch(View focused, int direction) {
int count = getItemCount();//获取item的总数
int fromPos = getPosition(focused);//当前焦点的位置
int lastVisibleItemPos = findLastVisibleItemPosition();//最新的已显示的Item的位置
switch (direction) {//根据按键逻辑控制position
case View.FOCUS_UP:
fromPos--;
break;
case View.FOCUS_DOWN:
fromPos++;
break;
}
Log.i("dz", "onInterceptFocusSearch , fromPos = " + fromPos + " , count = " + count+" , lastVisibleItemPos = "+lastVisibleItemPos);
if(fromPos < 0 || fromPos >= count ) {
//如果下一个位置<0,或者超出item的总数,则返回当前的View,即焦点不动
if (fromPos < 0) {
return super.onInterceptFocusSearch(focused, direction);
}
return focused;
} else {
//如果下一个位置大于最新的已显示的item,即下一个位置的View没有显示,则滑动到那个位置,让他显示,就可以获取焦点了
if (fromPos > lastVisibleItemPos) {
scrollToPosition(fromPos);
}
}
return super.onInterceptFocusSearch(focused, direction);
}
}
4.2 Item的根布局中添加android:focusable="true" 属性和android:background="@drawable/selector1" 4.3 使用FocusLinearLayoutManager :
RecyclerView.setLayoutManager(new FocusLinearLayoutManager(this, FocusLinearLayoutManager.VERTICAL, false));
这样就解决的之前说的问题。遇到以上的坑 记录分享一下,避免再次踩坑。(总而言之,没有想象中的那么难,系统会处理很多按键的事件,就是焦点和特殊键值需要处理一下)
|