您的位置:首页 > 移动开发 > Android开发

从源码出发浅析Android TV的焦点移动原理-上篇

2017-08-31 15:08 405 查看
转载自:https://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651232390&idx=1&sn=b997e7a784458ffc0ad2b9e8d1cbe545&chksm=f1d9e5e5c6ae6cf3b03f33f3d7d50b3fcf33236fa553f7948db96e53f8a2e7712e09d59b5d8f&mpshare=1&scene=23&srcid=0831jdB3xhs3xUQd2i8cHxua#rd


焦点:

焦点(Focus)可以理解为选中态,在Android TV上起很重要的作用。一个视图控件只有在获得焦点的状态下,才能响应按键的Click事件。



上图中,外面有一个绿色光圈的视图,就是当前有焦点的视图。

相对于手机上用手指点击屏幕产生的Click事件, 在使用Android TV的过程中,遥控器是一个主流的操作工具,通过点击遥控器的方向键来控制焦点的移动。当焦点移动到目标控件上之后,按下遥控器的确定键,才会触发一个Click事件,进而去做下一步的处理。焦点的移动如下图所示。




基础的用法:

在处理焦点的时候,有一些基础的用法需要知道。

首先,isFocusable()需要为true,一个控件才有资格可以获取到焦点,可以通过setFocusable(boolean)方法来设置。如果想要在触摸模式下获取焦点(在我们用手机开发的过程中),需要isFocusableInTouchMode()为true,可以通过setFocusableInTouchMode(boolean)来设置。也可以直接在xml布局文件中指定:
<Button
   ...
   android:focusable="true"
   android:focusableInTouchMode="true"/>


然后,就是控制焦点的移动了。在谷歌官方文档中提到:

焦点移动的时候(默认的情况下),会按照一种算法去找在指定移动方向上最近的邻居。在一些情况下,焦点的移动可能跟开发者的意图不符,这时开发者可以在布局文件中使用下面这些XML属性来指定下一个焦点对象:
nextFocusDown
nextFocusLeft
nextFocusRight
nextFocusUp


在Java代码中,让一个指定的View获取焦点,可以调用它的requestFocus()方法。


遇到的问题:

尽管有了官方文档中提到的基础用法,但是在进行Android TV开发的过程中,还是经常会遇到一些焦点方面的问题或者疑问,如

“明明指定了焦点id,焦点却跑丢了”

“onKeyDown里居然截获不到按键事件”

“我没有做任何焦点处理,焦点是怎么自己跑到那个View上的”

接下来,带着这些问题,我们就从源码的角度出发,简单分析一下焦点的移动原理。本文以API 23作为参考。


KeyEvent

在手机上,当手指触摸屏幕时,会产生一个的触摸事件,MotionEvent,进而完成点击,长按,滑动等行为。
而当按下遥控器的按键时,会产生一个按键事件,就是KeyEvent,包含“上”,“下”,“左”,“右”,“返回”,“确定”等指令。焦点的处理就在KeyEvent的分发当中完成。

首先,KeyEvent会流转到ViewRootImpl中开始进行处理,具体方法是内部类ViewPostImeInputStage中的processKeyEvent。(在API
17之前,是deliverKeyEventPostIme这个方法,逻辑大体一致,本文仅以processKeyEvent作为参考)

private
int processKeyEvent(QueuedInputEvent q) {  

 
   final KeyEvent event = (KeyEvent)q.mEvent;        ...
     

 //
Deliver the key to the view hierarchy.      

 //
1. 先去执行mView的dispatchKeyEvent      

 if
(mView.dispatchKeyEvent(event)) {      

 
   return FINISH_HANDLED;    

 
 }    

 
 ...    

 
 // Handle automatic focus changes.

 
     if (event.getAction() == KeyEvent.ACTION_DOWN) {  

 
       int direction = 0;    

 
     ...    

 
     if (direction != 0) {          

 
   View focused = mView.findFocus();    

 
         if (focused != null) {          

 
       // 2. 之后会通过focusSearch去找下一个焦点视图    

 
             View v = focused.focusSearch(direction);      

 
           if (v != null && v != focused) {        

 
             ...                

 
     if (v.requestFocus(direction, mTempRect)) {      

 
                   ...                

 
         return FINISH_HANDLED;          

 
           }          

 
       }
             

 
   // Give the focused view a last chance to handle the dpad key.    

 
             if (mView.dispatchUnhandledMove(focused, direction)) {      

 
               return FINISH_HANDLED;                

 
 }        

 
     } else {        

 
         // find the best view to give focus to in this non-touch-mode with no-focus    

 
             // 3. 如果当前本来就没有焦点视图,也会通过focusSearch找一个视图    

 
             View v = focusSearch(null, direction);    

 
             if (v != null && v.requestFocus(direction)) {    

 
                 return FINISH_HANDLED;        

 
         }          

 
   }          

 }
   

 
 }    

 
 return FORWARD;  

 }

