Android焦点流程梳理

892 阅读8分钟

前言

最近在看一些焦点处理的问题,认真处理起来发现不跟着源码自己走一遍焦点相关的流程,对于问题的分析上会比较困难。所以本文主要对焦点流程进行一次梳理,在处理类似问题时也可以作为手册阅读。

起源

一切都要从ViewRootImpl收到输入事件开始

// ViewRootImpl.ViewPostImeInputStage.java
@Override
protected int onProcess(QueuedInputEvent q) {
    if (q.mEvent instanceof KeyEvent) {
        return processKeyEvent(q); // KeyEvent会在此处处理
    } else {
        final int source = q.mEvent.getSource();
        if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
            return processPointerEvent(q);
        } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
            return processTrackballEvent(q);
        } else {
            return processGenericMotionEvent(q);
        }
    }
}

private int processKeyEvent(QueuedInputEvent q) {
    final KeyEvent event = (KeyEvent)q.mEvent;

    。。。

    // 1. KeyEvent事件分发
    if (mView.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }

    。。。

    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        if (groupNavigationDirection != 0) {
            if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                return FINISH_HANDLED;
            }
        } else {
            // 2. ACTION_DOWN时,寻找下一个获取焦点的View
            if (performFocusNavigation(event)) {
                return FINISH_HANDLED;
            }
        }
    }
    return FORWARD;
}

焦点流程主要分成两部分问题:

  • KeyEvent事件分发,寻找当前焦点需要消费事件的子View,并消费事件,见注释1处
  • 若事件没有消费,而且当前是KeyEvent.ACTION_DOWN时,需要寻找下一个焦点,见注释2处

寻找下一个焦点

// ViewRootImpl.ViewPostImeInputStage.java
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) {
        // 1. 获取目前拥有焦点的View
        View focused = mView.findFocus();
        if (focused != null) {
            // 2. 查找到下一个获取焦点的View
            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);
                }
                // 3. 让View获取焦点
                if (v.requestFocus(direction, mTempRect)) {
                    boolean isFastScrolling = event.getRepeatCount() > 0;
                    playSoundEffect(
                            SoundEffectConstants.getConstantForFocusDirection(direction,
                                    isFastScrolling));
                    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;
}

大概就是3步:

  1. 通过findFocus获取到当前拥有焦点的View
  2. 通过focusSearch获取到下一个获取焦点的View
  3. 让下一个获取焦点的View获取焦点

大概流程

这里大概流程如下 image.png

按键事件分发

dispatchKeyEvent

KeyEvent事件分发与触摸事件有类似,都是从顶层的DecorView开始寻找消费事件的子View。不同的是,它没有过多复杂的机制,譬如事件拦截。

ViewGroup#dispatchKeyEvent

// ViewGroup.java
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    。。。

    // 1. 如果父View自己拥有焦点,则判断自己是否消费事件
    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    // 2. 如果该父View包含拥有焦点的子View,将事件分发到对应子View方向    
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
            == PFLAG_HAS_BOUNDS) {
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }

    。。。
    return false;
}

ViewGroup#dispatchKeyEvent分两种情况:

  • 注释1,如果父View自己拥有焦点,则判断自己是否消费事件,实际是调用了父类View中定义的dispatchKeyEvent判断。
  • 注释2,如果父View当前包含了拥有焦点的子View,则将事件往拥有焦点的子View方向传递

也就是说,KeyEvent事件分发只会往拥有焦点的子View方向传递。也就有以下两种情况:

image.png

ps:红色部分是事件传递的方向。

View#dispatchKeyEvent

// View.java
public boolean dispatchKeyEvent(KeyEvent event) {
    。。。

    // Give any attached key listener a first crack at the event.
    //noinspection SimplifiableIfStatement
    ListenerInfo li = mListenerInfo;
    // 1
    if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        return true;
    }

    // 2
    if (event.dispatch(this, mAttachInfo != null
            ? mAttachInfo.mKeyDispatchState : null, this)) {
        return true;
    }

    。。。
    return false;
}

View#dispatchKeyEvent就与dispatchTouchEvent类似了,第一步

  • 判断OnKeyListener是否为null
  • 判断OnKeyListener.onKey是否返回true
  • View是否是Enable状态

