Android按键事件焦点移动源码分析

在市场上,手机硬件基本上占领android设备的绝大部分市场,而在TV上,由于人机交互的方式不同,并且当前主流的TV并不具备触摸屏(虽然目前的触屏电视已经面市,但是该类商显产品主要还是2B。),传统TV还是通过遥控器的方向按键进行操控,在android系统中则是通过焦点的移动标识来展示给用户当前的控制点。下面就从接收到遥控器的按键事件开始,一步步分析下系统中的焦点机制是如何响应工作的。(本文基于API 27源码进行分析)

首先,从底层驱动接收到遥控器按键或者触摸屏触摸事件后,通过一步步的转换到android framework中的用户界面层,会回调给ViewRootImpl中的ViewPostImeInputStage,这个内部类的代码稍长,因为不论是触屏还是按键,都是在这里进行初始的分发处理,在此,我们只重点关注按键事件以及焦点的处理:

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

    @Override
    protected void onDeliverToNext(QueuedInputEvent q) {
        ...
    }

    private boolean performFocusNavigation(KeyEvent event) {
        ...
    }

    private boolean performKeyboardGroupNavigation(int direction) {
        ...
    }

    private int processKeyEvent(QueuedInputEvent q) {
        ...
    }

    private int processPointerEvent(QueuedInputEvent q) {
        ...
    }

    private void maybeUpdatePointerIcon(MotionEvent event) {
        ...
    }

    private int processTrackballEvent(QueuedInputEvent q) {
        ...
    }

    private int processGenericMotionEvent(QueuedInputEvent q) {
        ...
    }
}

首先我们来看下onProcess回调中的参数QueuedInputEvent

<ViewRootImpl.java>
    private static final class QueuedInputEvent {
        public static final int FLAG_DELIVER_POST_IME = 1 << 0;
        public static final int FLAG_DEFERRED = 1 << 1;
        public static final int FLAG_FINISHED = 1 << 2;
        public static final int FLAG_FINISHED_HANDLED = 1 << 3;
        public static final int FLAG_RESYNTHESIZED = 1 << 4;
        public static final int FLAG_UNHANDLED = 1 << 5;
    
        public QueuedInputEvent mNext;

        public InputEvent mEvent;
        public InputEventReceiver mReceiver;
        public int mFlags;
    ...
    }
// InputEvent的两个子类
public class KeyEvent extends InputEvent implements Parcelable {}
public final class MotionEvent extends InputEvent implements Parcelable {}

触摸或者按键都是一系列的接收事件,QueuedInputEvent实际上是类似Message的一个队列,mNext变量指向的是下一个事件(单向链表的结构)。mEvent变量标记了该事件的类型,我们可以看到android中,InputEvent只有两个子类,一个是KeyEvent按键事件,另一个是MotionEvent触摸事件。回到上面的onProcess方法,很明显我们TV端的是KeyEvent事件,进入processKeyEvent进行按键事件的处理。

