View.requestFocus聚焦源码分析

1,619 阅读8分钟

View.requestFocus源码分析

我们需要某个控件View进行聚焦,一般会主动调用该控件的requestFocus方法。(本文基于API 27源码进行分析)

<View.java>
public final boolean requestFocus() {
    // 默认使用FOCUS_DOWN进行聚焦
    return requestFocus(View.FOCUS_DOWN);
}

一步步往下跟,requestFocus接着会走到requestFocusNoSearch方法中,看方法名就能理解,因为我们是直接requestFocus,意图就是指定某个View获得焦点,所以不需要走寻焦机制:

<View.java>
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
    // need to be focusable
    // 如果该view设置的focusable = false,直接返回
    if ((mViewFlags & FOCUSABLE) != FOCUSABLE
            || (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
        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;// 如果该view设置的focusableInTouchMode = false,直接返回
    }

    // need to not have any parents blocking us
    if (hasAncestorThatBlocksDescendantFocus()) {
        return false;// 如果parent中设置了FOCUS_BLOCK_DESCENDANTS,直接返回
    }

    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true;// 聚焦成功
}
  • 首先第一步会判断当前的View的focusable状态,如果是false,说明该View并不能获取焦点,也就没有必要再往下走了。
  • 接着会判断是否触摸模式,在触摸模式下,如果focusableInTouchMode是false的话,也说明该View通过触摸并不能获取焦点,也没必要往下走了。
  • 继续看下面一个判断hasAncestorThatBlocksDescendantFocus()方法:
<View.java>
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;
}

这个方法也就是遍历parent父View查找是否有设置FOCUS_BLOCK_DESCENDANTS,如果设置了,说明父View把焦点传递给拦截了,并不希望自己获得焦点,因此该方法会返回true。回到requestFocusNoSearch中,也就直接return不往下走了。

经过一系列的条件判断,如果可聚焦,并且父View未拦截焦点,最终走到核心方法handleFocusGainInternal中:

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

    if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
        mPrivateFlags |= PFLAG_FOCUSED;// 更新标记位(isFocused判断依据)
        // 当前状态下的焦点
        View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

        if (mParent != null) {
            mParent.requestChildFocus(this, this);// 清除当前焦点,将mFocus变量更新值为当前期望聚焦的view
            updateFocusedInCluster(oldFocus, direction);// android高版本新增的方法,此方法和键盘相关,在此不作重点关注
        }

        if (mAttachInfo != null) {
            // 通知ViewTreeObserver焦点变化
            mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
        }

        onFocusChanged(true, direction, previouslyFocusedRect);// 通知焦点变化回调
        refreshDrawableState();// 当前view聚焦,刷新drawable状态
    }
}

这个方法中,首先会更新当前View的标记位mPrivateFlags记录自己的isFocused状态,接着通过rootView查找到当前的焦点赋值给oldFocus,然后调用parent的requestChildFocus方法告知parent自己当前聚焦啦。

<ViewGroup.java>
@Override
public void requestChildFocus(View child, View focused) {
    if (DBG) {
        System.out.println(this + " requestChildFocus()");
    }
    // 再次判断是否设置了FOCUS_BLOCK_DESCENDANTS
    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);
    }
}

在这个方法里,focused这个参数其实没用到(unFocus作为形参传入,其实里面也根本没用到该参数),在上面第一次调用时传入的都是this,这个focused实际上就是直接的焦点,child在第一次调用时也是直接焦点,child == focused,但是通过mParent.requestChildFocus(this, focused);后,child这个参数就变成了直接焦点的父View,一层一层往上进行调用以此类推,这里要重点区分下两个参数含义。下面是官方对该参数的注释:

<ViewParent.java>
    /**
     * Called when a child of this parent wants focus
     *
     * @param child The child of this ViewParent that wants focus. This view
     *        will contain the focused view. It is not necessarily the view that
     *        actually has focus.
     * @param focused The view that is a descendant of child that actually has
     *        focus
     */
    public void requestChildFocus(View child, View focused);

在每个ViewGroup中都有一个mFocus变量,该变量的作用就是保存着当前ViewGroup下的焦点,并非直接焦点。(官方对这个变量含义的注释:The view contained within this ViewGroup that has or contains focus.) 接着回到这个requestChildFocus(View child, View focused)方法接着往下看,具体逻辑是:

  • 再次判断是否设置了FOCUS_BLOCK_DESCENDANTS,如果拦截则不继续往下走。
  • 一般情况下,当前焦点mFocused都和我们期望聚焦的view并非同一个,则进入分支调用mFocused.unFocus(focused)
<View.java>
void unFocus(View focused) {
    if (DBG) {
        System.out.println(this + " unFocus()");
    }
    clearFocusInternal(focused, false, false);
}

// 最终调用这个方法
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
    // 其实没用到focused这个参数
    if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
        mPrivateFlags &= ~PFLAG_FOCUSED;

        if (propagate && mParent != null) {
            mParent.clearChildFocus(this);// 通知parent清除自己(当前的焦点)的mFocus值,因为焦点已经不在该View树节点下
        }

        onFocusChanged(false, 0, null);// 回调焦点状态变更的通知
        refreshDrawableState();// 刷新失去焦点后的drawable状态

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

clearFocusInternal这个方法是mFocus进行调用的,也就是对当前的焦点所在的View进行清除焦点状态处理,主要做了下面几件事:

  • 通知parent调用clearChildFocus将mFocus变量置null,因为焦点已经不在该View树节点下。
  • 回调自身的焦点状态变更的通知,我们通常所设置的setOnFocusChangeListener的监听就是在这里面进行触发回调的。
  • 由于第1步中清除了自己的焦点状态,失焦之后自然需要刷新视图状态,这里会调用refreshDrawableState进行drawableState的刷新,也就是我们通常在xml中设置的selector状态属性。

