Android的View事件分发机制2

Posted 呼啸

tags:

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

2.源码机械View的事件分发机制

面试的时候经常会被问到View的事件分发,这到底是什么呢?

当我们点击屏幕的时候,就产生了点击事件,这个事件被分装成了一个类MotionEvent。而当这个MotionEvent产生后,系统就会将这个Motion传递给View的层级,MotionEvent在View中的层级传递过程,就叫做事件分发。事件分发有3个重要方法:

1.DispachTouchEvent(MotionEvent ev) :用来进行事件的分发

2.onInterceptTouchEvent(MotionEvent ev);用来进行事件的拦截,在dispatchTouchEvent方法中调用,需要注意的是,ViewGrop才有这个方法。

3.onTouchEvent(MotionEvent ev):用来处理点击事件,在dispatchTouchEvent方法中进行调用。

View的事件分发机制:

当点击事件产生之后,事件首先会传递给当前的Activity,这会调用Activity的dispatchTouchEvent方法。具体的事件处理工作则都是由Activity中的PhoneWindow来完成的。然后PhoneWindow再把事件处理工作交给DecorView,之后,再由DecorView将事件处理工作交给ViewGrop,我们来看下ViewGroup的dispacthTouchEvent方法来分析:

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) 
     ...
        if (onFilterTouchEventForSecurity(ev)) 
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) 
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) 
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) 
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                 else 
                    intercepted = false;
                
             else 
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            

         ....
        return handled;
    

这里,首先判断是不是DOWN事件,如果是,则进行初始化。清空之前所保留的状态。在resetTouchState方法中会把mFirstTouchTarget的值设置为null.这里为什么要进行初始化呢?原因就是一个完整的事件是以DOWN开始,以UP结束。所以,如果是DOWN事件,那么说明这是一个新的事件序列,所以需要清空之前的状态。

if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null)

这段代码处,如果条件满足,则执行里面的逻辑,这个mFirstTouchTarget又是啥呢?他用来表示,当前ViewGroup是否拦截了事件,如果拦截了,mFirstTouchTarget = null;如果没有拦截,则交由子View来处理,则mFirstTouchTarget != null.假设,当前的ViewGroup拦截了此事件,mFirstTouchTarget = null,这时候如果触发ACTION_DOWN事件,则会执行onInterceptTouchEvent(ev)方法;如果触发的ACTION_MOVE,ACTION_UP事件,则不再执行onInterceptToucheEvent(ev)方法,而是直接设置onInterceptTouchEvent方法,此后的一系列事件,均由这个ViewGroup处理。

接着往下看,这句代码:

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

这里有个FLAG_DISALLOW_INTERCEPT标志,这个标志的作用是禁止ViewGroup拦截除了Down之外的事件,一般通过子View的requestDisallowInterceptTouchEvent来设置。

我们来总结一下:

当ViewGroup要拦截事件的时候,那么后续的事件序列都交给它处理,而不用再调用onInterceptTouchEvent方法了。所以,onInterceptTouchEvent方法并不是每次事件都会被调用的。接下来我们查看下onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent ev) 
       
        return false;
    

这里是直接返回false.也就是默认是不拦截的。这个可以理解。毕竟ViewGroup都拦截了,咱们下面的view还接收个啥。所以当我们想拦截的时候,在我们的viewGroup里,就要重写这个方法。

我们接着再看看dispatchTouchEvent(MotionEvent ev)的剩余部分代码:

 final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) 
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) 
                                if (childWithAccessibilityFocus != child) 
                                    continue;
                                
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            

                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) 
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) 
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) 
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) 
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) 
                                        if (children[childIndex] == mChildren[j]) 
                                            mLastTouchDownIndex = j;
                                            break;
                                        
                                    
                                 else 
                                    mLastTouchDownIndex = childIndex;
                                
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);

在代码的开头有一个循环:

final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) 

这个循环是遍历ViewGroup的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到点击事件,则交由子元素来处理。需要注意的是,这个for循环是倒序遍历的,即从最上层的子View开始往内层遍历。比如我们写XML布局,那就是从XML文件最后写的view开始遍历。

接着看这句代码:

if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) 
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            

