Android TV 焦点与按键事件分析

转自:http://blog.****.net/yummykwok/article/details/56667260


在触摸屏出现在手机上之前,焦点是手机上人机交互中最重要的一个概念。焦点即用户当前的关注点(或区域),手机上将该区域以某种形式高亮显示,人们通过上、下、左、右方向键可以移动焦点,按确认键后手机将打开(或呈显)与当前焦点关联的内容;触摸屏的出现大大地简化了人机交互,触摸事件(TouchEvent)成了核心,焦点的存在感就很小了。

       但是对于电视来说,其显示屏面积大,人机距离远,触摸屏的方案显然不合理。因此目前Android电视的人机交互仍旧使用遥控器为主,焦点的重要性在电视上又显现出来了。通过遥控器将方向键或确认键信号(或信息)发送到电视端后,转换为标准按键事件(KeyEvent),而按键事件分发最终目标就是焦点。


1、初识View之焦点

ViewUI组件的基本构建,也自然就是焦点的承载者。View是否可聚焦,由FOCUSABLEFOCUSABLE_IN_TOUCH_MODE(触摸模式下也可以有焦点)两个FLAG标识。

[java] view plain copy
  1. public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {  
  2.     this(context);  
  3.     final TypedArray a = context.obtainStyledAttributes(  
  4.             attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);  
  5.     final int N = a.getIndexCount();  
  6.     for (int i = 0; i < N; i++) {  
  7.         int attr = a.getIndex(i);  
  8.         switch (attr) {  
  9.             ……  
  10.             case com.android.internal.R.styleable.View_focusable:  
  11.                 if (a.getBoolean(attr, false)) {  
  12.                     viewFlagValues |= FOCUSABLE;  
  13.                     viewFlagMasks |= FOCUSABLE_MASK;  
  14.                 }  
  15.                 break;  
  16.             case com.android.internal.R.styleable.View_focusableInTouchMode:  
  17.                 if (a.getBoolean(attr, false)) {  
  18.                     viewFlagValues |= FOCUSABLE_IN_TOUCH_MODE | FOCUSABLE;  
  19.                     viewFlagMasks |= FOCUSABLE_IN_TOUCH_MODE | FOCUSABLE_MASK;  
  20.                 }  
  21.                 break;  
  22.             ……  
  23.         }  
  24.     }  
  25.     ……  
  26. }  

从上面 View 的构建方法上看,在xml 里即可为其设置是否可聚焦,以 Button 举个栗子,

[java] view plain copy
  1. public class Button extends TextView {  
  2.     ……  
  3.     public Button(Context context, AttributeSet attrs) {  
  4.         this(context, attrs, com.android.internal.R.attr.buttonStyle);  
  5.     }  
  6.     ……  
  7. }  

Button设置了一个默认的style,我们找出源码看看,

[html] view plain copy
  1. <stylenamestylename="Widget.Button">  
  2.     <itemnameitemname="background">@drawable/btn_default</item>  
  3.     <strong><itemnameitemname="focusable">true</item></strong>  
  4.     <itemnameitemname="clickable">true</item>  
  5.     <itemnameitemname="textAppearance">?attr/textAppearanceSmallInverse</item>  
  6.     <itemnameitemname="textColor">@color/primary_text_light</item>  
  7.     <itemnameitemname="gravity">center_vertical|center_horizontal</item>  
  8. </style>  
聚焦后,Button 背景将发生改变,向用户表示该 View 已聚焦。我们可以打开该 style 设置的 background 的源文件btn_default 看看,

[html] view plain copy
  1. <selectorxmlns:androidselectorxmlns:android="http://schemas.android.com/apk/res/android">  
  2.    ......  
  3.    <itemandroid:state_focuseditemandroid:state_focused="true"  
  4.       android:drawable="@drawable/btn_default_normal_disable_focused"/>  
  5.     <item  
  6.         android:drawable="@drawable/btn_default_normal_disable"/>  
  7. </selector>  
可以看到,这是个 selector,状态变成已聚焦后,使用另一drawable做为背景(这个过程具体是怎么实现的,我们后面分析)。从上面分析看,TextView变成Button只需要为其style 设置几个关键的属性即可,最主要的是clickable,focusable, background,以下TextView即相当于Button了,

[java] view plain copy
  1. <TextView  
  2.     android:layout_width="wrap_content"  
  3.     android:layout_height="wrap_content"  
  4.     android:focusable="true"  
  5.     android:clickable="true"  
  6.     android:background=”@drawable/btn_default” />  
