问题

做过 Android TV 开发的同学基本上都碰上过焦点处理的问题,主要分为以下几类:

  • 指定第一个获取焦点的View
  • 指定 Next Focus View
  • 对于 RecyclerView 、Fragment 各种嵌套,如何更优雅的进行焦点记忆?
  • 界面上有焦点的View失焦后,系统是怎么进行焦点转移的?

第一点和第二点相对来说比较简单,利用 xml 配置可以轻松解决,但对于第三和第四如果通过业务逻辑代码去写的话会有很多问题,比如与业务强绑定,不好复用,与逻辑代码混杂在一起,不够优雅,可读性差等,那对于这些焦点问题,我们首先需要弄明白系统的焦点处理流程是怎样的?那我们通过 xml 属性和内部方法来进行讲解和说明

xml属性

  • focusable 决定是否可以获取焦点
  • focusableInTouchMode 决定触屏模式下是否可获取焦点
  • nextFocusUp 指定按键向上时获取焦点的 View ID
  • nextFocusDown 指定按键向下时获取焦点的 View ID
  • nextFocusRight|nextFocusLeft 向右、向左按键指定 View ID
  • nextFocusFoward 回车键获取焦点的 View

类方法

  • hasFocus() 当前 View|ViewGroup 是否有焦点或者子View 是否有焦点
  • isFocused() 当前 View 是否有焦点
  • requestFocus() 请求获取焦点
  • clearFocus() 清除焦点,同时会向上清除链路上涉及的View的焦点状态
  • setFocusable() 参考 focusable 说明
  • setFocusableInTouchMode() 参考 focusableInTouchMode 说明

hasFocus()

首先是 hasFocus() 方法,它在 View 和 ViewGroup 中的定义分别如下:

// View.java
/**
 * Returns true if this view has focus itself, or is the ancestor of the
 * view that has focus.
 *
 * @return True if this view has or contains focus, false otherwise.
 */
@ViewDebug.ExportedProperty(category = "focus")
public boolean hasFocus() {
    return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}

// ViewGroup.java
/**
 * Returns true if this view has or contains focus
 *
 * @return true if this view has or contains focus
 */
@Override
public boolean hasFocus() {
    return (mPrivateFlags & PFLAG_FOCUSED) != 0 || mFocused != null;
}

PFLAG_FOCUSED 变量就是用来标识这个 View 是否具有焦点 mFocused 是 ViewGroup 特有的,用于记录该 Group 下获取到焦点的 View

通过看 View 和 ViewGroup 的 hasFocus() 方法,涉及的含义是有差异的。

isFocused()

isFocused() 函数只有在 View 中定义,获取当前 View 本身是否有焦点,其定义如下:

/**
 * Returns true if this view has focus
 *
 * @return True if this view has focus, false otherwise.
 */
@ViewDebug.ExportedProperty(category = "focus")
public boolean isFocused() {
    return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}

会发现和 View 的 hasFocus() 一致,ViewGroup 中表示本身是否有焦点,和 hasFocus 有区别

requestFocus()

requestFocus() 在 View 中的定义如下:

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 ((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;
    }
    // need to not have any parents blocking us
    if (hasAncestorThatBlocksDescendantFocus()) {
        return false;
    }
    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true;
}

requestFocus 返回 boolean 值,看是否成功获取到了焦点,可以看到 xml 属性中的配置在此处有所体现,获取焦点主要还得看 handleFocusGainInternal 方法。这个等下再看,我们来看下 ViewGroup 的 requestFocus 又是怎样?

requestFocus() 方法在 ViewGroup 中的定义如下:

public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    if (DBG) {
        System.out.println(this + " ViewGroup.requestFocus direction="
                + direction);
    }
    int descendantFocusability = getDescendantFocusability();
    switch (descendantFocusability) {
        case FOCUS_BLOCK_DESCENDANTS:
            return super.requestFocus(direction, previouslyFocusedRect);
        case FOCUS_BEFORE_DESCENDANTS: {
            final boolean took = super.requestFocus(direction, previouslyFocusedRect);
            return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
        }
        case FOCUS_AFTER_DESCENDANTS: {
            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);
    }
}