<ViewRootImpl.java>
    @Override
    protected int onProcess(QueuedInputEvent q) {
        if (q.mEvent instanceof KeyEvent) {// 接收到的事件是按键事件
            return processKeyEvent(q);// 进入这个分支,按键事件处理
        } else {
            final int source = q.mEvent.getSource();// 手指触摸的touch事件
            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;// 获取到该按键事件信息,我们常见的KeyCode,Acton,RepeatCount等信息都包含在里面
    
        // Deliver the key to the view hierarchy.
        if (mView.dispatchKeyEvent(event)) {// mView实际上就是DecorView,这里看到如果dispatchKeyEvent返回true,会直接返回,这里的按键事件分发后面单独一篇讲解,对比touch事件分发要简单不少
            return FINISH_HANDLED;
        }
    
        if (shouldDropInputEvent(q)) {// 是否抛弃该事件,里面主要是判断View是否初始化或者还未add进来,window失去焦点(window失去焦点也就是说该window无法交互,所以接收事件也没用,直接返回)
            return FINISH_NOT_HANDLED;
        }
    
        int groupNavigationDirection = 0;
        // 根据tab和shift按键判断导航方向
        if (event.getAction() == KeyEvent.ACTION_DOWN
                && event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
            if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
                groupNavigationDirection = View.FOCUS_FORWARD;
            } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
                    KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
                groupNavigationDirection = View.FOCUS_BACKWARD;
            }
        }
    
        // 设置了快捷键
        // If a modifier is held, try to interpret the key as a shortcut.
        if (event.getAction() == KeyEvent.ACTION_DOWN
                && !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
                && event.getRepeatCount() == 0
                && !KeyEvent.isModifierKey(event.getKeyCode())
                && groupNavigationDirection == 0) {
            if (mView.dispatchKeyShortcutEvent(event)) {
                return FINISH_HANDLED;
            }
            if (shouldDropInputEvent(q)) {
                return FINISH_NOT_HANDLED;
            }
        }
    
        // Apply the fallback event policy.
        if (mFallbackEventHandler.dispatchKeyEvent(event)) {
            return FINISH_HANDLED;
        }
        if (shouldDropInputEvent(q)) {
            return FINISH_NOT_HANDLED;
        }
    
        // 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;
    }

上面经过一系列判断,包括Tab,Shift和快捷键的处理,我们这里重点关注最后的方向键导航处理performFocusNavigation(event)

<ViewRootImpl.java>
    private boolean performFocusNavigation(KeyEvent event) {
        int direction = 0;
        switch (event.getKeyCode()) {// 将按键事件的键值转换为View的焦点导航方向值
            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();// 从decorview中查找当前的焦点
            if (focused != null) {
                View v = focused.focusSearch(direction);// 根据方向查找下一个焦点,调用parent的focusSearch查找
                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)) {// 对查找到的焦点view调用requestFocus,清除oldFocus的焦点状态
                        playSoundEffect(SoundEffectConstants// 播放焦点移动音效,处理结束
                                .getContantForFocusDirection(direction));
                        return true;
                    }
                }
    
                // Give the focused view a last chance to handle the dpad key.
                if (mView.dispatchUnhandledMove(focused, direction)) {// 查找焦点失败,再提供一个机会去处理该次按键事件下view的移动
                    return true;
                }
            } else {// 如果当前都没有焦点
                if (mView.restoreDefaultFocus()) {// 重新初始化默认焦点,处理完毕
                    return true;
                }
        }
        }
        return false;
    }

这里面首先将按键的键值转换为焦点导航方向,主要有6个:FOCUS_BACKWARD,FOCUS_FORWARD,FOCUS_LEFT,FOCUS_UP,FOCUS_RIGHT,FOCUS_DOWN,接着通过findFocus查找到当前视图中的焦点。然后通过focusSearch方法(这个方法是查找焦点的关键方法,一些定制化逻辑可以通过修改此方法实现),根据当前焦点根据导航方向,去寻找下一个应该聚焦的View:

<View.java>
    public View focusSearch(@FocusRealDirection int direction) {
        if (mParent != null) {
            return mParent.focusSearch(this, direction);// 通过parent父View去查找下一个焦点
        } else {
            return null;
        }
    }

<ViewGroup.java>
    @Override
    public View focusSearch(View focused, int direction) {
        if (isRootNamespace()) {// 当前view==decorView,一般我们最终会走到这个分支
            // 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;
    }

View.focusSearch实际上是调用了parent的focusSearch,一层一层往上,最终也就是走到根布局DecorView的focusSearch,通过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) {
            next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);// 当前焦点不为null,首先判断用户对当前焦点的view在该方向上是否指定id,也就是我们通常xml中写的nextFocusLeft这种
        }
        if (next != null) {
            return next;// 如果用户指定了下个焦点id,直接返回该id对应的view
        }
        ArrayList<View> focusables = mTempList;// 这个集合是用来装所有可获得焦点的View
        try {
            focusables.clear();
            effectiveRoot.addFocusables(focusables, direction);// 查找可获得焦点的view,添加进集合
            if (!focusables.isEmpty()) {// 存在可获得焦点的view
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);// 继续在所有可获得焦点的view集合中查找下一个焦点
            }
        } finally {
            focusables.clear();// 查找完毕后清理数据,释放内存
        }
        return next;// 返回下一个焦点
    }
  • 首先会去判断用户有没有手动在xml中指定该方向的下一个焦点view的id,如果指定了直接返回该view作为下一个焦点,流程结束。对于findNextUserSpecifiedFocus方法逻辑还是比较好理解,在此不做展开分析。
  • 接着会查找所有可获得焦点的view,将它们添加到focusables集合中,缩小焦点查找范围。这里有个关键方法:addFocusables,这个方法在平时定制化开发中可以用于焦点记忆,例如leanback视图中每一行recyclerView中的焦点记忆。
