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

Posted 单灿灿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android焦点事件分发与传递机制相关的知识,希望对你有一定的参考价值。

如果您对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焦点事件分发与传递机制的主要内容,如果未能解决你的问题,请参考以下文章

Android View 事件分发机制

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )

Android事件分发机制

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 五 )

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 三 )

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 二 )