ViewGroup 重写了 View 的 requestFocus() 方法,它会根据 descendantFocusability 的值的不同进行不同的处理,是不是发现 xml 的很多属性是怎么起作用的了,descendantFocusability 有三个可能的值:

  • FOCUS_BLOCK_DESCENDANTS 对焦点进行拦截,不给子 View 进行请求焦点的机会
  • FOCUS_BEFORE_DESCENDANTS 先于子View进行焦点获取,只有自身没有获取到焦点才让子 View 去获取焦点
  • FOCUS_AFTER_DESCENDANTS 先让子 View 获取焦点,子 View 未获取到焦点时,自身再去获取焦点

descendantFocusability 属性的默认值为 FOCUS_BEFORE_DESCENDANTS

当为 FOCUS_BLOCK_DESCENDANTS 时,通过调用父类 requestFocus() 方法为自己请求焦点; 当为 FOCUS_BEFORE_DESCENDANTS 时,先为自己请求焦点,如果未能成功,那么再调用 onRequestFocusInDescendants() 方法,这个方法从名字就能知道是为子 View 请求焦点,或者说是把请求焦点的任务交给了后代; 当为 FOCUS_BEFORE_DESCENDANTS 时,与 FOCUS_BEFORE_DESCENDANTS 逻辑刚好相反。

因此,对于 ViewGroup 来说,这个方法的返回值和 View 有点不同:如果返回 true,说明这个 ViewGroup 获取了焦点或者它的某个后代成功地获取了焦点;否则,这个 ViewGroup 未能成功获取焦点并且它的子 View 也未能成功获取焦点。

现在我们看下 onRequestFocusInDescendants() 的定义:

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;
    for (int i = index; i != end; i += increment) {
        View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
            if (child.requestFocus(direction, previouslyFocusedRect)) {
                return true;
            }
        }
    }
    return false;
}

这个方法会遍历所有的子 View,寻找第一个自身或者后代能够成功获取焦点的 View,如果找到了,停止后续的遍历并返回 true;如果不存在这样的 View,返回 false。

unFocus()

这个方法是内部方法,应用程序不能直接调用,它在 View 和 ViewGroup 中有不同的定义:

// 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) {
    if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
        mPrivateFlags &= ~PFLAG_FOCUSED;
        if (propagate && mParent != null) {
            mParent.clearChildFocus(this);
        }
        onFocusChanged(false, 0, null);
        refreshDrawableState();
        if (propagate && (!refocus || !rootViewRequestFocus())) {
            notifyGlobalFocusCleared(this);
        }
    }
}

// ViewGroup.java
@Override
void unFocus(View focused) {
    if (DBG) {
        System.out.println(this + " unFocus()");
    }
    if (mFocused == null) {
        super.unFocus(focused);
    } else {
        mFocused.unFocus(focused);
        mFocused = null;
    }
}
  • View 中通过调用 clearFocusInternal() 方法清除 PFLAG_FOCUSED 标识,实现清除焦点
  • ViewGroup 中相对更复杂,如果 mFocused 是 null 则直接调用父类 unFocus 方法,否则执行 mFocused 的 unFocus 方法,将焦点路径上的所有 mFocused 清空,并将真实获取焦点的View 清除焦点

clearFocus()

这个方法只在 View 中有定义:

public void clearFocus() {
    if (DBG) {
        System.out.println(this + " clearFocus()");
    }
    clearFocusInternal(null, true, true);
}

它也会调用 clearFocusInternal() 方法,只不过传入的参数和 unFocus() 中的不同。其作用是如果此 View 有焦点的话,清除自身的焦点并将焦点路径删除。

焦点路径

我们 DecorView 是一个窗口的根节点,所有的 View 都在以 DecorView 为根的一颗树形结构上,而一个窗口同时仅能最多一个 View 获取到焦点,那么从 DecorView 到拥有焦点的 View 经过的路径称为焦点路径。

而在每次焦点重新确定后,系统都会建立这样一条焦点路径,这个就是在 ViewGroup 中通过 mFocused 变量来进行标识这条路径。

一个窗口中只有一个焦点视图,因此焦路径也只有一条,而焦点视图是会发生变化的,所以可以推测,系统在建立新的焦点路径时,会将原来的那条焦点路径删除。

现在我们来分析系统建立和删除焦点路径的过程。我们知道,View 成功获取焦点是在 handleFocusGainInternal() 中进行的,因此我们可以以这个方法为切入点追踪系统建立焦点路径的过程。handleFocusGainInternal() 定义如下:

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();
    }
}

