Android 控件获取焦点

控件请求焦点

  方法requestFocus()代码如下:

    public final boolean requestFocus() {
        return requestFocus(View.FOCUS_DOWN);
    }

    public final boolean requestFocus(int direction) {
        return requestFocus(direction, null);
    }

    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        return requestFocusNoSearch(direction, previouslyFocusedRect);
    }

    private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
        // need to be focusable
        if (!canTakeFocus()) {
            return false;
        }

        // need to be focusable in touch mode if in touch mode
        if (isInTouchMode() &&
            (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
               return false;
        }

        // need to not have any parents blocking us
        if (hasAncestorThatBlocksDescendantFocus()) {
            return false;
        }

        if (!isLayoutValid()) {
            mPrivateFlags |= PFLAG_WANTS_FOCUS;
        } else {
            clearParentsWantFocus();
        }

        handleFocusGainInternal(direction, previouslyFocusedRect);
        return true;
    }

  最终代码走到方法requestFocusNoSearch()方法中,canTakeFocus()方法代表控件是可以获取焦点,看下代码看看什么情况下可以获取焦点:

    private boolean canTakeFocus() {
        return ((mViewFlags & VISIBILITY_MASK) == VISIBLE)
                && ((mViewFlags & FOCUSABLE) == FOCUSABLE)
                && ((mViewFlags & ENABLED_MASK) == ENABLED)
                && (sCanFocusZeroSized || !isLayoutValid() || hasSize());
    }

  前面三个标识,都好理解,可见,可获取焦点,可使用状态。最后一个条件是一个||组成的逻辑表达式。静态变量sCanFocusZeroSized代表的是0大小的控件能否可以获取焦点,默认为false,根据项目的targetSdkVersion设置来确定该值。如果targetSdkVersion小于28,则该值为true。isLayoutValid()代码如下:

    /**
     * Returns true if this view has been through at least one layout since it
     * was last attached to or detached from a window.
     */
    public boolean isLaidOut() {
        return (mPrivateFlags3 & PFLAG3_IS_LAID_OUT) == PFLAG3_IS_LAID_OUT;
    }

    /**
     * @return {@code true} if laid-out and not about to do another layout.
     */
    boolean isLayoutValid() {
        return isLaidOut() && ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == 0);
    }

  isLaidOut()方法是检查PFLAG3_IS_LAID_OUT标识,代表控件上次绑定窗口之后至少执行过一次布局计算(layout()方法)。所以isLayoutValid()就是控件上次绑定窗口之后至少执行过一次布局计算并且不需要另一次布局。PFLAG_FORCE_LAYOUT代表需要执行另一次布局操作(measure()、layout())。所以isLayoutValid()方法的求反,就是控件还未设置PFLAG3_IS_LAID_OUT标识或者即将进行一次布局操作计算大小。还未设置PFLAG3_IS_LAID_OUT标识,代表控件还未绑定窗口或者绑定窗口还没有做第一次布局操作。
  第三个条件是hasSize(),它的意思就是控件的大小面积大于0。
  canTakeFocus()的最后一个条件意思就是控件的大小是大于0,或者绑定窗口还没有做第一次布局操作或即将去执行一次布局操作,或者设置目标targetSdkVersion小于28。
  requestFocusNoSearch在canTakeFocus()方法返回false的时候,就是不能获取到焦点的情况,也会返回false结束函数。代码继续向下,判断当前是触摸模式,并且在触摸模式下是否可以获取焦点。如果这个条件不满足,也会返回false结束函数。触摸模式是通过isInTouchMode()方法来判断的,既然有触摸模式,那有没有非触摸模式?下面解释一下触摸模式

