从Android源码的角度理解应用开发(2)-Focus机制

前言

为什么要有Focus机制

这是因为,如果界面上有两个按钮,假设你按了回车,这时候究竟代表着你按了哪个按钮呢?这时候就需要Focus来帮忙了,因为如果一个View得到焦点,那么这个View就可以处理键盘的输入,做出回应。

两种模式

然后Android的设备现在大多数都是触屏的,键盘非常少,但是还有类似键盘的输入类似TV的DPad。键盘输入与触屏输入是一对有矛盾的交互设计方式。所以Android有两个模式来分别对待这两种交互方式,触摸模式(TouchMode)普通模式,普通模式以键盘按下开始,触屏(Pointer操作,包括触屏,鼠标操作)为结束,而触摸模式相反,触屏为开始,触摸模式以键盘按下结束。

Focus机制必不可少

显然,普通模式是需要Focus机制来支持键盘Dpad等操作,但这并不代表着触摸模式就不需要焦点机制,比如手机需要打字时候软键盘需要对EditText进行输入,EditText就获取了焦点。

触摸模式与普通模式的切换

触摸模式与普通模式是通过ViewRootImpl中的ensureTouchModeLocally(boolean)来进行切换。

/**
* Ensure that the touch mode for this window is set, and if it is changing,
 * take the appropriate action.
 * @param inTouchMode Whether we want to be in touch mode.
 * @return True if the touch mode changed and focus changed was changed as a result
 */
private boolean ensureTouchModeLocally(boolean inTouchMode) {
    if (DBG) Log.d("touchmode", "ensureTouchModeLocally(" + inTouchMode + "), current "
            + "touch mode is " + mAttachInfo.mInTouchMode);

    if (mAttachInfo.mInTouchMode == inTouchMode) return false;

    mAttachInfo.mInTouchMode = inTouchMode;
    mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(inTouchMode);

    return (inTouchMode) ? enterTouchMode() : leaveTouchMode();
}

所以我们只需要找到ensureTouchModeLocally(boolean)在哪些地方切换就能知道什么时候进入触摸模式与退出触摸模式。

初始化模式

新建ViewRootImpl是否进入触摸模式是由WindowManagerService中的mInTouchModemInTouchMode代表着当前系统是否在TouchMode环境下。而mInTouchMode的开机初始化值由R.bool.config_defaultInTouchMode决定,之后将随用户对系统的操作决定。

用户操作:进入触摸模式

用户操作进入触摸模式的情况非常单一,代码在ViewRootImpl中

final class EarlyPostImeInputStage extends InputStage{
    ···
     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);
            }
        }
        return FORWARD;
    }
    private int processPointerEvent(QueuedInputEvent q) {
       final MotionEvent event = (MotionEvent)q.mEvent;
        ···

        // Enter touch mode on down or scroll.
        final int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) {
            ensureTouchMode(true); //进入触摸模式
        }
     ···
        return FORWARD;
     }
    ···
}

可以看到当有Pointer操作(鼠标,触摸)传到EarlyPostImeStage时候,Down操作与Scroll操作将进入触摸模式

用户操作:退出触摸模式

退出方法1

同理,在EarlyPostImeStage中会检测用户是不是使用Dpad或者键盘输入,如果是,也会退出触摸模式