将这个 View 的 PFLAG_FOCUSED 标志位设为 1,也就是让其成功地获取了焦点; 然后调用 mParent 的 requestChildFocus() 方法。这个方法有两个参数,第一个参数传入的是调用 mParent 该方法的那个子 View,第二个参数表示具有焦点的那个 View。 requestChildFocus 这个方法的定义在 ViewGroup 中:

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);
    }
}

这个方法的逻辑很清晰:

  • super.unFocus() 清除自身焦点
  • 清理旧焦点 View focus 状态,并将 mFocused 重新赋值
  • 向上递归调用 mParent 的 requestChildFocus 方法,完成焦点路径的切换

通过以上流程,即可实现将旧的焦点路径清除,并建立一条新的焦点路径

刚开始焦点是如何确定的?

我们知道 View 需要获取到焦点,就一定会调用 handleFocusGainInternal() 方法,那我们直接在该方法中加入断点,看他的调用栈是如何的?

android-focus-gain

根据方法栈我们可以一直追踪到 ViewRootImpl#focusableViewAvailable(),是这个方法触发了焦点的确定过程:

@Override
public void focusableViewAvailable(View v) {
    checkThread();
    if (mView != null) {
        if (!mView.hasFocus()) {
            if (sAlwaysAssignFocus) {
                v.requestFocus();
            }
        } else {
            // the one case where will transfer focus away from the current one
            // is if the current view is a view group that prefers to give focus
            // to its children first AND the view is a descendant of it.
            View focused = mView.findFocus();
            if (focused instanceof ViewGroup) {
                ViewGroup group = (ViewGroup) focused;
                if (group.getDescendantFocusability() == ViewGroup.FOCUS_AFTER_DESCENDANTS
                        && isViewDescendantOf(v, focused)) {
                    v.requestFocus();
                }
            }
        }
    }
}

可以看到只是简单的调用了 v 的 requestFocus() 方法去获取焦点,v 的实际类型是 DecorView,这样是不是所有的流程都清晰了?

焦点移动

上面讲到的是焦点获取的流程,在系统窗口失焦或者主动调用 requestFocus 时可以参考以上流程,那么当按键(上下左右)触发时,又是怎么确定下一个焦点的呢?这个时候可以看 ViewRootImpl 的 processKeyEvent() 方法中 performFocusNavigation 方法的调用:

private boolean performFocusNavigation(KeyEvent event) {
    int direction = 0;
    switch (event.getKeyCode()) {
        case KeyEvent.KEYCODE_DPAD_LEFT:
            if (event.hasNoModifiers()) {
                direction = View.FOCUS_LEFT;
            }
            break;
        case KeyEvent.KEYCODE_DPAD_RIGHT:
            if (event.hasNoModifiers()) {
                direction = View.FOCUS_RIGHT;
            }
            break;
        case KeyEvent.KEYCODE_DPAD_UP:
            if (event.hasNoModifiers()) {
                direction = View.FOCUS_UP;
            }
            break;
        case KeyEvent.KEYCODE_DPAD_DOWN:
            if (event.hasNoModifiers()) {
                direction = View.FOCUS_DOWN;
            }
            break;
        case KeyEvent.KEYCODE_TAB:
            if (event.hasNoModifiers()) {
                direction = View.FOCUS_FORWARD;
            } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
                direction = View.FOCUS_BACKWARD;
            }
            break;
    }
    if (direction != 0) {
        View focused = mView.findFocus();
        if (focused != null) {
            View v = focused.focusSearch(direction);
            if (v != null && v != focused) {
                // do the math the get the interesting rect
                // of previous focused into the coord system of
                // newly focused view
                focused.getFocusedRect(mTempRect);
                if (mView instanceof ViewGroup) {
                    ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                            focused, mTempRect);
                    ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                            v, mTempRect);
                }
                if (v.requestFocus(direction, mTempRect)) {
                    playSoundEffect(SoundEffectConstants
                            .getContantForFocusDirection(direction));
                    return true;
                }
            }

            // Give the focused view a last chance to handle the dpad key.
            if (mView.dispatchUnhandledMove(focused, direction)) {
                return true;
            }
        } else {
            if (mView.restoreDefaultFocus()) {
                return true;
            }
        }
    }
    return false;
}

