TV开发-Android焦点的一生

2,802 阅读14分钟

基于Android11

概述

Android View的焦点机制对于触摸屏的设备不是那么重要,但是对于TV设备,View的焦点机制至关重要,所以对于Tv开发者这是必备的技能。

这段时间火遍海内外的网红李子柒出了很多“一生系列”,什么南瓜的一生,葡萄的一生,大蒜的一生.....

这篇文章也借鉴这种方式,讨论一下Android View焦点的一生。

故事要从long long ago 讲起......, 不,故事要从你打开你的Android电视,手拿遥控器按下你神圣的一键开始。

按下一个向下键

在讨论这个操作之前,先说一下Android支持的输入设备,除了我们常见的触摸屏外,还支持键盘、鼠标、游戏手柄、遥控器等。在Linux有一种说法:一切皆文件,同样外接设备对于Linux系统也是一种文件,它们一般被创建在/dev/目录下,而输入设备则创建在/dev/input目录下:

chengfangpeng@ubuntu:/dev/input$ ls
by-id    event0  event10  event12  event14  event16  event2  event4  event6  event8  mice
by-path  event1  event11  event13  event15  event17  event3  event5  event7  event9  mouse0

上面是一台Linux台式机的/dev/input目录。

Android系统是基于Linux系统,所以设备管理都是由Linux内核完成的。下面是一台Android Tv设备的/dev/input目录

marconi:/dev/input # ls
event0 event1 event2 event3 event4 mice mouse0

当设备可用时,Linux内核会在/dev/input目录下创建event0-n或者其他的名称的节点文件,当设备不可用时则会删除该节点。当我们操作一个红外的遥控器,按下向下键时,Tv设备收到红外信号,触发一次硬件中断,Linux内核则会相应的接收到硬件中断,然后将中断加工成原始的输入事件数据写入到对应的设备节点中。用户空间可以通过read()函数将这个事件读取出来。

Android的输入系统就是不停的监听这些输入节点,当发现某个节点有输入时,就将节点的事件读取出来,分发给对应的接受者。

读取/dev/input事件

假如我们想看看/dev/input节点中这些事件长什么样子,可以通过getevent这个工具看到

chengfangpeng@ubuntu:~$ adb root
chengfangpeng@ubuntu:~$ adb shell getevent -lt /dev/input/event0
[    3378.901913] EV_KEY       KEY_DOWN             DOWN                
[    3378.901913] EV_SYN       SYN_REPORT           00000000            
[    3379.104075] EV_KEY       KEY_DOWN             UP                  
[    3379.104075] EV_SYN       SYN_REPORT           00000000 

可以监听/dev/input/event0这个节点下的输入事件,当我按下遥控器的向下键,监听到的事件。事件类型是KEY_DOWN 值是DOWN和UP,咦!不是还有个SY_REPORT,它是干什么的?EV_SYN是一个特别的事件类型,它用来把同一时刻产生的多个输入数据分割为多个数据包。我们看到了从/dev/input/event0中读出的数据,但是真正的Android是怎么处理的呢,这就涉及到Android的输入系统。

Android输入系统

输入系统的内容比较多,这里不做详细的介绍,后面会专门介绍这一块的逻辑,先挖个坑(不知道什么时候填),借用一张<<深入理解Android卷3>>的一张图,非常的清楚。

![image-20201015154906543](/home/chengfangpeng/Nutstore Files/Nutstore/res/image-20201015154906543.png)

我们输入的事件,如同穿天猴一样,蹭蹭的来到了我们当前所在的Window,这里除了输入系统的,又新加了WMS的逻辑,继续挖坑(之后填...),我们默认我们发的事件已经找到了当前的Window,或者说当前的Activity. 如果你对Activity、Window、PhoneWIndow、WindowMnager、WindowManagerService、ViewRootImpl、DecorView 这些概念也不太熟悉的话,那就继续挖坑(后面会出一篇Activity的一生)。但是在介绍View的焦点机制的时候,上面的一些概念是绕不开的。如果阅读过程中有疑问请查找相应的文章或源码。

ViewRootImpl事件分发

上面的章节提到,遥控器发的事件被传入到了Window中,那到底传到window哪里了,下面是通过点击向下键触发crash的堆栈信息,可以比较清晰的获取事件传递的路径,也验证了上面的结论。