```
final class EarlyPostImeInputStage extends InputStage{
    ···
     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);
            }
        }
        return FORWARD;
    }
    private int processKeyEvent(QueuedInputEvent q) {
       final KeyEvent event = (KeyEvent)q.mEvent;

        // If the key's purpose is to exit touch mode then we consume it
        // and consider it handled.
        if (checkForLeavingTouchModeAndConsume(event)) {
            return FINISH_HANDLED;
        }

        // Make sure the fallback event policy sees all keys that will be
        // delivered to the view hierarchy.
        mFallbackEventHandler.preDispatchKeyEvent(event);
        return FORWARD;
    }

    ···
}

private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
   // Only relevant in touch mode.
   if (!mAttachInfo.mInTouchMode) {
       return false;
   }

   // Only consider leaving touch mode on DOWN or MULTIPLE actions, never on UP.
   final int action = event.getAction();
   if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
       return false;
   }

   // Don't leave touch mode if the IME told us not to.
   if ((event.getFlags() & KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
       return false;
   }

   // 1.导航键退出触摸模式
   if (isNavigationKey(event)) {
       return ensureTouchMode(false);
   }

   // 2.键盘输入退出触摸模式
   if (isTypingKey(event)) {
       ensureTouchMode(false);
       return false;
   }

   return false;
}

退出方法2

退出TouchMode还可能通过辅助功能来退出,当用辅助功能转移焦点时候,就会退出触摸模式

//View.java
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
    ···
     switch (action) {
           ···
          case AccessibilityNodeInfo.ACTION_FOCUS: {
              if (!hasFocus()) {
                  // Get out of touch mode since accessibility
                  // wants to move focus around.
                  getViewRootImpl().ensureTouchMode(false);
                  return requestFocus();
              }
          } break;
          ···
     }
     ···
}

退出方法3

还有第三种退出触摸模式的方式:通过requestFocusFromTouch()这个方法相当于退出触摸模式后再调用一次requestFocus()

public final boolean requestFocusFromTouch() {
    // Leave touch mode if we need to
    if (isInTouchMode()) {
        ViewRootImpl viewRoot = getViewRootImpl();
        if (viewRoot != null) {
            viewRoot.ensureTouchMode(false);
        }
    }
    return requestFocus(View.FOCUS_DOWN);
}

因为有些大多数View只设置了Focusable属性,但是没有设置FocusableInTouchMode属性,在触摸模式情况下,只设置Focusable属性没有设置FocusableInTouchMode属性的View是无法获取焦点的,所以调用requestFocus会无效。所以必须调用requestFocusFromTouch先退出触摸模式后获取焦点。

焦点查找

ViewRootImpl部分

对于轨迹球的上下左右,键盘的上下左右,tab,shift-tab,或者Dpad的上下左右,如果当前焦点没有消费完事件,会触发系统自动寻找下个焦点。关键代码如下:

/**
* Delivers post-ime input events to the view hierarchy.
 */
final class ViewPostImeInputStage extends InputStage {
    ···
    @Override
    protected int onProcess(QueuedInputEvent q) {
        //1. 如果事件是KeyEvent,会走这里,接下来触发焦点转移(虽然轨迹球上下左右不是KeyEvent,但是可能会在人工合成阶段转化成KeyEvent)
        if (q.mEvent instanceof KeyEvent) {
            return processKeyEvent(q);
        } else {
            ···
        }
    }

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

        // Deliver the key to the view hierarchy.
        if (mView.dispatchKeyEvent(event)) {
            return FINISH_HANDLED;
        }

        if (shouldDropInputEvent(q)) {
            return FINISH_NOT_HANDLED;
        }

        // If the Control modifier is held, try to interpret the key as a shortcut.
        if (event.getAction() == KeyEvent.ACTION_DOWN
                && event.isCtrlPressed()
                && event.getRepeatCount() == 0
                && !KeyEvent.isModifierKey(event.getKeyCode())) {
            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;
        }

        // 2.KeyEvent如果没有被焦点消费的话,走这里
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            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;
            }
            // 3.焦点主要逻辑部分
            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 FINISH_HANDLED;
                        }
                    }

                    // Give the focused view a last chance to handle the dpad key.
                    if (mView.dispatchUnhandledMove(focused, direction)) {
                        return FINISH_HANDLED;
                    }
                } else {
                    // find the best view to give focus to in this non-touch-mode with no-focus
                    View v = focusSearch(null, direction);
                    if (v != null && v.requestFocus(direction)) {
                        return FINISH_HANDLED;
                    }
                }
            }
        }
        return FORWARD;
    }
}

从第3点焦点的主要逻辑部分可以看到,主要通过调用View(ViewGroup)的findFocus来做一次树高度的查找,从上到下找到当前焦点,再通过ViewRootImpl,ViewGroup或View的focusSearch查找,从下到上查到下一个焦点,并且对下一个焦点进行对焦。

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

//ViewGroup.java
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.java
 public View focusSearch(View focused, int direction) {
    checkThread();
    if (!(mView instanceof ViewGroup)) {
        return null;
    }
    return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);
}

FocusFinder部分

从上面逻辑可以看出,最后都将调用到FocusFinder的findNextFocus(viewgroup,view,direction),对于第一个参数viewgroup,就是window中addView时候添加的View,也就是decorView。对于第二个参数view,如果没有当前没有焦点则为null,否则就是焦点。所以接下来看findNextFocus(viewgroup,view,direction)的实现。

//FocusFinder.java
public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
        mFocusedRect.set(focusedRect);
        return findNextFocus(root, null, mFocusedRect, direction);
    }

    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        View next = null;
        if (focused != null) {
            next = findNextUserSpecifiedFocus(root, focused, direction);
        }
        if (next != null) {
            return next;
        }
        ArrayList focusables = mTempList;
        try {
            focusables.clear();
            root.addFocusables(focusables, direction);//1.添加可能获取到焦点的View
            if (!focusables.isEmpty()) {
                next = findNextFocus(root, focused, focusedRect, direction, focusables);//2.确定焦点区域            }
        } finally {
            focusables.clear();
        }
        return next;
    }

以上代码比较关键的逻辑就是1,2
对于第1点,添加可能的焦点集合,对于addFocusable的逻辑主要跟desendantFocusability属性有关,分为三种,block,after,before。addFocusable会遍历整个View树,如果某个节点是block,则不会添加次节点的子节点;如果当前节点是before,则会添加子节点与当前节点;如果当前节点是after,则只会在子节点全都不能获取焦点的情况下添加当前节点。注意这里能不能获取到焦点跟当前焦点的模式有关,如果是触摸模式,只有focusableInTouchMode的节点来能获取焦点,如果是普通模式,则只需要focusable即可获取焦点。

对于第2点,终于到了确定焦点区域。

    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
            int direction, ArrayList focusables) {
         //1. 确定焦点区域
        if (focused != null) {
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
            }
            // fill in interesting rect from 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;
                    }
                }
            }
        }
        //2 确定算法
        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);
        }
    }

可以知道,如果当前有焦点,则焦点区域的矩形为焦点,如果当前没有焦点,当按下“下”“右”时候,焦点区域为DecorView左上的端点,当按下“左”“上”焦点区域为DecorView右下的断电。然后根据按下的按键,选择findNextFocusInRelativeDirection或者findNextFocusInAbsoluteDirection算法,我们在此只分析findNextFocusInAbsoluteDirection算法:

findNextFocusInAbsoluteDirection

View findNextFocusInAbsoluteDirection(ArrayList focusables, ViewGroup root, View focused,
            Rect focusedRect, int direction) {
        //1.先把匹配矩形设置成最坏的情况,这样在接下来的比较中,总能把这种最坏的情况淘汰掉。
        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;

        int numFocusables = focusables.size();
        //2.遍历Focusables
        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;

            // get focus bounds of other view in same coordinate system
            focusable.getFocusedRect(mOtherRect);
            root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
            //3. 使用比较算法isBetterCandidate来求得最好的匹配结果
            if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
                mBestCandidateRect.set(mOtherRect);
                closest = focusable;
            }
        }
        return closest;
    }

可以看到,通过isBetterCandidate来比较所有focusables,选取最好的情况来作为下一个焦点。

isBetterCandidate

//是否rect1更加匹配
boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {

        //Candidate算法用于方向判断,如果rect1方向不对,那就不能淘汰rect2
        if (!isCandidate(source, rect1, direction)) {
            return false;
        }
        //如果rect2方向不对,但是rect1方向对,那么rect1更加匹配
        if (!isCandidate(source, rect2, direction)) {
            return true;
        }

        // beamBeats算法用于比较rect1,rect2主要通过在direction方向上是否重叠以及距离来比较
        if (beamBeats(direction, source, rect1, rect2)) {
            return true;
        }

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

        // 以上都比较不了,那么就用主次轴方向上距离的比较来算出结果
        return (getWeightedDistanceFor(
                        majorAxisDistance(direction, source, rect1),
                        minorAxisDistance(direction, source, rect1))
                < getWeightedDistanceFor(
                        majorAxisDistance(direction, source, rect2),
                        minorAxisDistance(direction, source, rect2)));
    }

你可能感兴趣的:(从Android源码的角度理解应用开发(2)-Focus机制)