触摸模式

  当用户通过一个方向键(例如D-pad),操纵用户的意图,可执行的控件获取焦点是必要的,这样用户就能看到要操作的控件是哪个。如果设备有触摸能力,当用户通过触摸它交互,这个时候就没有必要高亮显示控件或者让控件获取焦点了,这激活一个交互模式,就是触摸模式。
  用户一旦触摸了显示屏幕,设备就会进入触摸模式。从这时起,isFocusableInTouchMode()返回true的控件,在触摸的时候才能获取焦点。这点在前面ACTION_UP类型事件处理的时候讲过了。满足isFocusableInTouchMode()返回true的控件如可编辑文本的控件。但是像Button是不满足的,所以Button直接表现点击事件。
  用户如果又点击了方向键(例如D-pad),设备即将退出触摸模式,用户又能找到一个获取焦点的控件操作。
  接着调用hasAncestorThatBlocksDescendantFocus()方法判断是否存在祖先控件会阻止其获取焦点,如果存在,也会返回false。看一下该方法:

    private boolean hasAncestorThatBlocksDescendantFocus() {
        final boolean focusableInTouchMode = isFocusableInTouchMode();
        ViewParent ancestor = mParent;
        while (ancestor instanceof ViewGroup) {
            final ViewGroup vgAncestor = (ViewGroup) ancestor;
            if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS
                    || (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) {
                return true;
            } else {
                ancestor = vgAncestor.getParent();
            }
        }
        return false;
    }

  如果父控件的getDescendantFocusability()方法得到的标识为ViewGroup.FOCUS_BLOCK_DESCENDANTS,或者父控件在触摸模式下不能获取焦点并且父控件的shouldBlockFocusForTouchscreen()方法的值为true,就会返回true。并且该方法会通过父控件向上传递,只要有一个祖先控件满足上面的条件,就会返回true,也就是阻止当前控件获取焦点。
  下面看一下ViewGroup类的shouldBlockFocusForTouchscreen()方法:

    @ViewDebug.ExportedProperty(category = "focus")
    @InspectableProperty
    public boolean getTouchscreenBlocksFocus() {
        return (mGroupFlags & FLAG_TOUCHSCREEN_BLOCKS_FOCUS) != 0;
    }
    boolean shouldBlockFocusForTouchscreen() {
        // There is a special case for keyboard-navigation clusters. We allow cluster navigation
        // to jump into blockFocusForTouchscreen ViewGroups which are clusters. Once in the
        // cluster, focus is free to move around within it.
        return getTouchscreenBlocksFocus() &&
                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
                && !(isKeyboardNavigationCluster()
                        && (hasFocus() || (findKeyboardNavigationCluster() != this)));
    }

  getTouchscreenBlocksFocus()方法就是检测FLAG_TOUCHSCREEN_BLOCKS_FOCUS标识存在,该标识代表容器控件应该忽略它本身及子控件的焦点请求。mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)代表设备有一个触摸屏。第三个条件不好理解,用求反的符号,就是要排除掉这种情况,isKeyboardNavigationCluster()
&& (hasFocus() || (findKeyboardNavigationCluster() != this))。

键盘导航键区

  这个先说下键盘导航键区的概念,应用的某个操作组件使用一种复杂的视图层次结构,可考虑将多组界面元素组成一个键区,简化键盘导航这些元素的操作。用户可以在 Chromebook 设备上按 Meta+Tab 或 Search+Tab,在不同键区之间导航。键区的一些范例包括:侧面板、导航栏、主内容区域和可能包含多个子元素的元素。要将一个 View 或 ViewGroup 元素设置为一个键区,请在元素的布局 XML 文件中将 android:keyboardNavigationCluster 属性设置为 true,或者将 true 传递至应用界面逻辑中的 setKeyboardNavigationCluster()。注意:键区不能嵌套,不过,非嵌套键区可以显示在层次结构的不同层级。如果您尝试嵌套键区,框架仅会将最顶层的 ViewGroup 元素视为键区。
  接着看上面的第三个条件,isKeyboardNavigationCluster()

    public final boolean isKeyboardNavigationCluster() {
        return (mPrivateFlags3 & PFLAG3_CLUSTER) != 0;
    }

这个方法就是判断PFLAG3_CLUSTER标识,如果存在该标识,这个控件就是一个键盘导航键区的根源。接着看hasFocus()

    @Override
    public boolean hasFocus() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0 || mFocused != null;
    }

这个是ViewGroup类型的hasFocus(),返回true,就是代表控件有焦点或者包含焦点。mFocused是它的一个子控件,获得焦点或者它是获得焦点的子控件的祖先。通过mFocused 可以层层传递到获得焦点的子控件。再看findKeyboardNavigationCluster() != this,

    View findKeyboardNavigationCluster() {
        if (mParent instanceof View) {
            View cluster = ((View) mParent).findKeyboardNavigationCluster();
            if (cluster != null) {
                return cluster;
            } else if (isKeyboardNavigationCluster()) {
                return this;
            }
        }
        return null;
    }