三者成立则认为View消费事件,否则通过View#onKeyDown或者View#onKeyUp判断。事件的调用通过KeyEvent#dispatch,也就是注释2。

ps:与Touch事件类似,DecorView会先将事件传递给Activity,然后经过PhoneWindow回传给DecorView进行事件分发

获取当前拥有焦点的View

findFocus

findFocus的作用就是获取到当前拥有焦点的View,View的实现就是通过判断mPrivateFlags来确定自己是否拥有焦点。如果自己拥有焦点则返回自己。

// View.java
public View findFocus() {
    return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
}

ViewGroup的实现会在View的基础上扩展:

  • 如果自己拥有焦点则返回自己。
  • 如果自己包含了拥有焦点的子View,则往拥有焦点的子View方向寻找。
// ViewGroup.java
@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;
}

也就是下图中,红色箭头表示findFocus的调用方向,红色区域表示当前拥有焦点的View

image.png

下一个获取焦点的View

focusSearch

focusSearch寻找下一个获取焦点的View,它是以当前拥有焦点的View作为起点,往上传递。View的实现中,会调用父View的focusSearch,将自己也就是拥有焦点的View传递上去,同时携带当前需要移动的方向direction

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

ViewGroup中,会分为两种情况:

  • 没有到达根视图,那么与View相同直接传递上去。
  • 如果到达根视图,则会通过FocusFinder这一单例来寻找下一个获取焦点的View。ps:这里的根视图可以理解成是View树的最上层。
// ViewGroup.java
@Override
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;
}

ps:FocusFinder.getInstance().findNextFocus(this, focused, direction)中的this的意思是以当前View作为起点寻找下一个获取焦点的View。

方法的调用方向如下图红色部分:

image.png

焦点搜索控制技巧

可能会有这样一个需求:使用自定义RecyclerView做一个Tabbar效果时,不想在移动到最左或者最右时,焦点移出RecyclerView,那么可以从focusSearch入手,拦截它的搜索过程

override fun focusSearch(focused: View?, direction: Int): View? {
    if (focused == null || layoutManager == null || adapter == null || adapter?.itemCount == 0)
        return super.focusSearch(focused, direction)

    val view = super.focusSearch(focused, direction)

    if (view != null) {
        //  findContainingItemView获取到该view所在的父View,如果不在RecyclerView内则返回null
        val nextFocusItemView = findContainingItemView(view)
        //  为null证明焦点已经移出了RecyclerView
        if (nextFocusItemView == null) {    
            //  左右限制移出RecyclerView
            if (direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT) {
                return focused
            }
        }
    }

    return view
}

这里如果是左右移动到边界,则直接返回当前拥有焦点的View,使焦点还在当前View上,不会被移动到别处。

FocusFinder

// FocusFinder.java
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
    View next = null;
    ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
    if (focused != null) {
        // 1. 获取开发者指定的下一个获取焦点的View
        next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
    }
    if (next != null) {
        return next;
    }
    ArrayList<View> focusables = mTempList;
    try {
        focusables.clear();
        // 2. 通过遍历子View,可以获取焦点的备选View放入focusables集合中
        effectiveRoot.addFocusables(focusables, direction);
        if (!focusables.isEmpty()) {
            // 3. 通过可以获取焦点的备选view中获取到下一个获取焦点的View
            next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
        }
    } finally {
        focusables.clear();
    }
    return next;
}

FocusFinder.getInstance().findNextFocus中主要做了以下3件事:

  • 尝试获取用户在xml中指定的下一个获取焦点的View
  • 通过遍历子View,将可以**获取到下一个焦点的子View加入到备选集合focusables**中
  • 通过上述的备选集合focusables筛选出下一个获取焦点的View

用户在XML指定下一个获取焦点的View

用户可以在XML中指定不同方向下,基于该View的下一个获取焦点的View。

android:nextFocusLeft="@id/xxx"
android:nextFocusRight="@id/xxx"
android:nextFocusDown="@id/xxx"
android:nextFocusUp="@id/xxx"

获取焦点获取备选集合