对于设置是否可聚焦,View还提供以下方法:
[java] view plain copy
  1. public void setFocusable(boolean focusable) ;  
  2. public void setFocusableInTouchMode(boolean focusableInTouchMode);  

2、请求焦点

2.1 View的焦点请求

焦点的请求,View提供了以下几个方法,

[java] view plain copy
  1. public final boolean requestFocus();  
  2. public final boolean requestFocus(int direction);  
  3. public boolean requestFocus(int direction, Rect previouslyFocusedRect);  
我们打开源码看,这些方法都做了些什么


[File]android/view/View.java

[java] view plain copy
  1. public final boolean requestFocus() {  
  2.     return requestFocus(View.FOCUS_DOWN);  
  3. }  
  4.   
  5. public final boolean requestFocus(int direction) {  
  6.     return requestFocus(direction, null);  
  7. }  
  8.   
  9. public boolean requestFocus(int direction, Rect previouslyFocusedRect) {  
  10.     return requestFocusNoSearch(direction, previouslyFocusedRect);  
  11. }  
  12. private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {  
  13.     // need to be focusable  
  14.     if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||  
  15.             (mViewFlags & VISIBILITY_MASK) != VISIBLE) {  
  16.         return false;  
  17.     }  
  18.     // need to be focusable in touch mode if in touch mode  
  19.     if (isInTouchMode() &&  
  20.         (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {  
  21.            return false;  
  22.     }  
  23.     // need to not have any parents blocking us  
  24.     if (hasAncestorThatBlocksDescendantFocus()) {  
  25.         return false;  
  26.     }  
  27.     handleFocusGainInternal(direction, previouslyFocusedRect);  
  28.     return true;  
  29. }  
可以看到,前两个重载方法最终都走到第三个方法内,对于View来讲,关键就是看这个私有方法requestFocusNoSearch,这个方法主要做了以下4件事:

1)检查View 是否可聚焦,是否可见。聚焦前提是 FOCUSABLE并且VISIBLE

2)如果是触摸模式,则检查该模式下是否可聚焦(FOCUSABLE_IN_TOUCH_MODE

3)检查是否被上一层(ViewGroup)屏蔽焦点

4)当前View获取焦点,处理焦点变动

Android TV 焦点与按键事件分析


2.2 ViewGroup的焦点请求

ViewGroup是可以包含其它View 的一种特殊的 View,各种Layout均是它的子类;对于焦点请求,与View不同的是:

1)它可以优先让下层View请求焦点,失败后再自己请求

2)可以优先于下层View请求焦点,失败后再下层View请求

3)可以屏蔽下层View请求焦点

这三种对下一层请求焦点的控制,分别用了三个FLAG记录于mGroupFlags,依次对应为

1FOCUS_AFTER_DESCENDANTS

2FOCUS_BEFORE_DESCENDANTS

3FOCUS_BLOCK_DESCENDANTS

设置这个控制的方法和属性为:

[java] view plain copy
  1. public void setDescendantFocusability(int focusability);  
  2.   
  3. android:descendantFocusability  
设置好后,那么它具体是怎么控制的呢?我们分以下几种情况来分析:

1ViewGroup的下层View请求焦点: 按上一节说的,View请求焦点需要检查是否被上层屏蔽的,实际就是检查上层是否设置了FOCUS_BLOCK_DESCENDANTS这个FLAG,我们回到View.java查看hasAncestorThatBlocksDescendantFocus这个检查方法,

[java] view plain copy
  1. private boolean hasAncestorThatBlocksDescendantFocus() {  
  2.     final boolean focusableInTouchMode = isFocusableInTouchMode();  
  3.     ViewParent ancestor = mParent;  
  4.     while (ancestor instanceof ViewGroup) {  
  5.         final ViewGroup vgAncestor = (ViewGroup) ancestor;  
  6.         if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS  
  7.                 || (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) {  
  8.             return true;  
  9.         } else {  
  10.             ancestor = vgAncestor.getParent();  
  11.         }  
  12.     }  
  13.     return false;  
  14. }  
这个方法中,一层层往上找,看是否有ViewGroup设置了FOCUS_BLOCK_DESCENDANTS

2ViewGroup请求焦点:ViewGroup重写了requestFocus方法以实现控制优先级,

