Android之View和ViewGroup事件分发

学习android一段时间了,觉得事件分发是一个比较难的部分,自定义控件需要这方面的知识,因此花了一段时间研究了一下,在此记录下自己学习的过程,供以后学习使用。

View的时间分发过程dispatchTouchEvent—> onTouch –-> onTouchEvent

    /**
     * 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 (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

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

            if (onTouchEvent(event)) {
                return true;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }

dispatchTouchEvent的返回值由是否设置触摸回调的返回值或者onTouchEvent的返回值决定。

一个触摸事件发生后,首先如果设置了触摸事件的侦听,并且返回了true,表示该事件已经被消费了,dispatchTouchEvent方法会直接返回true。如果回调返回了false。那么会执行onTouchEvent方法,dispatchTouchEvent的返回值由onTouchEvent的返回值决定。默认情况下,只要可以点击并且是enable的,都会返回true。下面是onTouchEvent方法

 /**
     * Implement this method to handle touch screen motion events.
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true);
                       }

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }
            return true;
        }

        return false;
}

长按事件是在ACTION_DOWN 里面执行的,点击事件是在ACTION_UP里面执行的。长按事件的执行逻辑如下:

    class CheckForLongPress implements Runnable {

        private int mOriginalWindowAttachCount;

        public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick()) {
                    mHasPerformedLongPress = true;
                }
            }
        }

public boolean performLongClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {
            handled = showContextMenu();
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

如果执行了长按事件,并且长按的回调返回了true,那么mHasPerformedLongPress= true就不会再去执行performClick();

因此想要长按事件和单击事件同时发生,就要在长按的回调函数返回false。

单击事件和长按事件其实都是封装在触摸事件里面的。

ViewGroup的事件分发过程dispatchTouchEvent---> onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {  
    return false;  
}

默认情况下onInterceptTouchEvent返回false,即不拦截。

/** 
 * {@inheritDoc} 
 */  
@Override  
public boolean dispatchTouchEvent(MotionEvent ev) {  
    if (!onFilterTouchEventForSecurity(ev)) {  
        return false;  
    }  
  
    final int action = ev.getAction();  
    final float xf = ev.getX();  
    final float yf = ev.getY();  
    final float scrolledXFloat = xf + mScrollX;  
    final float scrolledYFloat = yf + mScrollY;  
    final Rect frame = mTempRect;  
    // 是否禁用拦截,如果为true表示不能拦截事件;反之,则为可以拦截事件  
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    // ACTION_DOWN事件,即按下事件  
    if (action == MotionEvent.ACTION_DOWN) {  
        if (mMotionTarget != null) {  
            // this is weird, we got a pen down, but we thought it was  
            // already down!  
            // XXX: We should probably send an ACTION_UP to the current  
            // target.  
            mMotionTarget = null;  
        }  
        // If we're disallowing intercept or if we're allowing and we didn't  
        // intercept。如果不允许事件拦截或者不拦截该事件,那么执行下面的操作  
        if (disallowIntercept || !onInterceptTouchEvent(ev))         // 1、是否禁用拦截、是否拦截事件的判断  
            // reset this event's action (just to protect ourselves)  
            ev.setAction(MotionEvent.ACTION_DOWN);  
            // We know we want to dispatch the event down, find a child  
            // who can handle it, start with the front-most child.  
            final int scrolledXInt = (int) scrolledXFloat;  
            final int scrolledYInt = (int) scrolledYFloat;  
            final View[] children = mChildren;  
            final int count = mChildrenCount;  
  
            for (int i = count - 1; i >= 0; i--)        // 2、迭代所有子view,查找触摸事件在哪个子view的坐标范围内  
                final View child = children[i];  
                // 该child是可见的  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                        || child.getAnimation() != null) {  
                    // 3、获取child的坐标范围  
                    child.getHitRect(frame);                 
                    // 4、判断发生该事件坐标是否在该child坐标范围内  
                    if (frame.contains(scrolledXInt, scrolledYInt))      
                        // offset the event to the view's coordinate system  
                        final float xc = scrolledXFloat - child.mLeft;  
                        final float yc = scrolledYFloat - child.mTop;  
                        ev.setLocation(xc, yc);  
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
                        // 5、child处理该事件,如果返回true,那么mMotionTarget为该child。正常情况下,  
                        // dispatchTouchEvent(ev)的返回值即onTouchEcent的返回值。因此onTouchEcent如果返回为true,  
                        // 那么mMotionTarget为触摸事件所在位置的child。 
                        if (child.dispatchTouchEvent(ev)) 
//默认的实现下View.dispatchTouchEvent(ev)返回值一定为true
                            // Event handled, we have a target now.  
                            mMotionTarget = child;  
                            return true; 
//表示子view已经能将触摸时间消费掉	
                        }  
               
                    }  
                }  
            }  
        }  
    }// end if  
  
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
            (action == MotionEvent.ACTION_CANCEL);  
  
    if (isUpOrCancel) {  
        // Note, we've already copied the previous state to our local  
        // variable, so this takes effect on the next event  
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
    }  
  
    // The event wasn't an ACTION_DOWN, dispatch it to our target if  
    // we have one.  
    final View target = mMotionTarget;  
    // 6、如果mMotionTarget为空,那么执行super.dispatchTouchEvent(ev),  
    // 即View.dispatchTouchEvent(ev),就是该View Group自己处理该touch事件,只是又走了一遍View的分发过程而已. (指没有找到view,也可能是下面两种情况) 