如果用户未指定下一个获取焦点的View,则会通过备选集合的方式筛选出下一个获取焦点的View。首先需要获取到这个备选集合。

  • 先来看看View的实现:

    // View.java
    public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
        addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL);
    }
    
    public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
            @FocusableMode int focusableMode) {
        if (views == null) {
            return;
        }
        if (!canTakeFocus()) {
            return;
        }
        if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
                && !isFocusableInTouchMode()) {
            return;
        }
        views.add(this);
    }
    

    这里比较简单,如果符合获取焦点的资格,就会将自己添加到集合当中。ps:views即为获取焦点的备选集合。

  • 再来看看ViewGroup的实现:

// ViewGroup.java
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
    final int focusableCount = views.size();

    final int descendantFocusability = getDescendantFocusability();
    final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen();
    final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen);

    // 1. 若descendantFocusability为FOCUS_BLOCK_DESCENDANTS,则会拦截子View获取焦点
    if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {
        if (focusSelf) {
            super.addFocusables(views, direction, focusableMode);
        }
        return;
    }

    if (blockFocusForTouchscreen) {
        focusableMode |= FOCUSABLES_TOUCH_MODE;
    }

    // 2. 若descendantFocusability为FOCUS_BEFORE_DESCENDANTS,则会优先将自己加入到集合
    if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
        super.addFocusables(views, direction, focusableMode);
    }

    int count = 0;
    final View[] children = new View[mChildrenCount];
    for (int i = 0; i < mChildrenCount; ++i) {
        View child = mChildren[i];
        // 3. 只允许添加当前是Visible的子View
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
            children[count++] = child;
        }
    }
    FocusFinder.sort(children, 0, count, this, isLayoutRtl());
    for (int i = 0; i < count; ++i) {
        children[i].addFocusables(views, direction, focusableMode);
    }
  
    // 4. 若descendantFocusability为FOCUS_AFTER_DESCENDANTS,
    // 则如果没有子View符合条件时将自己添加到集合
    if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf
            && focusableCount == views.size()) {
        super.addFocusables(views, direction, focusableMode);
    }
}

首先需要先科普一下ViewGroup获取焦点的3种策略

  • FOCUS_BEFORE_DESCENDANTS: 优先于子View获取焦点
  • FOCUS_AFTER_DESCENDANTS: 当子View都不获取焦点时,才获取焦点
  • FOCUS_BLOCK_DESCENDANTS: 禁止子View获取焦点

同样可以通过XML设置,譬如:

android:descendantFocusability="beforeDescendants"

而这里就是根据不同的策略对集合的添加进行控制:

  • descendantFocusability = FOCUS_BLOCK_DESCENDANTS,则会拦截子View获取焦点,见注释1处
  • descendantFocusability = FOCUS_BEFORE_DESCENDANTS,则会优先将自己加入到集合,见注释2处
  • 遍历子View,并调用它的addFocusables,如果是ViewGroup的话就会起到往下递归目的,见注释3处
  • descendantFocusability = FOCUS_AFTER_DESCENDANTS的话,则如果没有子View符合条件时将自己添加到集合,见注释4处

焦点记忆技巧

如果需要做焦点记忆的需求,就可以考虑在addFocusables这里下手了,譬如在RecyclerView中记录当前选中的位置,在addFocusables只将该位置对应的View添加到集合,这样就能将焦点重新给到这个View。

override fun addFocusables(views: ArrayList<View>?, direction: Int, focusableMode: Int) {
    // 根据选中位置,重新获取到View
    val view: View? = layoutManager?.findViewByPosition(currentSelectedPosition)
    if (hasFocus() || currentSelectedPosition < 0 || view == null) {
        super.addFocusables(views, direction, focusableMode)
    } else if (view.isFocusable && view.visibility == View.VISIBLE) {
        // 只将这个View添加到集合,并终止方法往下递归
        views?.add(view)
    } else {
        super.addFocusables(views, direction, focusableMode)
    }
}

寻找下一个焦点