该方法是通过递归父控件一直到ViewRootImpl类实例,ViewRootImpl类实例开始,从上到下寻找首先设置PFLAG3_CLUSTER标识的父控件返回。
  分析完上面的方法就能解释下isKeyboardNavigationCluster()
&& (hasFocus() || (findKeyboardNavigationCluster() != this))这个条件,控件是键盘导航键区的根并且包含焦点,或者当前控件是键盘导航键区的根并且嵌套在另外的键盘导航键区内,这两种情况即使设置了FLAG_TOUCHSCREEN_BLOCKS_FOCUS标识,也不阻止获取焦点。

  接着返回到requestFocusNoSearch()方法中,继续向下看代码,!isLayoutValid()这个条件前面说过,控件还未设置PFLAG3_IS_LAID_OUT标识或者或者即将进行一次布局操作计算大小,这个时候设置PFLAG_WANTS_FOCUS标识。如果isLayoutValid()为true,就是控件上次绑定窗口之后至少执行过一次布局计算并且不需要另一次布局,这个情况下调用clearParentsWantFocus(),用来清除当前控件的父控件的PFLAG_WANTS_FOCUS。设置PFLAG_WANTS_FOCUS标识,是为了在布局阶段如果控件没有获取焦点,会请求获取控件焦点。
  再向下,调用handleFocusGainInternal(direction, previouslyFocusedRect)去处理请求获取焦点。最后就返回true。

handleFocusGainInternal(direction, previouslyFocusedRect)

  看下handleFocusGainInternal(direction, previouslyFocusedRect)方法代码:

    void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " requestFocus()");
        }

        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            mPrivateFlags |= PFLAG_FOCUSED;

            View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

            if (mParent != null) {
                mParent.requestChildFocus(this, this);
                updateFocusedInCluster(oldFocus, direction);
            }

            if (mAttachInfo != null) {
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }

            onFocusChanged(true, direction, previouslyFocusedRect);
            refreshDrawableState();
        }
    }

  handleFocusGainInternal的操作的条件是控件没有PFLAG_FOCUSED标识,主要做了如下:
  1、给当前控件设置PFLAG_FOCUSED,这个标识就代表控件获取了焦点。
  2、调用getRootView().findFocus()获取原来的获取焦点的控件。
  3、如果当前父控件不为null,调用父控件的requestChildFocus(this, this)方法。该步骤主要是取消原来获取焦点控件的PFLAG_FOCUSED标识,并且更新控件焦点链上的mFocused。
  4、如果当前父控件不为null,更新键盘导航键区的信息。
  5、如果控件已经绑定到窗口,则派发全局焦点改变事件。
  6、调用onFocusChanged(true, direction, previouslyFocusedRect),这个是当前控件获取焦点执行焦点改变事件。具体见下面
  7、最后调用refreshDrawableState(),该方法会通过控件的状态来设置各个Drawable的状态。该方法在Android触摸事件派发(二) View的onTouchEvent()中有讲到
看getRootView()方法:

    public View getRootView() {
        if (mAttachInfo != null) {
            final View v = mAttachInfo.mRootView;
            if (v != null) {
                return v;
            }
        }

        View parent = this;

        while (parent.mParent != null && parent.mParent instanceof View) {
            parent = (View) parent.mParent;
        }

        return parent;
    }

如果mAttachInfo不为null,则代表当前控件已经绑定了窗口。通过mAttachInfo.mRootView得到当前窗口最顶级控件,这个控件就是DecorView类型的控件,如果该值不为null,则返回该值。如果当前控件没有绑定窗口,则通过mParent属性进行往上寻找,将最终找到的控件返回。在这儿,返回的就是DecorView类型的控件。也就是调用DecorView类的findFocus()方法。这个方法在ViewGroup类里,如下:

    @Override
    public View findFocus() {
        if (DBG) {
            System.out.println("Find focus in " + this + ": flags="
                    + isFocused() + ", child=" + mFocused);
        }

        if (isFocused()) {
            return this;
        }

        if (mFocused != null) {
            return mFocused.findFocus();
        }
        return null;
    }

isFocused()方法,就是检查当前控件是否具有焦点。

    @InspectableProperty(hasAttributeId = false)
    public boolean isFocused() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0;
    }