<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 (!isFocusable()) {// 不可聚焦,直接返回
            return;
        }
        if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
                && !isFocusableInTouchMode()) {// 触摸模式下,但是focusInTouchMode设置为false,直接返回
            return;
        }
        views.add(this);// 将自己添加到集合中
    }

<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) {// 如果设置了拦截焦点
            if (focusSelf) {
                super.addFocusables(views, direction, focusableMode);// 调用View的addFocusables将自己添加进集合
            }
            return;// 直接返回,不再添加自己view数结构下面的子View
        }

        if (blockFocusForTouchscreen) {
            focusableMode |= FOCUSABLES_TOUCH_MODE;
        }

        if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
            super.addFocusables(views, direction, focusableMode);// 调用View的addFocusables将自己添加进集合
        }

        int count = 0;
        final View[] children = new View[mChildrenCount];
        for (int i = 0; i < mChildrenCount; ++i) {// 遍历当前viewGroup下的所有子View
            View child = mChildren[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {// view处于可见状态
                children[count++] = child;// 赋值给children数组
            }
        }
        FocusFinder.sort(children, 0, count, this, isLayoutRtl());// 根据方向排序
        for (int i = 0; i < count; ++i) {
            children[i].addFocusables(views, direction, focusableMode);// 如果children[i]这个子view是viewGroup的话,递归调用继续查找该child viewGroup下的子View,直到查找所有最下层的子view,最终调用View.addFocusables判断是否可聚焦,可聚焦则添加进集合
        }
        // 走到这里,views中已经保存了所有可聚焦的子View

        // 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()) {// 如果是FOCUS_AFTER_DESCENDANTS,除了子view判断外,最后将自己也添加进去
            super.addFocusables(views, direction, focusableMode);
        }
    }

经过上面的addFocusables已经将所有可见状态并且可以聚焦的view全部收集到了focusables这个集合中,接着在该集合中去查找下一个焦点:

<FocusFinder.java>
    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        ...
            if (!focusables.isEmpty()) {// 存在可获得焦点的view
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);// 继续在所有可获得焦点的view集合中查找下一个焦点
            }
        ...
        return next;// 返回下一个焦点
    }


    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
            int direction, ArrayList<View> focusables) {
        if (focused != null) {// 当前焦点不为null
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
            }
            // fill in interesting rect from focused
            focused.getFocusedRect(focusedRect);// 获取到当前焦点的rect区域
            root.offsetDescendantRectToMyCoords(focused, focusedRect);// 考虑scroll滑动状态,即把视框拉伸至滑动到屏幕外的视图也可见状态,统一坐标系便于下面焦点查找计算
        } 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);// 当前焦点为null,将滑动后的左上角作为寻找起始点(scrollX,scrollY),走到这里的话这个focusedRect实际上是个点
                        break;
                    case View.FOCUS_FORWARD:
                        if (root.isLayoutRtl()) {// 会根据rtl区分(某些国家语言是从右往左书写习惯)
                            setFocusBottomRight(root, focusedRect);
                        } else {
                            setFocusTopLeft(root, focusedRect);
                        }
                        break;

                    case View.FOCUS_LEFT:
                    case View.FOCUS_UP:
                        setFocusBottomRight(root, focusedRect);// 当前焦点为null,将滑动后的右下角作为寻找起始点(scrollX,scrollY),走到这里的话这个focusedRect实际上是个点
                        break;
                    case View.FOCUS_BACKWARD:
                        if (root.isLayoutRtl()) {// 会根据rtl区分是否将坐标反转
                            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:// 我们重点只关注这方向键的焦点查找算法
                return findNextFocusInAbsoluteDirection(focusables, root, focused,
                        focusedRect, direction);
            default:
                throw new IllegalArgumentException("Unknown direction: " + direction);
        }
    }
  • 如果当前焦点不为null,先获取当前焦点的rect视图区域,考虑到scroll状态,将当前焦点的rect坐标系进行转换。
  • 如果当前焦点为null,根据导航方向,设置一个左上角或者右下角的rect为默认的起始参考点,根据这个点再结合方向去计算下一个焦点。
    这里我们重点看下上下左右移动的这个方法findNextFocusInAbsoluteDirection,大致看下它内部查找焦点算法:
