美文网首页
Android焦点事件分发与传递机制

Android焦点事件分发与传递机制

作者: 苏州韭菜明 | 来源:发表于2017-05-15 20:09 被阅读774次

    如果您对TouchEvent事件分发机制不太了解的,可以参考我的这篇文章——安卓TounchEvent事件分发机制。

    问题:TV端焦点满天飞,如何解决和处理?

    记得初入TV开发,以为很简单。TV的这些界面与布局太简单了,分分钟就可以把页面搭建出来,处理好,然后就没有然后了。。。。

    下面我们就从源码来带大家进行安卓TV焦点事件的传递

    这里先给出Android系统View的绘制流程:

    依次执行View类里面的如下三个方法:

    • measure(int ,int) :测量View的大小
    • layout(int ,int ,int ,int) :设置子View的位置
    • draw(Canvas) :绘制View内容到Canvas画布上

    ViewRootImpl的主要作用如下(此处不多讲,如有意图,看源码):

    • A:链接WindowManager和DecorView的纽带,更广一点可以说是Window和View之间的纽带。

    • B:完成View的绘制过程,包括measure、layout、draw过程。

    • C:向DecorView分发收到的用户发起的event事件,如按键触屏等事件。

    ViewRootImpl不再多余叙述,进入正题:

    Android焦点分发的主要方法以及拦截方法的讲解。

    在RootViewImpl中的函数通道是各种策略(InputStage)的组合,各策略负责的任务不同,如SyntheticInputStage、ViewPostImeInputStage、NativePostImeInputStage等等,这些策略以链表结构结构起来,当一个策略者没有消费事件时,就传递个下一个策略者。其中触摸和按键事件由ViewPostImeInputStage处理。

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

    processKeyEvent(QueuedInputEvent q)源码如下:

       @Override
            protected void onDeliverToNext(QueuedInputEvent q) {
                if (mUnbufferedInputDispatch
                        && q.mEvent instanceof MotionEvent
                        && ((MotionEvent)q.mEvent).isTouchEvent()
                        && isTerminalInputEvent(q.mEvent)) {
                    mUnbufferedInputDispatch = false;
                    scheduleConsumeBatchedInput();
                }
                super.onDeliverToNext(q);
            }
    
            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;
                }
    
                // Handle automatic focus changes.
                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;
                    }
                    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;
            }
    

    进入源码讲解:

    (1) 首先由dispatchKeyEvent进行焦点的分发

    如果dispatchKeyEvent方法返回true,那么下面的焦点查找步骤就不会继续了。

    dispatchKeyEvent方法返回true代表事件(包括焦点和按键)被消费了。

    dispatchKeyEvent(event)如果不了解,看我上一篇文章安卓TounchEvent事件分发机制。

    mView的dispatchKeyEvent方法,
    mView是是Activity的顶层容器DecorView,它是一FrameLayout

    所以这里的dispatchKeyEvent方法应该执行的是ViewGroup的dispatchKeyEvent()方法,而不是View的dispatchKeyEvent方法。

     @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onKeyEvent(event, 1);
            }
    
            if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
                    == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
                if (super.dispatchKeyEvent(event)) {
                    return true;
                }
            } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                    == PFLAG_HAS_BOUNDS) {
                if (mFocused.dispatchKeyEvent(event)) {
                    return true;
                }
            }
    
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
            }
            return false;
        }
    

    ViewGroup的dispatchKeyEvent简略执行流程

    首先ViewGroup会执行父类的dispatchKeyEvent方法,如果返回true那么父类的dispatchKeyEvent方法就会返回true,也就代表父类消费了该焦点事件,那么焦点事件自然就不会往下进行分发

    然后ViewGroup会判断mFocused这个view是否为空如果为空就会****return false,焦点继续往下传递;如果不为空,那就会return mFocused的dispatchKeyEvent方法返回的结果。这个mFocused是ViewGroup中当前获取焦点的子View,这个可以从requestChildFocus方法中得到答案。

    requestChildFocus()的源码如下:

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

    居然有这个彩蛋?

    View的dispatchKeyEvent简略执行流程

    public boolean dispatchKeyEvent(KeyEvent event) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onKeyEvent(event, 0);
            }
    
            // Give any attached key listener a first crack at the event.
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
                return true;
            }
    
            if (event.dispatch(this, mAttachInfo != null
                    ? mAttachInfo.mKeyDispatchState : null, this)) {
                return true;
            }
    
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
            }
            return false;
        }
    

    要修改ViewGroup焦点事件的分发:

    • 重写view的dispatchKeyEvent方法
    • 给某个子view设置onKeyListener监听

    焦点没有被dispatchKeyEvent拦截的情况下的继续代码中的处理过程,还是进入ViewRootImpl源码

                // Handle automatic focus changes.
                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;
                    }
                    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;
                            }
                        }
                    }
                }
    

    dispatchKeyEvent方法返回false后,先得到按键的方向direction一个int值。direction值是后面来进行焦点查找的。

    接着会调用DecorView的findFocus()方法一层一层往下查找已经获取焦点的子View。

    DecorView则是PhoneWindow类的一个内部类,继承于FrameLayout,由此可知它是一个ViewGroup。

    那么,DecroView到底充当了什么样的角色呢?

    其实,DecorView是整个ViewTree的最顶层View,它是一个FrameLayout布局,代表了整个应用的界面。在该布局下面,有标题view内容view这两个子元素。

     @Override
        public View findFocus() {
            if (DBG) {
                System.out.println("Find focus in " + this + ": flags="
                        + isFocused() + ", child=" + mFocused);
            }
    
            if (isFocused()) {
                return this;
            }
    
            if (mFocused != null) {
                return mFocused.findFocus();
            }
            return null;
        }
    

    View的findFocus方法

     /**
         * Find the view in the hierarchy rooted at this view that currently has
         * focus.
         *
         * @return The view that currently has focus, or null if no focused view can
         *         be found.
         */
        public View findFocus() {
            return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
        }
    

    View的hasFocus()方法和isFocused()方法对比

    Stackoverflow解释来了:

    hasFocus() is different from isFocused(). hasFocus() == true means that the View or one of its descendants is focused. If you look closely, there's a chain of hasFocused Views till you reach the View that isFocused.

     /**
         * Returns true if this view has focus itself, or is the ancestor of the
         * view that has focus.
         *
         * @return True if this view has or contains focus, false otherwise.
         */
        @ViewDebug.ExportedProperty(category = "focus")
        public boolean hasFocus() {
            return (mPrivateFlags & PFLAG_FOCUSED) != 0;
        }
    
      /**
         * Returns true if this view has focus
         *
         * @return True if this view has focus, false otherwise.
         */
        @ViewDebug.ExportedProperty(category = "focus")
        public boolean isFocused() {
            return (mPrivateFlags & PFLAG_FOCUSED) != 0;
        }
    

    接着,如果mView.findFocus()方法返回的mFocused不为空,说明找到了当前获取焦点的view(mFocused),接着focusSearch会把direction(遥控器按键按下的方向)作为参数,找到特定方向下一个将要获取焦点的view,最后如果该view不为空,那么就让该view获取焦点。

    我们来看一下focusSearch方法的源码以及具体实现。

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

    focusSearch其实是一层一层地网上调用父View的focusSearch方法,直到当前view是根布局(isRootNamespace()方法),通过注释可以知道focusSearch最终会调用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦点view是通过FocusFinder来找到的。

    FocusFinder是什么?

    根据给定的按键方向,通过当前的获取焦点的View,查找下一个获取焦点的view这样算法的类。焦点没有被拦截的情况下,Android焦点的查找最终都是通过FocusFinder类来实现的。

    FocusFinder是如何通过findNextFocus方法寻找焦点的?

     public final View findNextFocus(ViewGroup root, View focused, int direction) {
            return findNextFocus(root, focused, null, 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);
                if (!focusables.isEmpty()) {
                    next = findNextFocus(root, focused, focusedRect, direction, focusables);
                }
            } finally {
                focusables.clear();
            }
            return next;
        }
        
    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);
                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;
                        }
                    }
                }
            }
            
        
        private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
            // check for user specified next focus
            View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
            if (userSetNextFocus != null && userSetNextFocus.isFocusable()
                    && (!userSetNextFocus.isInTouchMode()
                            || userSetNextFocus.isFocusableInTouchMode())) {
                return userSetNextFocus;
            }
            return null;
        }
    

    FocusFinder类通过findNextFocus来找焦点的。一层一层往寻找,后面会执行findNextUserSpecifiedFocus()方法,这个方法会执行focused(即当前获取焦点的View)的findUserSetNextFocus方法,如果该方法返回的View不为空,
    且isFocusable = true && isInTouchMode() = true的话。

    FocusFinder找到的焦点就是findNextUserSpecifiedFocus()返回的View。

    findNextFocus会优先根据XML里设置的下一个将获取焦点的View的ID值来寻找将要获取焦点的View。

     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 int id = mID;
                    return root.findViewByPredicateInsideOut(this, new Predicate<View>() {
                        @Override
                        public boolean apply(View t) {
                            return t.mNextFocusForwardId == id;
                        }
                    });
                }
            }
            return null;
        }
    

    焦点事件分发步骤:

    • DecorView会调用dispatchKey一层一层进行焦点的分发,如果dispatchKeyEvent方法返回true的话,那么焦点或者按键事件就不会往下分发了。

    • 如果你想拦截某个子View,对其设置OnKeyListener进行焦点的拦截。

    • 如果焦点没有被拦截的话,那么焦点就会交给系统来处理,还是会继续分发,直到找到那个获取焦点的View

    • Android底层先会记录按键的方向,后面DecorView会一层一层往下调用findFocus方法找到当前获取焦点的View

    • 后面系统又会根据按键的方向,执行focusSearch方法来寻找下一个将要获取焦点的View

    • focusSearch内部其实是通过FocusFinder来查找焦点的。FocusFinder会优先通过View在XML布局设置的下一个焦点的ID来查找焦点。

    • 最终如果找到将要获取焦点的View,就让其requestFocus。如果请求无效,将其放在onWindowFocusChanged()这个方法中去请求。这是在Activity寻找到焦点的时候。

    我的前一篇文章,主要是介绍了TouchEvent事件分发机制,省略了焦点分发传递机制的代码,这篇文章与此相反。如果将两个结合起来,太繁杂,冗长了。分开反而有利于您的理解。至此,事件分发机制,你也了解的差不多了,給个粉吧!

    相关文章

      网友评论

          本文标题:Android焦点事件分发与传递机制

          本文链接:https://www.haomeiwen.com/subject/oizhxxtx.html