这句代码的意思就是判断下触摸点的位置是否在子View的范围内,或者子View是否在播放动画。如果这两个条件都不复合,则view的焦点不可访问,执行continue,执行遍历下一个view.

接着我们再来看看这句代码:

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) 

我们点击这个dispatchTransformedTouchEvent方法看看:

   /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) 
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) 
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) 
                handled = super.dispatchTouchEvent(event);
             else 
                handled = child.dispatchTouchEvent(event);
            
            event.setAction(oldAction);
            return handled;
        
...

这个代码里的内容主要是:

如果有子View,则调用子View的dispatchTouchEvent方法。如果ViewGroup没有子View,则调用super.dispatchTouchEvent(event)方法。ViewGroup是继承自View的。那么我们看看这个super.dipatchTouchEvent,也就是view.dispatchTouchEvent方法:


    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) 
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) 
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) 
                return false;
            
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        
        boolean result = false;

        if (mInputEventConsistencyVerifier != null) 
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) 
            // Defensive cleanup for new gesture
            stopNestedScroll();
        

        if (onFilterTouchEventForSecurity(event)) 
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) 
                result = true;
            
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) 
                result = true;
            

            if (!result && onTouchEvent(event)) 
                result = true;
            
        

        if (!result && mInputEventConsistencyVerifier != null) 
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) 
            stopNestedScroll();
        

        return result;
    

可以看到,如果OnTouchListener 不为null 并且onTouch方法返回true,则表示事件被消费。就不会执行onTouchEvent(event),否则则会执行。可以看出,OnTouchListener的onTouch方法优先级要高于,onTouchEvent(event)方法。接着我们再来OnTouchEvent方法可以看到,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么onTouchEvent()就会返回true消耗这个事件。CLICKABLE和LONG_CLICKABLE代表View可以被点击和长按点击。这可以通过View的setClickable和setLongClickable来设置。也通过setOnClickListener和setLongClickListener来设置。接着,在ACTION_UP中调用performClick方法:

 public boolean performClick() 
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) 
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
         else 
            result = false;
        

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    

可以看到,如果View设置了点击事件OnClickListener,那么onClick方法就被执行。这个View事件分发机制的传递过程。

我们可以从头到尾再总结一遍:

当一个点击事件发生,事件首先传递给activity,调用activity的dispatchTouEvent然后调用phoneWindow的这个方法,然后再给decorView,然后再给根viewGroup。在viewGroup的dispatchTouEvent方法中,先判断是否DOWN事件,如果是,则进行一些必要的初始化。接着就会看是否拦截,如果是拦截,这个时候如果事件是DOWN,则会执行onInterceptTouchEvent。如果是move,up则不会再调用onInterceptTouchEvent方法。而是直接设置intercepted为true.此后的一个事件序列军由这个ViewGroup处理。注意,如果是DOWN事件,并且设置了FLAG_DISALLOW_INTERCEPT标志位,则是禁止ViewGroup拦截除了DOWN以外的其他事件。这个标志位可以通过子view的requestDisallowInterceptTouchEvent来设置。

默认onInterceptTouchEvent不拦截,返回false,如果拦截,则需要重写这个方法。

ViewGroup在dispatchTouchEvent方法中,会对子元素进行倒序遍历,判断子元素是否能接收到点击事件。接着再判断触摸点的位置是否在子元素的范围内或者子元素在播放动画。会有一个接着判断这个子View是否为null,如果不是,则调用子View的dispacthTouchEvent。如果是,则调用ViewGroup的super.dispatchTouchEvent.也就是View.dispatchTouchEvent.在这个方法中,会判断事件是否被消费。如果onTouchListener不为null,并且onTouch方法返回true,说明被消费。就不会执行onTouchEvent(event)否则就会执行。在这个方法中,只要view的clickable和long_clickable有一个为true,那么onTouchEvent()就会返回true,消耗这个事件。然后再ACION_UP中来执行perfromClick。

以上是关于Android的View事件分发机制2的主要内容,如果未能解决你的问题,请参考以下文章

Android的View事件分发机制2

Android View的事件分发机制

浅谈Android 事件分发机制

Android View 事件分发机制

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

Android中View的事件分发机制