2020-10-15 16:52:59.634 1378-1378/com.xray.focus.sample E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.xray.focus.sample, PID: 1378
    java.lang.RuntimeException: dispatchKeyEvent crash
        at com.xray.focus.sample.widget.TraceKevEventView.dispatchKeyEvent(TraceKevEventView.java:25)
        at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
        at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
        at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
        at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
        at com.android.internal.policy.DecorView.superDispatchKeyEvent(DecorView.java:436)
        at com.android.internal.policy.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1830)
        at android.app.Activity.dispatchKeyEvent(Activity.java:3827)
        at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:350)
        at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:5337)
        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5205)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4726)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4779)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4745)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4885)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4753)
        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4942)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4726)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4779)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4745)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4753)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4726)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4779)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4745)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4918)
        at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:5079)
        at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:2844)
        at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:2427)
        at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:2418)
        at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:2821)
        at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:143)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:327)
        at android.os.Looper.loop(Looper.java:169)
        at android.app.ActivityThread.main(ActivityThread.java:7021)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:486)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:872)

从ViewRootImpl开始

ViewRootImpl是整个View树的最上层,在Activity中DecorView是最顶层的View,但是它的mParent是ViewRootImpl,所以严格意义上说ViewRootImpl是整个View树的最上层。遥控器事件会被分发到ViewPostImeInputStage的onProcess方法中。

#ViewRootImpl.java 
/**
     * Delivers post-ime input events to the view hierarchy.
     */
    final class ViewPostImeInputStage extends InputStage {
        public ViewPostImeInputStage(InputStage next) {
            super(next);
        }

        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                //执行事件的分发
                return processKeyEvent(q);
            } 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;

         	...

            // Handle automatic focus changes.
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                if (groupNavigationDirection != 0) {
                    if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                        return FINISH_HANDLED;
                    }
                } else {
                    if (performFocusNavigation(event)) {//处理焦点的移动
                        return FINISH_HANDLED;
                    }
                }
            }
            return FORWARD;
        }

处理焦点的移动

 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;
            }
            if (direction != 0) {
                //这个mView就是DecorView,也是我们Viwe树最底层的View,
                //然后通过findFoucs去查找整个View树中已经获取焦点的那个View.
                View focused = mView.findFocus();
                if (focused != null) {//focused不为空,那么就通过这个focused去查找下一个要获取焦点的View
                    View v = focused.focusSearch(direction);
                    if (v != null && v != focused) {//找到了下一个要获取焦点的View
                        // 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)) {//让这个将要获取焦点的View真正获取焦点,也有可能获取不到
                            playSoundEffect(SoundEffectConstants
                                    .getContantForFocusDirection(direction));//执行获取到焦点后的音效
                            return true;
                        }
                    }

                   // 没有找到焦点 给view最后一次机会消费按键事件的机会
                    if (mView.dispatchUnhandledMove(focused, direction)) {
                        return true;
                    }
                } else {
                    // 如果当前界面没有焦点走这里,设置一个默认焦点
                    if (mView.restoreDefaultFocus()) {
                        return true;
                    }
                }
            }
            return false;
        }

从View树中查找当前拥有焦点的View

遍历整个View树,找到当前那个正拥有焦点的View , findFocus方法在View和ViewGroup中有不同的实现,这种查找方式和findViewById的实现方式非常的类似。先看View中的实现,其实就是判断mPrivateFlags中是否设置了PFLAG_FOCUSED,无意中说出了焦点的本质,View焦点就是这个PFLAG_FOCUSED属性。

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

ViewGroup中的实现:

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

        if (isFocused()) {
            return this;
        }

        if (mFocused != null) {//mFocused这个成员变量代表它自己有焦点或者它包含了有焦点的View
            return mFocused.findFocus();//继续向下查找
        }
        return null;
    }

查找下一个将要获取焦点的View

回到上面ViewRootImpl的performFocusNavigation方法,我们find到了当前拥有焦点的View,接下来,通过focused去查找下一个将要获取焦点的View.这个过程是通过focusSearch这个方法实现的,当然它在View和ViewGroup中都有实现。