从几处关键的代码,可以看到这里的逻辑是:

先去执行mView的dispatchKeyEvent

之后会通过focusSearch去找下一个焦点视图

如果当前本来就没有焦点View,也会通过focusSearch找一个视图

ViewRootImpl就是ViewRoot,继承了ViewParent,但本身并不是一个View,可以看作是View树的管理者。而这里的成员变量mView就是DecorView,它指向的对象跟Window和Activity的mDecor指向的对象是同一个对象。所有的View组成了一个View树,每一个View都是树中的一个节点,如下图所示:



最上层的根是DecorView,中间是各ViewGroup,最下层是View。
本文的分析都是基于View树的。

在processKeyEvent中,首先走了mView的dispatchKeyEvent,也就是从DecorView开始进行KeyEvent的分发。


1. dispatchKeyEvent

首先走DecorView的dispatchKeyEvent,之后会依次从Activity->ViewGroup->View的方向分发KeyEvent。

有兴趣的话可以通过trace看一下KeyEvent的流转方向:



对于KeyEvent的分发,之后会另开一篇细讲,包括KeyEvent的处理优先级,长按的识别等,这里只简单看一下ViewGroup和View的dispatchKeyEvent。

首先看ViewGroup的dispatchKeyEvent。
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
   ...
   if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
           == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
       // 1.1 以View的身份处理KeyEvent
       if (super.dispatchKeyEvent(event)) {
           return true;
       }
   } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
           == PFLAG_HAS_BOUNDS) {
       // 1.2 以ViewGroup的身份把KeyEvent交给mFocused处理
       if (mFocused.dispatchKeyEvent(event)) {
           return true;
       }
   }
   ...
   return false;
}


通过flag的判断,有两个处理路径,也可以看到在处理keyEvent时,ViewGroup扮演两个角色:

View的角色,也就是此时keyEvent需要在自己与其他View之间流转

ViewGroup的角色,此时keyEvent需要在自己的子View之间流转

当作View的时候,会调用自己View的dispatchKeyEvent。
当作ViewGroup的时候,会调用当前焦点View的dispatchKeyEvent。
其实,从概念上来看,都是调用当前有焦点View的dispatchKeyEvent,只不过有时是自己本身,有时是他的子View。

再看看View的dispatchKeyEvent
public boolean dispatchKeyEvent(KeyEvent event) {
   ...
   ListenerInfo li = mListenerInfo;
   // 1.3 如果设置了mOnKeyListener,则优先走onKey方法
   if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
           && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
       return true;
   }
   // 1.4 把View自己当作参数传入,调用KeyEvent的dispatch方法
   if (event.dispatch(this, mAttachInfo != null
           ? mAttachInfo.mKeyDispatchState : null, this)) {
       return true;
   }
   ...
   return false;
}

View这里,会优先处理OnKeyListener的onKey回调。
然后才可能会走KeyEvent的dispatch,最终走到View的OnKeyDown或者OnKeyUp。



其中任何一步都可以通过return
true的方式来消费掉这个KeyEvent,结束这个分发过程。


2. focusSearch

如果dispatchKeyEvent没有消费掉这个KeyEvent,会由系统来处理焦点的移动。
通过View的focusSearch方法找到下一个获取焦点的View,然后调用requestFocus

那focusSearch是如何找到下一个焦点视图的呢?
// View.java
public View focusSearch(@FocusRealDirection int direction) {
   if (mParent != null) {
       return mParent.focusSearch(this, direction);
   } else {
       return null;
   }
}


View并不会直接去找,而是交给它的parent去找。
// ViewGroup.java
public View focusSearch(View focused, int direction) {
   if (isRootNamespace()) {
       // root namespace means we should consider ourselves the top of the
       // tree for focus searching; otherwise we could be focus searching
       // into other tabs.  see LocalActivityManager and TabHost for more info
       return FocusFinder.getInstance().findNextFocus(this, focused, direction);
   } else if (mParent != null) {
       return mParent.focusSearch(focused, direction);
   }
   return null;
}


判断是否为顶层布局,若是则执行对应方法,若不是则继续向上寻找,说明会从内到外的一层层进行判断,直到最外层的布局为止。

有意思的是,Android提供了设置isRootNamespace的方法,但又hide了起来不让使用,看来这个逻辑还有待优化。
/**
* {@hide}
*
* @param isRoot true if the view belongs to the root namespace, false
*        otherwise
*/
public void setIsRootNamespace(boolean isRoot) {
   if (isRoot) {
       mPrivateFlags |= PFLAG_IS_ROOT_NAMESPACE;
   } else {
       mPrivateFlags &= ~PFLAG_IS_ROOT_NAMESPACE;
   }
}


最后的算法交给了FocusFinder
FocusFinder.getInstance().findNextFocus(this, focused, direction);