// FocusFinder.java
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
        int direction, ArrayList<View> focusables) {
    if (focused != null) {
        if (focusedRect == null) {
            focusedRect = mFocusedRect;
        }
        
        // 1. 获取到focused所在区域,并将其换算成与根视图相同的坐标系
        focused.getFocusedRect(focusedRect);
        root.offsetDescendantRectToMyCoords(focused, focusedRect);
    } else {
        if (focusedRect == null) {
            focusedRect = mFocusedRect;
            // make up a rect at top left or bottom right of root
            switch (direction) {
                case View.FOCUS_RIGHT:
                case View.FOCUS_DOWN:
                    setFocusTopLeft(root, focusedRect);
                    break;
                case View.FOCUS_FORWARD:
                    if (root.isLayoutRtl()) {
                        setFocusBottomRight(root, focusedRect);
                    } else {
                        setFocusTopLeft(root, focusedRect);
                    }
                    break;

                case View.FOCUS_LEFT:
                case View.FOCUS_UP:
                    setFocusBottomRight(root, focusedRect);
                    break;
                case View.FOCUS_BACKWARD:
                    if (root.isLayoutRtl()) {
                        setFocusTopLeft(root, focusedRect);
                    } else {
                        setFocusBottomRight(root, focusedRect);
                    break;
                }
            }
        }
    }
       
    switch (direction) {
        case View.FOCUS_FORWARD:
        case View.FOCUS_BACKWARD:
            return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
                    direction);
        case View.FOCUS_UP:
        case View.FOCUS_DOWN:
        case View.FOCUS_LEFT:
        case View.FOCUS_RIGHT:
            // 2. 根据focused所在区域获取到下一个获取焦点的View
            return findNextFocusInAbsoluteDirection(focusables, root, focused,
                    focusedRect, direction);
        default:
            throw new IllegalArgumentException("Unknown direction: " + direction);
    }
}

焦点寻找是就近原则,默认会根据对比两块Rect区域的位置,来确定焦点位置。所以findNextFocus做了两件事:

  • 求出当前拥有焦点的View即focused的Rect区域,并将它转换成与根视图root相同的坐标系。ps:这样是为了后续比较时能在同一坐标系下比较。
  • 根据focused所在区域获取到下一个获取焦点的View
// FocusFinder.java
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
        Rect focusedRect, int direction) {
    // 1. 最佳候选View所在的区域,初始值为focused的区域
    mBestCandidateRect.set(focusedRect);
    switch(direction) {
        case View.FOCUS_LEFT:
            mBestCandidateRect.offset(focusedRect.width() + 1, 0);
            break;
        case View.FOCUS_RIGHT:
            mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
            break;
        case View.FOCUS_UP:
            mBestCandidateRect.offset(0, focusedRect.height() + 1);
            break;
        case View.FOCUS_DOWN:
            mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
    }

    View closest = null;
    
    // 2. 遍历候选集合,筛选出最佳候选区域
    int numFocusables = focusables.size();
    for (int i = 0; i < numFocusables; i++) {
        View focusable = focusables.get(i);

        // only interested in other non-root views
        if (focusable == focused || focusable == root) continue;

        // 3. 获取候选的View所在区域,并将它转换为与root相同坐标系
        focusable.getFocusedRect(mOtherRect);
        root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
        // 4. 对比两个区域,如果可行,就更新最佳候选View所在的区域
        if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
            mBestCandidateRect.set(mOtherRect);
            closest = focusable;
        }
    }
    return closest;
}

mBestCandidateRect最佳候选View的区域,这里大概分几步:

  • mBestCandidateRect初始化为当前拥有焦点的区域focusedRect
  • 通过遍历候选集合,筛选出最佳候选区域,与其对应的View对象
  • 在对比前需要将候选的View所在区域转换成与root相同坐标系
  • 对比区域可行后,会更新mBestCandidateRect,以及对应的View对象,整个过程类似求最大值的过程。

至此,下一个获取焦点的View就得出来了。

获取焦点

requestFocus

View#requestFocus

View获取焦点的流程

// View.java
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;
    }

    // 在TouchMode下需要声明focusableInTouchMode为true才能获取焦点
    if (isInTouchMode() &&
        (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
           return false;
    }

    // 如果存在descendantFocusability为FOCUS_BLOCK_DESCENDANTS的父View则不能获取焦点
    if (hasAncestorThatBlocksDescendantFocus()) {
        return false;
    }

    if (!isLayoutValid()) {
        mPrivateFlags |= PFLAG_WANTS_FOCUS;
    } else {
        clearParentsWantFocus();
    }
    
    // 获取焦点
    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true;
}

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

    if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
        mPrivateFlags |= PFLAG_FOCUSED;  // 1. 标记为获取了焦点

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

        if (mParent != null) {
            // 2. 通知自己的父View自己获取了焦点
            mParent.requestChildFocus(this, this);
            updateFocusedInCluster(oldFocus, direction);
        }

        if (mAttachInfo != null) {
            // 3. 回调viewTreeObserver的OnGlobalFocusChange
            mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
        }
        
        // 4. 如果有注册OnFocusChangeListener,会有回调
        onFocusChanged(true, direction, previouslyFocusedRect);
        refreshDrawableState();
    }
}