[java] view plain copy
  1. @Override  
  2. public boolean requestFocus(int direction, Rect previouslyFocusedRect) {  
  3.     int descendantFocusability = getDescendantFocusability();  
  4.     switch (descendantFocusability) {  
  5.         case FOCUS_BLOCK_DESCENDANTS:  
  6.             return super.requestFocus(direction, previouslyFocusedRect);  
  7.         case FOCUS_BEFORE_DESCENDANTS: {  
  8.             final boolean took = super.requestFocus(direction, previouslyFocusedRect);  
  9.             return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);  
  10.         }  
  11.         case FOCUS_AFTER_DESCENDANTS: {  
  12.             final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);  
  13.             return took ? took : super.requestFocus(direction, previouslyFocusedRect);  
  14.         }  
  15.         ……  
  16.     }  
  17. }  
  18. protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {  
  19.     ……  
  20.     for (int i = index; i != end; i += increment) {  
  21.         View child = children[i];  
  22.         if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {  
  23.             if (child.requestFocus(direction, previouslyFocusedRect)) {  
  24.                 return true;  
  25.             }  
  26.         }  
  27.     }  
  28.     return false;  
  29. }  

2.3焦点的变更

2.1中提到View请求焦点最后一步是处理焦点变动,我们来细看下里面都做了些什么

[java] view plain copy
  1. void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {  
  2.     if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {  
  3.         mPrivateFlags |= PFLAG_FOCUSED;//标记已聚焦  
  4.         if (mParent != null) {  
  5.             mParent.requestChildFocus(thisthis);//告知上层ViewGroup自己已聚焦  
  6.         }  
  7.         if (mAttachInfo != null) {  
  8.             //通知OnGlobalFocusChangeListener  
  9.             View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;  
  10.             mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);  
  11.         }  
  12.         onFocusChanged(true, direction, previouslyFocusedRect);//回调OnFocusChangeListener  
  13.         refreshDrawableState();//更新drawable 状态,包括foreground以及前面提及的background  
  14.     }  
  15. }  
至此,焦点请求到显示更新已经明了,但还有个问题,同一个界面上只可以有一个焦点,当一个View获取焦点,应当让前一个焦点失焦。这意味着必须有个地方记录当前焦点,担此重任的即是ViewGroup里私有变量mFocused

[java] view plain copy
  1. public abstract class ViewGroup extends View implements ViewParent, ViewManager {  
  2.     ……  
  3.     // The view contained within this ViewGroup that has or contains focus.  
  4.     private View mFocused;  
  5.     ……  
  6. }  
这个变量指向的可能是:

1)下一层有焦点的View(ViewGroup)

2)焦点在其下层的ViewGroup

3null,焦点不在它的下层


举个例子:

Android TV 焦点与按键事件分析

很明显,如果界面上有焦点的话,从上层往下一层层找,就能找到。View/ViewGroup提供findFocus方法,用于找到当前范围内的焦点,

[java] view plain copy
  1. [File]View.java  
  2. public View findFocus() {  
  3.     return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;//返回自己如果已聚焦  
  4. }  
  5.   
  6.   
  7. [File]ViewGroup.java  
  8.   
  9. @Override  
  10. public View findFocus() {  
  11.     if (isFocused()) {  
  12.         return this;//返回自己如果已聚焦  
  13.     }  
  14.     if (mFocused != null) {  
  15.         return mFocused.findFocus();//焦点在下层,返回下层findFocus结果  
  16.     }  
  17.     return null;//无焦点  
  18. }  
那么问题来了,这个 mFocused 是怎么更新的呢,又是怎么让它失焦呢?关键就在于 handleFocusGainInternal 中的这个调用:

[java] view plain copy
  1. mParent.requestChildFocus(thisthis);//告知上层ViewGroup自己已聚焦  
[File] ViewGroup.java
[java] view plain copy
  1. public void requestChildFocus(View child, View focused) {  
  2.     if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {  
  3.         return;  
  4.     }  
  5.     // Unfocus us, if necessary  
  6.     super.unFocus(focused);//清除自己的焦点,如果有的话  
  7.     // We had a previous notion of who had focus. Clear it.  
  8.     if (mFocused != child) {  
  9.         if (mFocused != null) {  
  10.             mFocused.unFocus(focused);//让自己范围内已聚焦的失焦  
  11.         }  
  12.         mFocused = child;//更新为包含焦点的child  
  13.     }  
  14.     if (mParent != null) {  
  15.         mParent.requestChildFocus(this, focused);//告知上层ViewGroup自己包含焦点  
  16.     }  
  17. }  
们可以看requestChildFocus 这个方法会一层层往上调用,让 mFocused 失焦,然后更新为新的 child;具体地,前一焦点是怎么被清除的呢,我们来看下 unFocus 这个方法,