注意一点:这里面的focused参数其实根本没用到,但是这个focused才是真正最直接的焦点。

清除了当前焦点之后,回到parent的requestChildFocus中,将我们期望聚焦的child赋值给mFocused,前面说过这个mFocus变量就是保存着当前的焦点,走到这步,我们调用View.requestFocus已经成功将焦点从oldFocus转移到newFocus上了。

<ViewGroup.java>
ViewGroup.requestChildFocus
...
// 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);
}
...

接下去还会再次通过parent一层一层的告诉父View,当前焦点在我这。也就是说某一个子View如果聚焦了,它会将自己赋值给parent的mFocus变量,这样下次查找焦点,就可以通过顶层的parent一级一级通过mFocus变量进行findFocus查找到最下层的直接焦点。这里展开一下findFocus方法就很明白了:

<ViewGroup.java>
@Override
public View findFocus() {
    if (DBG) {
        System.out.println("Find focus in " + this + ": flags="
                + isFocused() + ", child=" + mFocused);
    }
    // 如果当前isFocused了,说明我自己已经是焦点了,直接返回自己
    if (isFocused()) {
        return this;
    }
    // mFocus不为null,说明焦点在这个mFocus的View树下
    if (mFocused != null) {
        return mFocused.findFocus();
    }
    return null;
}

<View.java>
public View findFocus() {
    // 当遍历到直接子View之后就是根据标志位进行判断
    return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
}

举个例子:A包含B,B包含C,A和B都是ViewGroup,C是直接View,A的mFocus是B,B的mFocus是C,注意一点,这里面只是说mFocus != null,并不是说A和B的isFocused也是true的,要区别开hasFocusisFocused。这里一层层的往上走,最终会走到ViewRootImpl的requestChildFocus进行UI重绘。

<ViewGroup.java>
public boolean hasFocus() {
    return (mPrivateFlags & PFLAG_FOCUSED) != 0 || mFocused != null;
}

<ViewRootImpl.java>
@Override
public void requestChildFocus(View child, View focused) {
    if (DEBUG_INPUT_RESIZE) {
        Log.v(mTag, "Request child focus: focus now " + focused);
    }
    checkThread();
    scheduleTraversals();// UI重绘
}

回到上面,handleFocusGainInternal中的mPrivateFlags |= PFLAG_FOCUSED;这里修改了标记位,实际上isFocused就是通过这个标记位进行判断的。

<View.java>
@ViewDebug.ExportedProperty(category = "focus")
public boolean isFocused() {
    return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}

至此,View.requestFocus的调用流程结束,焦点已经从之前的oldFocus转移到新的newFocus上了。接下来,我们继续分析下ViewGroup.requestFocus方法:

<ViewGroup.java>
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    if (DBG) {
        System.out.println(this + " ViewGroup.requestFocus direction="
                + direction);
    }
    int descendantFocusability = getDescendantFocusability();
    // 主要还是看ViewGroup设置的焦点拦截模式
    switch (descendantFocusability) {
        case FOCUS_BLOCK_DESCENDANTS:// 拦截掉了焦点,直接调用super的逻辑在自己中requestFocus
            return super.requestFocus(direction, previouslyFocusedRect);
        case FOCUS_BEFORE_DESCENDANTS: {// 首先调用super的逻辑在自己中requestFocus,如果自己请求焦点失败再遍历子View进行requestFocus
            final boolean took = super.requestFocus(direction, previouslyFocusedRect);
            return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
        }
        case FOCUS_AFTER_DESCENDANTS: {// 与FOCUS_BEFORE_DESCENDANTS相反,先遍历子View进行requestFocus,如果子View都请求焦点失败后再调用super的逻辑在自己中requestFocus
            final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
            return took ? took : super.requestFocus(direction, previouslyFocusedRect);
        }
        default:
            throw new IllegalStateException("descendant focusability must be "
                    + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
                    + "but is " + descendantFocusability);
    }
}

这里面的逻辑还是很清晰的,主要判断依据就是焦点拦截模式descendantFocusability

  • FOCUS_BLOCK_DESCENDANTS:自身拦截掉焦点,直接对自己进行requestFocus调用去请求焦点
  • FOCUS_BEFORE_DESCENDANTS:自身优先子View获得焦点,先对自己进行requestFocus调用去请求焦点,如果失败再遍历子View让子View进行聚焦
  • FOCUS_AFTER_DESCENDANTS:先遍历子View让子View进行聚焦,如果子View都没有聚焦,则再对自己进行requestFocus调用去请求焦点

下面我们看下onRequestFocusInDescendants里做了些什么:

<ViewGroup.java>
protected boolean onRequestFocusInDescendants(int direction,
        Rect previouslyFocusedRect) {
    int index;
    int increment;
    int end;
    int count = mChildrenCount;
    if ((direction & FOCUS_FORWARD) != 0) {// 从前往后遍历
        index = 0;
        increment = 1;
        end = count;
    } else {// 从后往前遍历
        index = count - 1;
        increment = -1;
        end = -1;
    }
    final View[] children = mChildren;// mChildren数组中保存了所有的childView
    for (int i = index; i != end; i += increment) {
        View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {// 遍历子View,并且View可见
            if (child.requestFocus(direction, previouslyFocusedRect)) {// 该子View请求焦点
                return true;// 请求焦点成功,直接返回
            }
        }
    }
    return false;
}

onRequestFocusInDescendants主要功能就是遍历该ViewGroup下所有子View,然后对可见的子View调用requestFocus,如果请求焦点成功,则直接返回true,至此,ViewGroup.requestFocus也处理完毕了。