isFocused()方法返回true,代表找到了焦点控件。就返回当前控件。如果当前控件不是获取焦点的控件,就通过成员变量mFocused的findFocus()方法来找。容器如果具有焦点,mFocused有可能就是获取焦点的子控件,也可能是包含焦点控件的子控件。所以这里也会递归调用findFocus()方法来找到获取焦点的控件。
  找到原来的获取焦点的控件,返回handleFocusGainInternal(direction, previouslyFocusedRect)方法,继续执行,如果父控件不为null,则调用mParent.requestChildFocus(this, this)方法。

    @Override
    public void requestChildFocus(View child, View focused) {
        if (DBG) {
            System.out.println(this + " requestChildFocus()");
        }
        if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
            return;
        }

        // Unfocus us, if necessary
        super.unFocus(focused);

        // We had a previous notion of who had focus. Clear it.
        if (mFocused != child) {
            if (mFocused != null) {
                mFocused.unFocus(focused);
            }

            mFocused = child;
        }
        if (mParent != null) {
            mParent.requestChildFocus(this, focused);
        }
    }

requestChildFocus()方法也是一个通过mParent向它的父控件传递requestChildFocus()方法,其中参数focused是获取焦点的控件,而child则可以是获取焦点的控件或者包含焦点的控件。首先检查控件本身的FOCUS_BLOCK_DESCENDANTS标识是否存在,如果存在,就返回,不继续执行了,这个标识就代表阻止子孙控件获取焦点的意思。接着调用父类View的unFocus(focused)方法,如果mFocused和参数child不一致,则需要调用mFocused的unFocus(focused),同时将mFocused = child。最后就通过mParent,调用父控件的requestChildFocus(this, focused)方法。
  从handleFocusGainInternal()方法可以知道,请求焦点的控件的PFLAG_FOCUSED已经设置了。那么原来已经设置了PFLAG_FOCUSED标识的控件应该取消掉该标识,并且祖先控件的mFocused如果发生变化,应该重新设置。所以handleFocusGainInternal()方法里面调用mParent.requestChildFocus(this, this)就是为了做这些的,并且通过mParent进行传递,一直传递到所有的祖先控件。
  从requestChildFocus()方法里面看到,如果mFocused 发生变化,会重新设置mFocused 的值。那么取消之前的获取焦点的控件的PFLAG_FOCUSED标识的工作,应该由unFocus()来完成了,所以看下unFocus()方法,先看下View类的unFocus(focused),再接着看下ViewGroup类的unFocus(focused)方法。

View类的unFocus(focused)

View类的unFocus(focused):

    void unFocus(View focused) {
        if (DBG) {
            System.out.println(this + " unFocus()");
        }

        clearFocusInternal(focused, false, false);
    }
    ………………
    void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
        if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
            mPrivateFlags &= ~PFLAG_FOCUSED;
            clearParentsWantFocus();

            if (propagate && mParent != null) {
                mParent.clearChildFocus(this);
            }

            onFocusChanged(false, 0, null);
            refreshDrawableState();

            if (propagate && (!refocus || !rootViewRequestFocus())) {
                notifyGlobalFocusCleared(this);
            }
        }
    }    