[File]View.java

[java] view plain copy
  1. void unFocus(View focused) {  
  2.     clearFocusInternal(focused, falsefalse);//去除聚焦标志,通知listener, 更新Drawable 状态  
  3. }  
[File]ViewGroup.java

[java] view plain copy
  1. @Override  
  2. void unFocus(View focused) {  
  3.     if (mFocused == null) {  
  4.         super.unFocus(focused);  
  5.     } else {  
  6.         mFocused.unFocus(focused);  
  7.         mFocused = null;  
  8.     }  
  9. }  
对于 ViewGroup 来说,如果mFocused 有记录,则调用其 unFocus 方法,最后将其置为 null。这样就做到了一层层住下更新mFocused,最终调用焦点View 的 clearFocusInternal 。至此,焦点的请求到更新的逻辑就应该了然于胸了。

2.4  <requestFocus/> 标签

这个标签用于布局文件中,如:

[java] view plain copy
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:orientation="vertical"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent">  
  5.     <Button  
  6.         android:id="@+id/btn0"  
  7.         android:layout_width="match_parent"  
  8.         android:layout_height="wrap_content"/>  
  9.     <Button  
  10.         android:id="@+id/btn1"  
  11.         android:layout_width="match_parent"  
  12.         android:layout_height="wrap_content">  
  13.         <requestFocus/>  
  14.     </Button>  
  15. </LinearLayout>  

