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

来源:互联网 发布:js字符串包含某个字符 编辑:程序博客网 时间:2024/05/17 11:56

前言

为什么要有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.javapublic 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.javapublic 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.javapublic 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<View> 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<View> 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<View> 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)));    }
5 0
原创粉丝点击