<FocusFinder.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);// 将当前焦点的rect赋值给mBestCandidateRect
        switch(direction) {// 在反方向上偏移一个width或者height+1个像素点,虚构出来的下一个候补焦点(优先级应该是最低的,因为是反方向平移)
            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++) {// 开始遍历所有可聚焦的子view
            View focusable = focusables.get(i);

            // only interested in other non-root views
            if (focusable == focused || focusable == root) continue;// 如果集合中的view是当前的焦点或者viewGroup,直接跳过继续查找下一个

            // get focus bounds of other view in same coordinate system
            focusable.getFocusedRect(mOtherRect);
            root.offsetDescendantRectToMyCoords(focusable, mOtherRect);// 将该view也进行坐标系转换,和当前焦点在同一个坐标系进行计算

            if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
                mBestCandidateRect.set(mOtherRect);// 如果找到一个符合的,则将其的区域赋值给虚构的候补焦点,参照物变了之后,继续遍历看有没有更优的
                closest = focusable;// 这个closest会不断刷新,因为每次进入该分支,最新的focusable符合条件都会优于上一个候补焦点
            }
        }
        return closest;
    }

先获取当前焦点的视图区域rect,然后将该区域按照导航方向的反方向偏移1个像素+当前焦点的width或者height,得到一个虚构的焦点区域mBestCandidateRect。接着就开始遍历之前收集到的所有可见可聚焦的view集合,如果当前遍历的view就是当前焦点或者rootView,直接忽略跳过继续往下遍历查找。遍历的时候,会将遍历的view坐标转换,只有转换坐标后和当前焦点在同一个坐标系,这样才能为下面算法提供准确参数:

<FocusFinder.java>
    // 几个参数含义: direction方向,source当前焦点,rect1当前对比的view,rect2虚构的候补焦点(如果有符合的,rect2会刷新为当前符合条件的view区域,即如果成立,rect1会赋值给下次该方法的rect2)
    // 这几个参数命名比较容易弄混,尤其是下面调用算法的时候又改名了,要区分清楚
    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;
        }
        // 走到这里说明上面的isCandidate返回true,也就是当前遍历的rect1符合条件。例如direction为左,说明rect1在当前焦点的左侧,符合条件,加入候选,进行下一步判断
        // we know that rect1 is a candidate.. if rect2 is not a candidate,
        // rect1 is better
        if (!isCandidate(source, rect2, direction)) {// 第一次走到这的话这个isCandidate肯定返回false,因为rect2第一次是我们之前虚构的候补焦点,是在导航的反方向,肯定为false,直接返回true。再后面的话,相当于上一个候补和当前焦点进行比较,肯定返回true,继续下一步判断
            return true;
        }

        // if rect1 is better by beam, it wins
        if (beamBeats(direction, source, rect1, rect2)) {// 当前遍历的view也符合条件,将它和上一个候补进行比较
            return true;// 当前遍历的view优于上一个候补,将当前遍历的赋值给最新的closest,也就是目前遍历过程中最优焦点
        }

        // 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(// 计算rect1和rect2相对于当前焦点的距离
                        majorAxisDistance(direction, source, rect1),
                        minorAxisDistance(direction, source, rect1))
                < getWeightedDistanceFor(
                        majorAxisDistance(direction, source, rect2),
                        minorAxisDistance(direction, source, rect2)));
    }