添加了该标签的可聚焦的 View ,如上布局中的 btn1, 将在加载的时候(LayoutInflater#inflate)调用它的 requestFocus 方法, 

[java] view plain copy
  1. public abstract class LayoutInflater {  
  2.     ......  
  3.     private static final String TAG_REQUEST_FOCUS = "requestFocus";  
  4.     ......  
  5.     void rInflate(XmlPullParser parser, View parent, Context context,  
  6.             AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {  
  7.         ......  
  8.         while (((type = parser.next()) != XmlPullParser.END_TAG ||  
  9.                 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {  
  10.             ......  
  11.             if (TAG_REQUEST_FOCUS.equals(name)) {  
  12.                 parseRequestFocus(parser, parent);  
  13.             }  
  14.             ......  
  15.         }  
  16.         ......  
  17.     }  
  18.   
  19.     private void parseRequestFocus(XmlPullParser parser, View view)  
  20.             throws XmlPullParserException, IOException {  
  21.         view.requestFocus();//请求焦点  
  22.         ......  
  23.     }  
  24.     ......  
  25. }  


3. 按键事件KeyEvent)与焦点查找

KeyEvent的分发与 TouchEvent 的分发,大致类似,从ViewRootImpl 开始一层层往下分发,

[java] view plain copy
  1. ViewRootImpl.java (API 25)  
  2. private int processKeyEvent(QueuedInputEvent q) {  
  3.     final KeyEvent event = (KeyEvent)q.mEvent;  
  4.     // Deliver the key to the view hierarchy.  
  5.     if (mView.dispatchKeyEvent(event)) {//调用顶层View(一般为ViewGroup)的 dispatchKeyEvent  
  6.         return FINISH_HANDLED;  
  7.     }  
  8.     …...  
  9.     // Handle automatic focus changes.  
  10.     //如果前面都没有消费掉这个事件,下面将自动根据按键方向查找焦点  
  11.     if (event.getAction() == KeyEvent.ACTION_DOWN) {  
  12.         int direction = 0;  
  13.         switch (event.getKeyCode()) {  
  14.             case KeyEvent.KEYCODE_DPAD_LEFT://左  
  15.                 if (event.hasNoModifiers()) {  
  16.                     direction = View.FOCUS_LEFT;  
  17.                 }  
  18.                 break;  
  19.             case KeyEvent.KEYCODE_DPAD_RIGHT://右  
  20.                 if (event.hasNoModifiers()) {  
  21.                     direction = View.FOCUS_RIGHT;  
  22.                 }  
  23.                 break;  
  24.             case KeyEvent.KEYCODE_DPAD_UP://上  
  25.                 if (event.hasNoModifiers()) {  
  26.                     direction = View.FOCUS_UP;  
  27.                 }  
  28.                 break;  
  29.             case KeyEvent.KEYCODE_DPAD_DOWN://下  
  30.                 if (event.hasNoModifiers()) {  
  31.                     direction = View.FOCUS_DOWN;  
  32.                 }  
  33.                 break;  
  34.             case KeyEvent.KEYCODE_TAB:  
  35.                 if (event.hasNoModifiers()) {  
  36.                     direction = View.FOCUS_FORWARD;  
  37.                 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {  
  38.                     direction = View.FOCUS_BACKWARD;  
  39.                 }  
  40.                 break;  
  41.         }  
  42.         if (direction != 0) {  
  43.             View focused = mView.findFocus();//找到聚焦的View  
  44.             if (focused != null) {//已有焦点  
  45.                 View v = focused.focusSearch(direction);//从已聚焦的View查找下一可聚焦的view  
  46.                 if (v != null && v != focused) {  
  47.                     ……  
  48.                     if (v.requestFocus(direction, mTempRect)) {  
  49.                         //播放按键音效  
  50.                         playSoundEffect(SoundEffectConstants  
  51.                                 .getContantForFocusDirection(direction));  
  52.                         return FINISH_HANDLED;  
  53.                     }  
  54.                 }  
  55.                 // 没找到新焦点,最后给mView 一次处理焦点移动的机会  
  56.                 if (mView.dispatchUnhandledMove(focused, direction)) {  
  57.                     return FINISH_HANDLED;  
  58.                 }  
  59.             } else {  
  60.                 // find the best view to give focus to in this non-touch-mode with no-focus  
  61.                 View v = focusSearch(null, direction);//从顶层开始查找下一可聚焦的view  
  62.                 if (v != null && v.requestFocus(direction)) {//请求焦点  
  63.                     return FINISH_HANDLED;  
  64.                 }  
  65.             }  
  66.         }  
  67.     }  
  68.     return FORWARD;  
  69. }  
可以看到,dispatchKeyEvent如果没有消费掉,将自动查找焦点。


3.1 KeyEvent分发

如果不重写dispatchKeyEventKeyEvent分发的最终目标是当前焦点View/ViewGroup。还是以下面这个图为例,分发的路径是RootViewGroup-->ViewGroup2-->view2

Android TV 焦点与按键事件分析

实现较TouchEvent的分发简单许多,就是根据前面提到的ViewGroupmFocused来定位,我们来看下ViewGroupdispatchKeyEvent的实现,

[File]ViewGroup.java

[java] view plain copy
  1. @Override  
  2. public boolean dispatchKeyEvent(KeyEvent event) {  
  3.     if (mInputEventConsistencyVerifier != null) {  
  4.         mInputEventConsistencyVerifier.onKeyEvent(event, 1);  
  5.     }  
  6.     if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))  
  7.             == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {  
  8.         if (super.dispatchKeyEvent(event)) {//如果ViewGroup自己聚焦了,则进分发给自己处理  
  9.             return true;  
  10.         }  
  11.     } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)  
  12.             == PFLAG_HAS_BOUNDS) {//焦点在mFocused中,继续往下分发  
  13.         if (mFocused.dispatchKeyEvent(event)) {  
  14.             return true;  
  15.         }  
  16.     }  
  17.     if (mInputEventConsistencyVerifier != null) {  
  18.         mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);  
  19.     }  
  20.     return false;  
  21. }  
最终分发到焦点View上,将回调 OnKeyListener 或 KeyEvent.Callback,

[File]View.java

[java] view plain copy
  1. public boolean dispatchKeyEvent(KeyEvent event) {  
  2.     if (mInputEventConsistencyVerifier != null) {  
  3.         mInputEventConsistencyVerifier.onKeyEvent(event, 0);  
  4.     }  
  5.     // 回调OnKeyListener 的 onKey 方法  
  6.     ListenerInfo li = mListenerInfo;  
  7.     if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED  
  8.             && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {  
  9.         return true;  
  10.     }  
  11.     // View 实现了KeyEvent.Callback,包含onKeyDown,onKeyUp,onKeyLongPress等方法  
  12.     // 这里将分发给这个callback  
  13.     if (event.dispatch(this, mAttachInfo != null  
  14.             ? mAttachInfo.mKeyDispatchState : nullthis)) {  
  15.         return true;  
  16.     }  
  17.     if (mInputEventConsistencyVerifier != null) {  
  18.         mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);  
  19.     }  
  20.     return false;  
  21. }  
以看到默认的ViewGroup分发 KeyEvent 过程不会找焦点,不消费方向键,而是由ViewRootImpl 来处理。那么另一个重要的按键“确认键”呢如果当前有焦点,然后按下确认键可能需要产生点击事件,这件事就是在 View 的 onKeyDown,onKeyUp 中处理的,