unFocus(focused)方法调用clearFocusInternal(focused, false, false)方法,该方法是为了让原来占用焦点的控件取消焦点。对已经获得焦点的控件执行如下:
  1、取消PFLAG_FOCUSED标识。
  2、调用clearParentsWantFocus(),清除祖先控件的PFLAG_WANTS_FOCUS标识。
  3、当前的参数propagate为false,这个参数代表是否向上祖先控件传递,假如该参数为true,并且父控件不为null,则调用父控件的clearChildFocus(this)方法。这个方法是将父控件的mFocused置为null,并且继续去执行祖先控件的clearChildFocus(this)方法。
  4、调用onFocusChanged(false, 0, null)方法,通知焦点改变了。
  5、调用refreshDrawableState()改变Drawable的显示。
  6、在参数propagate 为true,refocus 为false的情况下,会调用notifyGlobalFocusCleared(this)方法发送全局焦点改变通知。如果propagate 为true,refocus 为true,rootViewRequestFocus()的方法返回false,也会发送全局焦点改变通知。rootViewRequestFocus()方法是根控件DecorView去请求焦点,返回false也就是请求焦点失败。
  从传递过来的参数propagate值为false,refocus值为false,可知第3、6步不会执行。
  看下第4步骤的onFocusChanged(false, 0, null)方法:

   @CallSuper
    protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
            @Nullable Rect previouslyFocusedRect) {
        if (gainFocus) {
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
        } else {
            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
        }

        // Here we check whether we still need the default focus highlight, and switch it on/off.
        switchDefaultFocusHighlight();

        if (!gainFocus) {
            if (isPressed()) {
                setPressed(false);
            }
            if (hasWindowFocus()) {
                notifyFocusChangeToImeFocusController(false /* hasFocus */);
            }
            onFocusLost();
        } else if (hasWindowFocus()) {
            notifyFocusChangeToImeFocusController(true /* hasFocus */);
        }

        invalidate(true);
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnFocusChangeListener != null) {
            li.mOnFocusChangeListener.onFocusChange(this, gainFocus);
        }

        if (mAttachInfo != null) {
            mAttachInfo.mKeyDispatchState.reset(this);
        }

        if (mParent != null) {
            mParent.onDescendantUnbufferedRequested();
        }

        notifyEnterOrExitForAutoFillIfNeeded(gainFocus);
    }

  该方法组主要做如下:
  ① 根据是获取焦点还是失去焦点,处理辅助功能相关事件
  ② 打开或者关闭默认焦点高亮,对应调用switchDefaultFocusHighlight()
  ③ 如果失去焦点,如果是按压状态,取消按压状态,如果目前窗口获取窗口焦点,向输入法焦点控制器通知该控件丢失焦点。如果控件失去焦点还会调用onFocusLost(),取消控件的按压状态,并且如果现在存在长按事件消息,会取消。
  ④ 如果获得焦点,并且目前窗口获取窗口焦点,向输入法焦点控制器通知该控件获得焦点。
  ⑤ 刷新界面
  ⑥ 如果控件当前注册了焦点改变接口,现在会执行该接口回调函数onFocusChange(this, gainFocus)
  ⑦ 停止追踪和这个控件相关。这个追踪是用来KeyEvent事件的长按事件的,如果实现长按事件,在KeyEvent的ACTION_DOWN事件,记录按键的code和目标。现在如果控件的焦点改变的时候,并且记录的目标是这个控件的话,就将记录code和目标清除掉。
  ⑧调用父类控件,检查是否设置不缓存派发。这个不缓存派发,只在触摸事件中使用,如果设置了,输入系统则不会批次派发触摸事件,有了就会派发。
  ⑨ 通知自动填充服务该控件获取或失去焦点。
  clearFocusInternal(focused, false, false)方法调用的onFocusChanged()方法,参数gainFocus为false,所以是执行的失去焦点。
  先看看②步骤中的switchDefaultFocusHighlight()方法:

    private void switchDefaultFocusHighlight() {
        if (isFocused()) {
            final boolean needed = isDefaultFocusHighlightNeeded(mBackground,
                    mForegroundInfo == null ? null : mForegroundInfo.mDrawable);
            final boolean active = mDefaultFocusHighlight != null;
            if (needed && !active) {
                setDefaultFocusHighlight(getDefaultFocusHighlightDrawable());
            } else if (!needed && active) {
                // The highlight is no longer needed, so tear it down.
                setDefaultFocusHighlight(null);
            }
        }
    }

  该方法的首要条件是在控件目前是获取焦点的状态,检查是否需要默认高亮,并且默认高亮的Drawable不能为null,就是是否是active,如果需要默认高亮,但是还没有设置默认高亮的Drawable,那么就调用setDefaultFocusHighlight(getDefaultFocusHighlightDrawable())进行设置默认高亮的Drawable,这就相当于打开了默认焦点高亮,后面就可以显示了;如果不需要默认高亮,不过现在存在默认高亮的Drawable,现在调用setDefaultFocusHighlight(null)将其设置为null,这就相当于将默认高亮关闭。接着看isDefaultFocusHighlightNeeded()方法,看下什么情况下,需要默认高亮,

    @TestApi
    public boolean isDefaultFocusHighlightNeeded(Drawable background, Drawable foreground) {
        final boolean lackFocusState = (background == null || !background.isStateful()
                || !background.hasFocusStateSpecified())
                && (foreground == null || !foreground.isStateful()
                || !foreground.hasFocusStateSpecified());
        return !isInTouchMode() && getDefaultFocusHighlightEnabled() && lackFocusState
                && isAttachedToWindow() && sUseDefaultFocusHighlight;
    }

  参数background是背景Drawable,foreground是前景Drawable。Drawable类的isStateful()方法表示它的外观会随着它的状态改变,取反就是不会随着状态改变。background.hasFocusStateSpecified()表示background设置了属性state_focused,取反就是没有设置该属性。所以lackFocusState为true,表示背景Drawable不会随着状态改变或者背景Drawable没有设置state_focused属性,并且前景Drawable不会随着状态改变或者前景Drawable没有设置state_focused属性。
  getDefaultFocusHighlightEnabled()代表控件默认焦点高亮启用,它可以通过setDefaultFocusHighlightEnabled(boolean defaultFocusHighlightEnabled)方法或者xml布局文件中控件的defaultFocusHighlightEnabled属性进行设置。
  !isInTouchMode()代表不是在触摸模式下,前面解释过了。sUseDefaultFocusHighlight是系统的资源配置文件里的config_useDefaultFocusHighlight属性值。isAttachedToWindow()代表控件是绑定到窗口上了。
  上面所有方法的并集就是需要默认焦点高亮的。
  接着看看setDefaultFocusHighlight(Drawable highlight)方法:

    private void setDefaultFocusHighlight(Drawable highlight) {
        mDefaultFocusHighlight = highlight;
        mDefaultFocusHighlightSizeChanged = true;
        if (highlight != null) {
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
                mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            }
            highlight.setLayoutDirection(getLayoutDirection());
            if (highlight.isStateful()) {
                highlight.setState(getDrawableState());
            }
            if (isAttachedToWindow()) {
                highlight.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
            }
            // Set callback last, since the view may still be initializing.
            highlight.setCallback(this);
        } else if ((mViewFlags & WILL_NOT_DRAW) != 0 && mBackground == null
                && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
            mPrivateFlags |= PFLAG_SKIP_DRAW;
        }
        invalidate();
    }