重点算法1,计算是否在导航的那侧,在导航方向上允许有重叠。这个算法都是比较xy方向的边界大小,相对于下面的算法2真的是容易理解很多,稍微画几个图就能理解了。

<FocusFinder.java>
    // srcRect当前焦点,destRect比较的view,direction方向
    boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
        switch (direction) {
            case View.FOCUS_LEFT:// 向左:只比较left和right,就是dest是否整体在src的左侧,这里说的是整体,dest可以与src有交集,但是dest的左右边界都不能超过src的右边界
                return (srcRect.right > destRect.right || srcRect.left >= destRect.right) 
                        && srcRect.left > destRect.left;
            case View.FOCUS_RIGHT:// 向右:只比较left和right,就是dest是否整体在src的右侧,这里说的是整体,dest可以与src有交集,但是src的左右边界都不能超过dest的右边界
                return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
                        && srcRect.right < destRect.right;
            case View.FOCUS_UP:// 向上:只比较top和bottom,就是dest是否整体在src的上面,这里说的是整体,dest可以与src有交集,但是dest的上下边界都不能超过src的下边界
                return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
                        && srcRect.top > destRect.top;
            case View.FOCUS_DOWN:// 向下:只比较top和bottom,就是dest是否整体在src的下面,这里说的是整体,dest可以与src有交集,但是src的上下边界都不能超过dest的下边界
                return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
                        && srcRect.bottom < destRect.bottom;
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }

重点算法2,这算法看着真的很乱,官方的注释也不好理解,不好描述,还是自己画几张图按流程跑一下去理解吧。

<FocusFinder.java>
    // direction方向,source当前焦点,rect1比较的view1,rect2比较的view2(rect1和rect2具体看上面算法调用的顺序)
    // 第一次调用:rect1当前遍历的view,rect2上一次符合条件的候补焦点
    // 第二次调用:rect1上一次符合条件的候补焦点,rect2当前遍历的view
    boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
        final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);// rect1和当前焦点在相对于导航方向的垂直方向是否有重叠,导航方向为左右x轴时比较y轴重叠
        final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);// rect2和当前焦点在相对于导航方向的垂直方向是否有重叠,导航方向为上下y轴时比较x轴重叠

        // if rect1 isn't exclusively in the src beam, it doesn't win
        if (rect2InSrcBeam || !rect1InSrcBeam) {// rect2有重叠,或者rect1没有重叠
            // 第一次调用:上一次符合条件的候补焦点与当前焦点有重叠,或者当前遍历的view与当前焦点没有重叠
            return false;// 如果第一次进入此return false,下次进来肯定跳过这里
        }

        // we know rect1 is in the beam, and rect2 is not

        // if rect1 is to the direction of, and rect2 is not, rect1 wins.
        // for example, for direction left, if rect1 is to the left of the source
        // and rect2 is below, then we always prefer the in beam rect1, since rect2
        // could be reached by going down.
        if (!isToDirectionOf(direction, source, rect2)) {
            return true;
        }

        // for horizontal directions, being exclusively in beam always wins
        if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
            return true;
        }        

        // for vertical directions, beams only beat up to a point:
        // now, as long as rect2 isn't completely closer, rect1 wins
        // e.g for direction down, completely closer means for rect2's top
        // edge to be closer to the source's top edge than rect1's bottom edge.
        return (majorAxisDistance(direction, source, rect1)
                < majorAxisDistanceToFarEdge(direction, source, rect2));
    }
  • 计算相对于导航方向的垂直方向上是否有重叠