[File]View.java

[java] view plain copy
  1. public boolean onKeyDown(int keyCode, KeyEvent event) {  
  2.     if (KeyEvent.isConfirmKey(keyCode)) {//如果是确认键  
  3.         if ((mViewFlags & ENABLED_MASK) == DISABLED) {  
  4.             return true;  
  5.         }  
  6.         // Long clickable items don't necessarily have to be clickable.  
  7.         if (((mViewFlags & CLICKABLE) == CLICKABLE  
  8.                 || (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)  
  9.                 && (event.getRepeatCount() == 0)) {  
  10.             // For the purposes of menu anchoring and drawable hotspots,  
  11.             // key events are considered to be at the center of the view.  
  12.             final float x = getWidth() / 2f;  
  13.             final float y = getHeight() / 2f;  
  14.             setPressed(true, x, y);//设置状态为已按下  
  15.             checkForLongClick(0, x, y);  
  16.             return true;  
  17.         }  
  18.     }  
  19.     return false;  
  20. }  
  21.   
  22. public boolean onKeyUp(int keyCode, KeyEvent event) {  
  23.     if (KeyEvent.isConfirmKey(keyCode)) {  
  24.         if ((mViewFlags & ENABLED_MASK) == DISABLED) {  
  25.             return true;  
  26.         }  
  27.         if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {  
  28.             setPressed(false);//设置状态为未按下  
  29.             if (!mHasPerformedLongPress) {  
  30.                 // This is a tap, so remove the longpress check  
  31.                 removeLongPressCallback();  
  32.                 return performClick();//回调OnClickListener  
  33.             }  
  34.         }  
  35.     }  
  36.     return false;  
  37. }  

3.2焦点查找

前面提到ViewRootImpl里可能会根据按键方向查找焦点,如果已有聚焦的View,就调用 View focusSearch,从该View开始查找,否则调用自己的focusSearch 方法从顶层开始查找。我们先来看 View 的这个方法,

[File]View.java

[java] view plain copy
  1. public View focusSearch(@FocusRealDirection int direction) {  
  2.     if (mParent != null) {  
  3.         return mParent.focusSearch(this, direction);  
  4.     } else {  
  5.         return null;  
  6.     }  
  7. }  
View简单地让上一层ViewGroup来查找,再来看ViewGroup的这个方法,

[File]ViewGroup.java

[java] view plain copy
  1. public View focusSearch(View focused, int direction) {  
  2.     if (isRootNamespace()) {// installDecor时设置mDecor.setIsRootNamespace(true)  
  3.         // root namespace means we should consider ourselves the top of the  
  4.         // tree for focus searching; otherwise we could be focus searching  
  5.         // into other tabs.  see LocalActivityManager and TabHost for more info  
  6.         return FocusFinder.getInstance().findNextFocus(this, focused, direction);  
  7.     } else if (mParent != null) {  
  8.         return mParent.focusSearch(focused, direction);  
  9.     }  
  10.     return null;  
  11. }  
一直调用上一层 ViewGroup 的focusSearch,直到当前是rootView,使用 FocusFinder 在rootView范围内开始查找,实际上 ViewRootImpl里也同样是使用FocusFinder 来查找,我们下面看下findNextFocus这个方法,

[File]FocusFinder.java

[java] view plain copy
  1. public final View findNextFocus(ViewGroup root, View focused, int direction) {  
  2.     if (focused != null) {  
  3.         // check for user specified next focus//查找用户指定的下一个焦点  
  4.         View userSetNextFocus = focused.findUserSetNextFocus(root, direction);  
  5.         if (userSetNextFocus != null &&  
  6.             userSetNextFocus.isFocusable() &&  
  7.             (!userSetNextFocus.isInTouchMode() ||  
  8.              userSetNextFocus.isFocusableInTouchMode())) {  
  9.             return userSetNextFocus;  
  10.         }  
  11.         // fill in interesting rect from focused  
  12.         ……  
  13.         //将 mFocusedRect 设成focused的区域  
  14.     } else {  
  15.         // make up a rect at top left or bottom right of root  
  16.         //将 mFocusedRect 设成root的区域  
  17.         ……  
  18.     }  
  19.     return findNextFocus(root, focused, mFocusedRect, direction);//根据区域和方向查找  
  20. }  
如果已经存在焦点,并且该焦点 View 设置了某方向的下一焦点ViewID,那么根据ID 找出这个View 即可;否则根据当前焦点区域按方向查找,这个算法这里就暂不介绍了。