主要做了三个步骤:

  • 如果View没有处理按键, 把上下左右tab等按键转换成对应方向
  • 在当前焦点 View 上通过 focusSearch 方法查找对应方向的下一个View
  • 查找到的 View 调用 requestFocus ,因此主要的流程在 focusSearch

focusSearch()

View 的 focusSearch 方法:

public View focusSearch(@FocusRealDirection int direction) {
    if (mParent != null) {
        return mParent.focusSearch(this, direction);
    } else {
        return null;
    }
}

ViewGroup 的 focusSearch 方法:

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;
}

ViewRootImpl 的 focusSearch 方法:

@Override
public View focusSearch(View focused, int direction) {
    checkThread();
    if (!(mView instanceof ViewGroup)) {
        return null;
    }
    return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);
}

可以发现我们可以通过重写 focusSearch 方法来改变系统默认的焦点获取顺序

FocusFinder 工具类查找焦点

通过以上代码会发现焦点查找都是通过 FocusFinder 工具类进行的,我们来看下核心的 findNextFocus 方法:

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
    View next = null;
    ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
    if (focused != null) {
        next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
    }
    if (next != null) {
        return next;
    }
    ArrayList<View> focusables = mTempList;
    try {
        focusables.clear();
        effectiveRoot.addFocusables(focusables, direction);
        if (!focusables.isEmpty()) {
            next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
        }
    } finally {
        focusables.clear();
    }
    return next;
}

核心步骤也就两步:

  • findNextUserSpecifiedFocus 查询是否配置 nextFocusXXX ,有的话直接返回
  • effectiveRoot 获取 focusables 列表,根据方向在 focusables 列表中查找最近的 View

总结

以上讲述了焦点是如何确定的以及如何转移的,那回到我们一开始的问题,我们该如何更优雅的去处理失焦和焦点记忆问题?

  • 重写 ViewGroup requestChildFocus 方法记录最后获取焦点 View,实现焦点记忆
  • 重写 ViewGroup focusSearch 方法,实现焦点转移顺序修改
  • 重写 addFocusables 方法,处理失焦问题

提供一个重写的 FrameLayout 示例:

public class MFrameLayout extends FrameLayout {

    private static final String TAG = "MFrameLayout";

    private IFocusSearchCallback mCallback;
    private View mLastFocusView = null;

    public MFrameLayout(@NonNull Context context) {
        super(context);
    }

    public MFrameLayout(@NonNull Context context,
                        @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
                        int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public MFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
                        int defStyleAttr,
                        int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public void requestChildFocus(View child, View focused) {
        if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
            return;
        }

        super.requestChildFocus(child, focused);
        mLastFocusView = child.findFocus();
    }

    @Override
    public View focusSearch(View focused, int direction) {
        // 先在本ViewGroup内部找,如果找到,则直接返回
        View findChildView = FocusFinder.getInstance().findNextFocus(this, focused, direction);
        if (findChildView != null) {
            return findChildView;
        }
        View view = super.focusSearch(focused, direction);

        if (mCallback != null && (view == null || !MyViewUtils.hasView(view, this))) {
            view = mCallback.searchFailed(focused, view, direction);
        }
        return view;
    }

    @Override
    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
        // 当前无焦点的情况有两种
        // 1.之前获取到过焦点,然后焦点移出,此时需要处理焦点记忆问题
        // 2.一次都没有获取过焦点,需要指定第一个获取焦点的View,否则走默认系统行为
        if (!hasFocus()) {
            // step 1 焦点记忆
            if (mLastFocusView != null && MyViewUtils.isViewReallyVisible(mLastFocusView)) {
                mLastFocusView.addFocusables(views, direction, focusableMode);
            } else if (mCallback != null) {
                // step 2 获取第一个获取焦点的子View
                View v = mCallback.firstFocusView();
                if (v != null && v.getVisibility() == VISIBLE) {
                    v.addFocusables(views, direction, focusableMode);
                } else {
                    super.addFocusables(views, direction, focusableMode);
                }
            }
        } else {
            super.addFocusables(views, direction, focusableMode);
        }
    }

    public void setFocusSearchCallback(IFocusSearchCallback callback) {
        this.mCallback = callback;
    }
}

参考:

http://liwenkun.me/2018/04/06/android-focus/