<FocusFinder.java>
    // direction方向,rect1当前焦点,rect2待比较的view
    boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
        switch (direction) {
            case View.FOCUS_LEFT:
            case View.FOCUS_RIGHT:
                return (rect2.bottom > rect1.top) && (rect2.top < rect1.bottom);// 左右按键时比较y方向是否重叠
            case View.FOCUS_UP:
            case View.FOCUS_DOWN:
                return (rect2.right > rect1.left) && (rect2.left < rect1.right);// 上下按键时比较x方向是否重叠
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
  • 计算是否完全在当前src的一侧
<FocusFinder.java>
    // src当前焦点,dest待比较的view
    boolean isToDirectionOf(int direction, Rect src, Rect dest) {// 比较dest是否完全在当前焦点的左/右/上/下
        switch (direction) {
            case View.FOCUS_LEFT:
                return src.left >= dest.right;
            case View.FOCUS_RIGHT:
                return src.right <= dest.left;
            case View.FOCUS_UP:
                return src.top >= dest.bottom;
            case View.FOCUS_DOWN:
                return src.bottom <= dest.top;
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
  • 计算主轴方向距离
<FocusFinder.java>
    // 计算主轴方向距离
    static int majorAxisDistance(int direction, Rect source, Rect dest) {
        return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
    }

    static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
        switch (direction) {
            case View.FOCUS_LEFT:
                return source.left - dest.right;
            case View.FOCUS_RIGHT:
                return dest.left - source.right;
            case View.FOCUS_UP:
                return source.top - dest.bottom;
            case View.FOCUS_DOWN:
                return dest.top - source.bottom;
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
  • 计算相对于主轴方向的垂直方向距离
<FocusFinder.java>
    // 计算次轴方向距离
    static int minorAxisDistance(int direction, Rect source, Rect dest) {
        switch (direction) {
            case View.FOCUS_LEFT:
            case View.FOCUS_RIGHT:
                // the distance between the center verticals
                return Math.abs(
                        ((source.top + source.height() / 2) -
                        ((dest.top + dest.height() / 2))));
            case View.FOCUS_UP:
            case View.FOCUS_DOWN:
                // the distance between the center horizontals
                return Math.abs(
                        ((source.left + source.width() / 2) -
                        ((dest.left + dest.width() / 2))));
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
  • 计算相对距离,以FOCUS_LEFT为例,majorAxisDistance相当于当前焦点左侧与比较view的右侧的x轴距离,minorAxisDistance相当于在y轴方向上,当前焦点中心点与比较view的中心点的距离。计算13 * x² * y²,这个13的权重系数不知道google是如何制定的,这里就理解为主轴的权重优先级更高吧。(如果是我设计的话,应该会直接计算x和y的距离平方根进行比较了。)
<FocusFinder.java>
    int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
        return 13 * majorAxisDistance * majorAxisDistance
                + minorAxisDistance * minorAxisDistance;
    }
majorAxisDistance.jpg
  • 唉,这方法又得和上面的majorAxisDistance进行区分,以FOCUS_LEFT为例,同样是计算x轴方向,但是majorAxisDistance计算的是souce的左侧和待比较view的右侧距离,这个方法计算的是source的左侧和待比较view的左侧的距离:
<FocusFinder.java>
    static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
        return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
    }

    // 也是计算主轴方向,但是和majorAxisDistance有区别
    static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
        switch (direction) {
            case View.FOCUS_LEFT:
                return source.left - dest.left;
            case View.FOCUS_RIGHT:
                return dest.right - source.right;
            case View.FOCUS_UP:
                return source.top - dest.top;
            case View.FOCUS_DOWN:
                return dest.bottom - source.bottom;
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
majorAxisDistanceToFarEdge.jpg

遍历过程中,每次进入isBetterCandidate成立后,closest都会更新为下一个焦点的最优解,遍历结束后,这个closest就是计算出来的下一个焦点,直接返回给上面的ViewRootImpl.performFocusNavigation,至此寻焦结束,接着用该查找出来的焦点view调用requestFocus,requestFocus之前已经分析过,主要就是清除上一个焦点的状态,刷新当前焦点,流程结束。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267