该方法先将mDefaultFocusHighlight设置为参数highlight,将mDefaultFocusHighlightSizeChanged设为true,代表默认焦点高亮Drawable的面积发生了变化。
  如果参数highlight 不为null,即为设置了新的Drawable。如果之前设置了PFLAG_SKIP_DRAW标识,现在需要将它去除,因为这个标识是为了跳过该控件的绘制。下面再解释这个标识,先往下看,根据控件的布局方法来设置highlight的布局方向,highlight如果也是根据状态变化,这个时候也设置它的状态。如果控件已经绑定到窗口,根据窗口和控件和祖先控件的可见性,来设置highlight的可见性状态。再设置highlight的Callback接口。
  如果参数highlight 为null,并且背景mBackground为null,mForegroundInfo.mDrawable为null,及默认高亮、背景、前景都不需要绘制,并且这个时候也设置了WILL_NOT_DRAW标识,这个时候,就设置PFLAG_SKIP_DRAW标识。WILL_NOT_DRAW标识基本上来设置ViewGroup类型的控件,因为它没有内容,并且这个时候,也不用绘制默认高亮、背景、前景,所以绘制的时候就可以跳过该控件的绘制,直接绘制子控件。PFLAG_SKIP_DRAW标识就是为了这个目的。
  最后这两种情况都会调用invalidate(),刷新界面。
  该方法的isShown()是该控件和祖先控件中只要有一个不可见,就会返回false。