// 1,拦截事件 或者2.在不拦截事件target view的onTouchEvent返回false的情况都会执行到这一步.  这种情况下
//执行super.dispatchTouchEvent(ev);也就是当成view来分发事件,过程同 view的时间分发过程一致 
    if (target == null) {  
        // We don't have a target, this means we're handling the  
        // event as a regular view.  
        ev.setLocation(xf, yf);  
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        }  
        return super.dispatchTouchEvent(ev); 
// 调用super.dispatchTouchEvent(ev); 表示子view没能将触摸时间消费掉,就会将触摸事件传递给父view

    }  
  
    // if have a target, see if we're allowed to and want to intercept its  
    // events  
// 7、如果没有禁用事件拦截,并且onInterceptTouchEvent(ev)返回为true,即进行事件拦截.  
//-----似乎只有target!=null也就是子view处理down还返回true,然后拦截事件发生了才会执行下面的if
//也就是对move 和 up 事件的拦截会执行到这里。
//由于在down时child.dispatchTouchEvent(ev)返回了true,所以target有了值。下面的代码是让子view执行ACTION_CANCEL事件
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
        final float xc = scrolledXFloat - (float) target.mLeft;  
        final float yc = scrolledYFloat - (float) target.mTop;  
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        ev.setLocation(xc, yc);  
        //   
        if (!target.dispatchTouchEvent(ev)) {
//拦截move事件,会让子view分发cancel事件  
            // target didn't handle ACTION_CANCEL. not much we can do  
            // but they should have.  
        }  
        // clear the target  
        mMotionTarget = null;  
        // Don't dispatch this event to our own view, because we already  
        // saw it when intercepting; we just want to give the following  
        // event to the normal onTouchEvent().  
        return true;  
    }  
  
    if (isUpOrCancel) {  
        mMotionTarget = null;  
    }  
  
    // finally offset the event to the target's coordinate system and  
    // dispatch the event.  
    final float xc = scrolledXFloat - (float) target.mLeft;  
    final float yc = scrolledYFloat - (float) target.mTop;  
    ev.setLocation(xc, yc);  
  
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        mMotionTarget = null;  
    }  
// 事件不拦截,且target view在ACTION_DOWN时返回true,那么后续事件由target来处理事件  
// 执行到这里的条件是子view 在ACTION_DOWN时返回true,这样target不为null,并且
//还不会执行target == null 的判断才会执行到这里
    return target.dispatchTouchEvent(ev);  
}

拦截的使用方法

@Override 
    public booleanonInterceptTouchEvent(MotionEvent ev) 
    { 
        int action =ev.getAction(); 
        switch (action) 
        { 
        case MotionEvent.ACTION_DOWN: 
            return true ;  
        caseMotionEvent.ACTION_MOVE: 
            return true ;  
        caseMotionEvent.ACTION_UP: 
            return true ;  
        } 
         
        return false; 
    } 

1.如果你在DOWNretrun true ,则DOWN,MOVE,UP子View都不会捕获事件;onInterceptTouchEvent(ev) return true的时候,mMotionTarget 为null ;

2.如果你在MOVEreturn true , 则子View在MOVE和UP都不会捕获事件。onInterceptTouchEvent(ev) return true的时候,此时target还不为null,会执行target.dispatchTouchEvent(ev)来分发cancel事件,接着会把mMotionTarget置为null ;

3.拦截down事件,会执行到target==null,然后调用super.dispatchTouchEvent(ev);即父view来处理。

4.拦截move事件,子view会处理cancel事件,父view不会处理该触摸事件,并且让mMotionTarget = null;结果return true。认为这一个动作已完成,不会再回传到父控件的OnTouchEvent中处理(通过源代码发现确实是这样子)。但是实际上触摸事件是一连串事件,下一个move事件发生后,会判断target == null,执行return super.dispatchTouchEvent(ev);也就是让拦截的ViewGroup来处理后续的MOVE、UP事件(我是这么理解的^_^)。
总结一下就是:触摸事件对DOWN事件不进行拦截,因此DOWN事件可以被子View正常的处理。但是在MOVE时对事件进行了拦截,那么子View就无法接收到MOVE以及后面的事件了,它会收到一个CANCEL事件,后续的事件将会被拦截的ViewGroup的onTouchEvent进行处理。

5.requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截

如果ViewGroup的onInterceptTouchEvent(ev)当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;那还有补救的措施。requestDisallowInterceptTouchEvent(true)便可以使子view接收到,因为会跳过if(!disallowIntercept && onInterceptTouchEvent(ev)),执行returntarget.dispatchTouchEvent(ev);通过源码很容易解释。

6.但是如果是在ACTION_DOWN时返回true来拦截的,那么子view无论怎么做都不可能捕获任何事件,因为此时target == null,肯定会执行return super.dispatchTouchEvent(ev);

 7.onTouchEvent收到ACTION_DOWN,是否一定能收到ACTION_MOVE,ACTION_UP?收到ACTION_MOVE,能否说明它已经收到过ACTION_DOWN?

第一个如果MOVE和UP被父View拦截就收不到了。第二个可能是ViewGroup拦截了子View的MOVE事件,虽然DOWN事件传递了下去被子View消费,但是由于拦截了MOVE接下来会在ViewGroup中处理MOVE事件。

最后,一个重要的知识点:一般的View默认都是不可点击的。例如:View、ViewGroup、ImageView。但所有的View默认都是enable的除非设置为disable。这个时候就要小心:他们在处理默认的onTouchEvent的时候返回的是false!(disable并且clickable或者longclickable会返回true表明已经被消费)返回false意味着对它们的touch事件会向上抛。

参考的文章:http://blog.csdn.net/lmj623565791/article/details/39102591

http://blog.csdn.net/lmj623565791/article/details/38960443

http://blog.csdn.net/xiaanming/article/details/21696315

原文地址:https://www.cnblogs.com/qhyuan1992/p/6071981.html