我们先看View中的实现。逻辑比较简单, 就是如果有父布局,就一直往上找。

#View.java
/**
     * Find the nearest view in the specified direction that can take focus.
     * This does not actually give focus to that view.
     *
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
     *
     * @return The nearest focusable in the specified direction, or null if none
     *         can be found.
     */
    public View focusSearch(@FocusRealDirection int direction) {
        if (mParent != null) {
            return mParent.focusSearch(this, direction);
        } else {
            return null;
        }
    }

我们的布局一般情况都不只一层,而mParent就是一个ViewGroup,所以现在看看ViewGroup中focusSearch的实现。

#ViewGroup.java
/**
     * Find the nearest view in the specified direction that wants to take
     * focus.
     *
     * @param focused The view that currently has focus
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and
     *        FOCUS_RIGHT, or 0 for not applicable.
     */
    @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;
    }

首先判断当前ViewGroup是不是在View树的最上层,如果不是就继续往上找,如果是,则执行如下逻辑:

return FocusFinder.getInstance().findNextFocus(this, focused, direction);

FocusFinder是查找FoucsView的一个工具类,现在看一下findNextFocus方法

#FocusFinder.java
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        View next = null;
        ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
        if (focused != null) {//如果当前的焦点不为空,从用户指定的view中查找,focused设置了nextFoucs***Id属性
            next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
        }
        if (next != null) {
            return next;
        }
        ArrayList<View> focusables = mTempList;
        try {
            focusables.clear();
            effectiveRoot.addFocusables(focusables, direction);//将可以获取焦点的备选view放入focusables集合中
            if (!focusables.isEmpty()) {//候选view的集合不为空时,从候选集合中继续查找
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
            }
        } finally {
            focusables.clear();
        }
        return next;
    }

首先,如果focused不为空,从用户指定nextFocus***Id来获取下一个获取焦点的View。通常我们可以在xml中指定下一个获取焦点的View的id,如下:

  	   android:nextFocusLeft=""
       android:nextFocusRight=""
       android:nextFocusDown=""
       android:nextFocusUp=""

findNextUserSpecifiedFocus就是从设置的这些id中查找下一个获取焦点的View.

#FocusFinder.java 
private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
        // check for user specified next focus
        View userSetNextFocus = focused.findUserSetNextFocus(root, direction);//从当前已经获取焦点的View中查找用户指定的下一个获取焦点的View
        View cycleCheck = userSetNextFocus;
        boolean cycleStep = true; // we want the first toggle to yield false
        while (userSetNextFocus != null) {
            if (userSetNextFocus.isFocusable()
                    && userSetNextFocus.getVisibility() == View.VISIBLE
                    && (!userSetNextFocus.isInTouchMode()
                            || userSetNextFocus.isFocusableInTouchMode())) {
                return userSetNextFocus;
            }
            userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
            if (cycleStep = !cycleStep) {
                cycleCheck = cycleCheck.findUserSetNextFocus(root, direction);
                if (cycleCheck == userSetNextFocus) {
                    // found a cycle, user-specified focus forms a loop and none of the views
                    // are currently focusable.
                    break;
                }
            }
        }
        return null;
    }
#View.java
/**
     * If a user manually specified the next view id for a particular direction,
     * use the root to look up the view.
     * @param root The root view of the hierarchy containing this view.
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD,
     * or FOCUS_BACKWARD.
     * @return The user specified next view, or null if there is none.
     */
    View findUserSetNextFocus(View root, @FocusDirection int direction) {
        switch (direction) {
            case FOCUS_LEFT:
                if (mNextFocusLeftId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusLeftId);
            case FOCUS_RIGHT:
                if (mNextFocusRightId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusRightId);
            case FOCUS_UP:
                if (mNextFocusUpId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusUpId);
            case FOCUS_DOWN:
                if (mNextFocusDownId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusDownId);
            case FOCUS_FORWARD:
                if (mNextFocusForwardId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusForwardId);
            case FOCUS_BACKWARD: {
                if (mID == View.NO_ID) return null;
                final View rootView = root;
                final View startView = this;
                // Since we have forward links but no backward links, we need to find the view that
                // forward links to this view. We can't just find the view with the specified ID
                // because view IDs need not be unique throughout the tree.
                return root.findViewByPredicateInsideOut(startView,
                    t -> findViewInsideOutShouldExist(rootView, t, t.mNextFocusForwardId)
                            == startView);
            }
        }
        return null;
    }