ViewGroup类的unFocus(focused)
    @Override
    void unFocus(View focused) {
        if (DBG) {
            System.out.println(this + " unFocus()");
        }
        if (mFocused == null) {
            super.unFocus(focused);
        } else {
            mFocused.unFocus(focused);
            mFocused = null;
        }
    }

  在容器控件的mFocused 为null的时候,会调用对应的View类的unFocus(View focused)方法,上面已经讲述;如果不为null,则会调用mFocused.unFocus(focused),这又会根据mFocused是ViewGroup类型还是View类型,继续执行,最后会将mFocused 设置为null。

  handleFocusGainInternal()方法执行完了mParent.requestChildFocus(this, this),需要执行updateFocusedInCluster(oldFocus, direction)方法,如下:

    private void updateFocusedInCluster(View oldFocus, @FocusDirection int direction) {
        if (oldFocus != null) {
            View oldCluster = oldFocus.findKeyboardNavigationCluster();
            View cluster = findKeyboardNavigationCluster();
            if (oldCluster != cluster) {
                // Going from one cluster to another, so save last-focused.
                // This covers cluster jumps because they are always FOCUS_DOWN
                oldFocus.setFocusedInCluster(oldCluster);
                if (!(oldFocus.mParent instanceof ViewGroup)) {
                    return;
                }
                if (direction == FOCUS_FORWARD || direction == FOCUS_BACKWARD) {
                    // This is a result of ordered navigation so consider navigation through
                    // the previous cluster "complete" and clear its last-focused memory.
                    ((ViewGroup) oldFocus.mParent).clearFocusedInCluster(oldFocus);
                } else if (oldFocus instanceof ViewGroup
                        && ((ViewGroup) oldFocus).getDescendantFocusability()
                                == ViewGroup.FOCUS_AFTER_DESCENDANTS
                        && ViewRootImpl.isViewDescendantOf(this, oldFocus)) {
                    // This means oldFocus is not focusable since it obviously has a focusable
                    // child (this). Don't restore focus to it in the future.
                    ((ViewGroup) oldFocus.mParent).clearFocusedInCluster(oldFocus);
                }
            }
        }
    }

这个方法在原来获取焦点和现在获取焦点的控件的键盘导航键区如果不同,之前键区会记录它的最后的获取焦点的控件,调用oldFocus.setFocusedInCluster(oldCluster)实现。接着在direction等于FOCUS_FORWARD或者FOCUS_BACKWARD,会执行清除旧键区记录的焦点控件。还有一种情况,也会执行清除旧键区记录的焦点控件,就是旧焦点控件是容器类型,并且该容器与子孙控件获取焦点的关系是ViewGroup.FOCUS_AFTER_DESCENDANTS(这个就是先让子孙控件获取焦点),并且切换之前的容器控件还是目前获取焦点控件的祖先控件的情况下。但是这种情况下,他们两个的键区怎么会不同呢,没想明白。
  继续看下View类的setFocusedInCluster(View cluster)和ViewGroup类的clearFocusedInCluster(View child),View类的setFocusedInCluster(View cluster)代码如下:

    private void setFocusedInCluster(View cluster) {
        if (this instanceof ViewGroup) {
            ((ViewGroup) this).mFocusedInCluster = null;
        }
        if (cluster == this) {
            return;
        }
        ViewParent parent = mParent;
        View child = this;
        while (parent instanceof ViewGroup) {
            ((ViewGroup) parent).mFocusedInCluster = child;
            if (parent == cluster) {
                break;
            }
            child = (View) parent;
            parent = parent.getParent();
        }
    }

如果当前请求控件是容器类型,则将它的mFocusedInCluster置为null,就是清除了。如果参数cluster和当前请求控件是相同对象,则退出,不向下执行。接着就通过while循环,向上寻找父控件,一直找到参数cluster为止。对于找到的父控件,设置它的成员变量mFocusedInCluster为child,这样就通过键区的根容器控件的mFocusedInCluster维持了一个链条,一直到当前控件。
  接着看ViewGroup类的clearFocusedInCluster(View child),如下:

    void clearFocusedInCluster(View child) {
        if (mFocusedInCluster != child) {
            return;
        }
        clearFocusedInCluster();
    }

    /**
     * Removes the focusedInCluster chain from this up to the cluster containing it.
     */
    void clearFocusedInCluster() {
        View top = findKeyboardNavigationCluster();
        ViewParent parent = this;
        do {
            ((ViewGroup) parent).mFocusedInCluster = null;
            if (parent == top) {
                break;
            }
            parent = parent.getParent();
        } while (parent instanceof ViewGroup);
    }

可以看到这个的操作是和上面的setFocusedInCluster(View cluster)的操作是相反的,就是取消键区的根容器控件的mFocusedInCluster维持了一个链条,将成员变量mFocusedInCluster都设置为null。
  以上就是控件获取焦点的主要步骤和方法,里面的细节还是挺多的,需要仔细理解、加深记忆。

你可能感兴趣的:(android)