isRootNamespace()的ViewGroup把自己和当前焦点(View)以及方向传入。
// FocusFinder.java

public final View findNextFocus(ViewGroup root, View focused, int direction) {
   return findNextFocus(root, focused, null, direction);
}

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
   View next = null;
   if (focused != null) {
       // 2.1 优先从xml或者代码中指定focusid的View中找
       next = findNextUserSpecifiedFocus(root, focused, direction);
   }
   if (next != null) {
       return next;
   }
   ArrayList<View> focusables = mTempList;
   try {
       focusables.clear();
       root.addFocusables(focusables, direction);
       if (!focusables.isEmpty()) {
           // 2.2 其次,根据算法去找,原理就是找在方向上最近的View
           next = findNextFocus(root, focused, focusedRect, direction, focusables);
       }
   } finally {
       focusables.clear();
   }
   return next;
}


这里root是上面isRootNamespace()为true的ViewGroup,focused是当前焦点视图

优先找开发者指定的下一个focus的视图 ,就是在xml或者代码中指定NextFocusDirection Id的视图

其次,根据算法去找,原理就是找在方向上最近的视图


2.1 findNextUserSpecifiedFocus

// FocusFinder.javaprivate
View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
 
 // check for user specified next focus    

View
userSetNextFocus = focused.findUserSetNextFocus(root, direction);  

 if
(userSetNextFocus != null && userSetNextFocus.isFocusable()    

 
     && (!userSetNextFocus.isInTouchMode()          

 
       || userSetNextFocus.isFocusableInTouchMode())) {

 
     return userSetNextFocus;  

 }
 

 return
null;

}

首先执行View的findUserSetNextFocus方法

//
View.javaView findUserSetNextFocus(View root, @FocusDirection int direction) {
 

 switch
(direction) {      

 case
FOCUS_LEFT:      

 
   if (mNextFocusLeftId == View.NO_ID) return null;        

 
 return findViewInsideOutShouldExist(root, mNextFocusLeftId);    

 
 ...    

 
 }  

 }
 

 return
null;

}

比如,按了“左”方向键,如果设置了mNextFocusLeftId,则会通过findViewInsideOutShouldExist去找这个View。

mNextFocusLeftId一般是在xml里面设置的,比如
<Button
   android:id="@+id/btn_1"
   android:nextFocusLeft="@+id/btn_2"
   ... />


也可以在java代码里设置
mBtn1.setNextFocusLeftId(R.id.btn_2);


来看看findViewInsideOutShouldExist做了什么。
private View findViewInsideOutShouldExist(View root, int id) {
   if (mMatchIdPredicate == null) {
       // 可以理解为一个判定器,如果id匹配则判定成功
       mMatchIdPredicate = new MatchIdPredicate();
   }
   mMatchIdPredicate.mId = id;
   View result = root.findViewByPredicateInsideOut(this, mMatchIdPredicate);
   ...
   return result;
}

public final View findViewByPredicateInsideOut(View start, Predicate<View> predicate) {
   View childToSkip = null;
   for (;;) {
       // 从当前起始节点开始寻找(ViewGroup是遍历自己的child),寻找id匹配的View,跳过childToSkip,具体可去看View和ViewGroup中该方法的具体实现
       View view = start.findViewByPredicateTraversal(predicate, childToSkip);
       if (view != null || start == this) {
           return view;
       }

       ViewParent parent = start.getParent();
       if (parent == null || !(parent instanceof View)) {
           return null;
       }

       // 如果如果当前节点没有,则往上一级,从自己的parent中查找,并跳过自己
       childToSkip = start;
       start = (View) parent;
   }
}

protected View findViewByPredicateTraversal(Predicate<View> predicate, View childToSkip) {
   if (predicate.apply(this)) {
       return this;
   }
   return null;
}


ViewGroup的findViewByPredicateTraversal
// ViewGroup
@Override
protected View findViewByPredicateTraversal(Predicate<View> predicate, View childToSkip) {
   if (predicate.apply(this)) {
       return this;
   }

   final View[] where = mChildren;
   final int len = mChildrenCount;

   for (int i = 0; i < len; i++) {
       View v = where[i];

       if (v != childToSkip && (v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
           v = v.findViewByPredicate(predicate);

           if (v != null) {
               return v;
           }
       }
   }

   return null;
}


可以看到,findViewInsideOutShouldExist这个方法从当前指定视图去寻找指定id的视图。首先从自己开始向下遍历,如果没找到则从自己的parent开始向下遍历,直到找到id匹配的视图为止。

这里要注意的是,也许存在多个相同id的视图(比如ListView,RecyclerView,ViewPager等场景),但是这个方法只会返回在View树中节点范围最近的一个视图,这就是为什么有时候看似指定了focusId,但实际上焦点却丢失的原因,因为焦点跑到了另一个“意想不到”的相同id的视图上。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