如果用户没有指定下一个获取焦点的View, 接下来就会走Android内部的寻焦算法,首先把可以获取焦点的候选View,放置到一个View列表focusables中,而执行这个添加任务是通过这个addFocusables方法,它会同样也是从上到下遍历整个View树。

    effectiveRoot.addFocusables(focusables, direction);

所以addFocusables也分View和ViewGroup,先看View的逻辑,如果这个View符合获取焦点的资格,那么将其添加到views中。先看View中的实现:

# View.java
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);//符合获取焦点的资格,将自己添加到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);

        if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {//当descendantFocusability为FOCUS_BLOCK_DESCENDANTS时,
            //禁止让自己的子View获取焦点
            if (focusSelf) {
                super.addFocusables(views, direction, focusableMode);//验证自己是否可以被添加的views中,如果符合就添加之
            }
            return;
        }

        if (blockFocusForTouchscreen) {
            focusableMode |= FOCUSABLES_TOUCH_MODE;
        }

        if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {//当descendantFocusability
            //为FOCUS_BEFORE_DESCENDANTS优先子View获取焦点时
            super.addFocusables(views, direction, focusableMode);//验证自己是否可以被添加的views中,如果符合就添加之
        }

        int count = 0;
        final View[] children = new View[mChildrenCount];
        for (int i = 0; i < mChildrenCount; ++i) {
            View child = mChildren[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                children[count++] = child;
            }
        }
        FocusFinder.sort(children, 0, count, this, isLayoutRtl());
        for (int i = 0; i < count; ++i) {//将自己符合条件的子View添加到views中
            children[i].addFocusables(views, direction, focusableMode);
        }

        // When set to FOCUS_AFTER_DESCENDANTS, we only add ourselves if
        // there aren't any focusable descendants.  this is
        // to avoid the focus search finding layouts when a more precise search
        // among the focusable children would be more interesting.
        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获取焦点

而在ViewGroup中默认的descendantFocusability是FOCUS_BEFORE_DESCENDANTS:

 private void initViewGroup() {
     	...
        setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
     	...
    }

上面就是addFocusables的流程,这个方法很有用,通过复写它,可以简化focusables的大小,从而提高寻焦的效率。Tv开发Leanback框架中GridLayoutManager类的onAddFocusables方法,就是实现的RecyclerView的addFocusables方法。

下一个获取焦点的候选名单已经找好了,但是我们选哪个呢,继续我们寻焦逻辑,findNextFocus还有一个同名重用方法,在这个方法将从这些候选名单中找出那个最优者,当然也有可能找不到。

#FoucsFinder.java
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
            int direction, ArrayList<View> focusables) {
        if (focused != null) {
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
            }
            // fill in interesting rect from focused
            focused.getFocusedRect(focusedRect);//获取focused所在的区域
            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);//如果focused为空,那么FOCUS_RIGHT和FOCUS_DOWN对于的
                        //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;
                    }
                }
            }
        }
		//上面的逻辑就是通过focused计算出它所占据的区域
        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:
                //上下左右方向寻找下个获取焦点view的算法
                return findNextFocusInAbsoluteDirection(focusables, root, focused,
                        focusedRect, direction);
            default:
                throw new IllegalArgumentException("Unknown direction: " + direction);
        }
    }

上面的逻辑就是通过focused计算出它所占据的区域,一个Rect,然后通过这个focusedRect去和所有的候选者的Rect去做对比,找出“最亲近”的那个,以FOCUS_UP、FOCUS_DOWN、FOCUS_LEFT、FOCUS_RIGHT为例,它的算法实现在findNextFocusInAbsoluteDirection方法中,离真相越来越近了,是不是很鸡冻!!!