View#requestFocus最后会调用到handleFocusGainInternal方法

  • 更新mPrivateFlags,标记为自己已经获取了焦点
  • 调用ViewGroup#requestChildFocus,通知自己的父View自己获取了焦点
  • 回调相关监听
    • viewTreeObserver的OnGlobalFocusChange
    • OnFocusChangeListener.onFocusChange

ViewGroup#requestChildFocus

// ViewGroup.java
@Override
public void requestChildFocus(View child, View focused) {
    if (DBG) {
        System.out.println(this + " requestChildFocus()");
    }
    // 如果descendantFocusability为FOCUS_BLOCK_DESCENDANTS则直接返回
    if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
        return;
    }

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

    // 1. 清空旧的焦点
    if (mFocused != child) {
        if (mFocused != null) {
            mFocused.unFocus(focused);
        }

        mFocused = child;
    }
    // 2. 继续往父View方向传递
    if (mParent != null) {
        mParent.requestChildFocus(this, focused);
    }
}

ViewGroup#requestChildFocus主要做了两件事:

  • 清空旧的焦点状态
  • 继续调用父View的requestChildFocus,目的是继续更新父View的记录状态

ViewGroup#requestFocus

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

    boolean result;
    switch (descendantFocusability) {
        // 1. 不允许子View获取焦点
        case FOCUS_BLOCK_DESCENDANTS:
            result = super.requestFocus(direction, previouslyFocusedRect);
            break;
        // 2. 自己优先于子View获取焦点    
        case FOCUS_BEFORE_DESCENDANTS: {
            final boolean took = super.requestFocus(direction, previouslyFocusedRect);
            result = took ? took : onRequestFocusInDescendants(direction,
                    previouslyFocusedRect);
            break;
        }
        // 3. 子View优先于自己获取焦点
        case FOCUS_AFTER_DESCENDANTS: {
            final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
            result = took ? took : super.requestFocus(direction, previouslyFocusedRect);
            break;
        }
        default:
            throw new IllegalStateException("descendant focusability must be "
        + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
        + "but is " + descendantFocusability);
    }
    if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) {
        mPrivateFlags |= PFLAG_WANTS_FOCUS;
    }
    return result;
}

// 4. 寻找可以获取焦点的子View
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;
}

onRequestFocusInDescendants的作用是寻找一个子View获取焦点,见注释4处

这里主要是扩展了获取焦点策略的能力,也是根据定义

  • FOCUS_BLOCK_DESCENDANTS不允许子View获取焦点,所以这里直接令自己去获取焦点,见注释1处
  • FOCUS_BEFORE_DESCENDANTS:自己优先于子View获取焦点,所以这里会先自己调用super.requestFocus,再通过结果调用onRequestFocusInDescendants,见注释2处
  • FOCUS_AFTER_DESCENDANTS子View优先于自己获取焦点,所以会先通过onRequestFocusInDescendants返回的结果,再视情况令自己获得焦点,见注释3处

开发中,可以重写onRequestFocusInDescendants以此来控制您想希望获取焦点的子View获取焦点

默认获取焦点

回到刚开始的流程,如果目前还没有找到拥有焦点的View,会怎样呢?

// ViewRootImpl.ViewPostImeInputStage.java#performFocusNavigation
View focused = mView.findFocus();
if (focused != null) {
    。。。
} else {
    if (mView.restoreDefaultFocus()) {
        return true;
    }
}

// View.java
public boolean restoreDefaultFocus() {
    return requestFocus(View.FOCUS_DOWN);
}

如果找不到当前拥有焦点的View,则会调用View#restoreDefaultFocus,实际上就是requestFocus,一般其实是ViewGroup#requestFocus,因为mView是DecorView。那这样就有机会通过onRequestFocusInDescendants令子View获取到焦点。

最后

本文主要对焦点流程进行了梳理,包括焦点的产生与搜索过程,事件分发的过程。

参考

TV开发-Android焦点的一生

Android的按键分发--遇到问题后结合源码进行分析