#FoucsFinder.java
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
            Rect focusedRect, int direction) {
        // initialize the best candidate to something impossible
        // (so the first plausible view will become the best choice)
        mBestCandidateRect.set(focusedRect);//mBestCandidateRect为最佳候选View所在的区域,初始化为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;

        int numFocusables = focusables.size();
        for (int i = 0; i < numFocusables; i++) {//遍历整个focusables,寻找下一个最和是的获取焦点的View
            View focusable = focusables.get(i);

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

            // get focus bounds of other view in same coordinate system
            focusable.getFocusedRect(mOtherRect);//将focusable的位置信息保存在mOtherRect中
            root.offsetDescendantRectToMyCoords(focusable, mOtherRect);//统一坐标

            if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
                //比较mOtherRect和mBestCandidateRect,如果mOtherRect比mBestCandidateRect更靠近focusedRect则
                //将mOtherRect赋值给mBestCandidateRect
                mBestCandidateRect.set(mOtherRect);
                closest = focusable;
            }
        }
     //当循环完毕,closest就是哪个下一个获取焦点的最佳View
        return closest;
    }

说明一下上面的逻辑: 给出一个mBestCandidateRect变量,它里面保存了当前那个“最亲近”的最优解,通过遍历focusables,和mBestCandidateRect做对比,如果有比mBestCandidateRect更优的解,则替换这个mBestCandidateRect,直到遍历完成,最后的这个mBestCandidateRect就是我们找的那个最优解。过程类似选择排序。所以还有最后的一个问题了,就是两个Rect的对比,哪个会胜出?哪个更"亲近"?

比较两个View的区域哪个是最好的候选者

#FoucsFinder.java
/**
     * Is rect1 a better candidate than rect2 for a focus search in a particular
     * direction from a source rect?  This is the core routine that determines
     * the order of focus searching.
     * @param direction the direction (up, down, left, right)
     * @param source The source we are searching from
     * @param rect1 The candidate rectangle
     * @param rect2 The current best candidate.
     * @return Whether the candidate is the new best.
     */
    boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {

        // to be a better candidate, need to at least be a candidate in the first
        // place :)
        if (!isCandidate(source, rect1, direction)) {//判断是否是候选者,候选view需要在指定的方向里
            return false;
        }

        // we know that rect1 is a candidate.. if rect2 is not a candidate,
        // rect1 is better
        if (!isCandidate(source, rect2, direction)) {
            return true;
        }

        // if rect1 is better by beam, it wins
        if (beamBeats(direction, source, rect1, rect2)) {//判断两个候选区域哪个在光束里,哪个获胜,啥叫光束?后面会介绍,图文介绍
            return true;
        }

        // if rect2 is better, then rect1 cant' be :)
        if (beamBeats(direction, source, rect2, rect1)) {
            return false;
        }

        // otherwise, do fudge-tastic comparison of the major and minor axis
        return (getWeightedDistanceFor(//比重权重距离,与direction有关
                        majorAxisDistance(direction, source, rect1),
                        minorAxisDistance(direction, source, rect1))
                < getWeightedDistanceFor(
                        majorAxisDistance(direction, source, rect2),
                        minorAxisDistance(direction, source, rect2)));
    }

主要代码都贴了,但是如果你没有很大的耐心的话,弄清楚上面的这几段算法代码还是比较困难的(所以一些分支代码就不贴了),我现在直接给出结论,然后你可以再看代码来验证这些结论,当然我会给出一个demo,方便你来验证这些结论,是不是很贴心。上面的代码总结出来有3条算法规则,一图胜千言,我将用3幅图解释这3条算法规则:

  • 第一条规则: isCandidate

    首先是比较传入的两个Rect,哪个在候选区域,那么啥叫候选区域?候选区域要位于焦点方向上,也就是direction所在的方向里。举个例子,假如你的direction是FOCUS_LEFT, 那么候选区域必须在Focused区域的左边,如下图: A就在候选区域,而B不是。

  • 第二条规则:beamBeats

    还是以FOCUS_LEFT为例,如下图,A和BEAM(这个BEAM就是那个光束,看图一下就明白了吧)有重合的部分而B没有,则AB比拼中,A胜出。

  • 第三条规则:getWeightedDistanceFor

    如果beamBeats中,A和B都没有和BEAM有重合的区域,或者都有重合的区域,那该怎么办?这就需要第三条规则, 比较AB区域距离Focused的权重距离,这个权重距离怎么算呢?还是以direction为FOCUS_LEFT为例, 为什么要强调这个direction,因为它会影响权重距离的计算,比如FOCUS_LEFT,他会增加dx的权重,比如

    A距离的平方: 13 * dx2^2 + dy2^2, B距离的平方: 13 * dx1^2 + dy2^2. 如果是FOCUS_DOWN则会加大dy的权重。

 

到现在focusSearch的逻辑已经走完了,有两种结果,一种是我们找到了下一个获取焦点的View,另一种是没找到。如果没找到,让View处理Event事件怎么消费,或者不消费。如果找到了呢?回到ViewRootImpl.performFocusNavigation方法,接下来就是使用找到的View去requestFocus. 它的作用是通知自己的七大姑八大姨说我拿到焦点了,也就是整个View树,同时,拿到焦点后更新一下自己的状态,要不用户怎么知道你这个View拿到焦点了呢,对吧。好,上代码,先看View的requestFocus方法,当然ViewGroup也复写了这个方法。

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

    private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
        // need to be focusable
        if (!canTakeFocus()) {//判断当前View有没有获取焦点的资格
            return false;
        }

        // need to be focusable in touch mode if in touch mode
        if (isInTouchMode() &&
            (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {//如果是在touch mode,需要
            //focusableInTouchMode为true
               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;
    }

获取焦点

 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) {
                //层层上报,告诉父布局获取当前View获取焦点,并且将旧的焦点清除
                //当传入到最上层的ViewRootImpl会触发一次View树重绘
                mParent.requestChildFocus(this, this);
                updateFocusedInCluster(oldFocus, direction);
            }

            if (mAttachInfo != null) {
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }
			//通知监听焦点变化的listener
            onFocusChanged(true, direction, previouslyFocusedRect);
            refreshDrawableState();//更新drawble状态,高亮显示等
        }
    }

层层上报,告诉父布局获取当前View获取焦点,并且将旧的焦点清除

#ViewGroup.java
@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方法层层上报,到上传到View树的最上层,也就是ViewRootImpl中,会触发一次View树的重绘。这一点在UI效率优化时,应该会用到。

#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();//触发View重绘
    }

总结一下View的requestFoucs做的工作:

  • 首先判断当前View是否有资格获取焦点
  • 如果符合获取焦点的资格,View添加PFLAG_FOCUSED,并且层层的通知父布局,确保只有一个View获取焦点,当调用到View的最上层ViewRootImpl时会触发整个View树的重绘。
  • 调用onFoucsChange,通知监听者。
  • 更新drawable状态。

上面是View的requestFocus流程,下面看一下ViewGroup的requestFoucs流程,会比View稍微复杂一些。

 public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " ViewGroup.requestFocus direction="
                    + direction);
        }
        int descendantFocusability = getDescendantFocusability();

        boolean result;
        switch (descendantFocusability) {
            case FOCUS_BLOCK_DESCENDANTS://禁止子View获取焦点
                result = super.requestFocus(direction, previouslyFocusedRect);
                break;
            case FOCUS_BEFORE_DESCENDANTS: {//优先子View获取焦点
                final boolean took = super.requestFocus(direction, previouslyFocusedRect);
                result = took ? took : onRequestFocusInDescendants(direction,
                        previouslyFocusedRect);
                break;
            }
            case FOCUS_AFTER_DESCENDANTS: {//子View优先获取焦点
                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;
    }

onRequestFocusInDescendants 负责让子View获取焦点,开发者可以复写该方法,自定义子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;
    }

总结

好了,总结一下,从用遥控器按下键开始,到设备接收到按键信号,然后通过Android输入系统转化为Event事件,分发给当前的窗口的ViewRootImpl,然后通过View的focusSearch查找的下一个获取焦点的View,再通过requestFocus,让它真生得到焦点。这篇文章主要在讨论焦点,所以涉及到Android的输入系统、WMS系统等都比较简单的略过,但是既然是焦点的一生,所以把其来龙去脉都提及了一下,但是涉及的这些模块又非常的重要且复杂,希望后面会单独出文章介绍。完!

参考资料

Android 按键的焦点分发处理机制

深入理解Android卷3

关于我

  • 公众号: CodingDev qrcode_for_gh_0e16b0c63d2d_258.jpg