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 boolean onInterceptTouchEvent(MotionEvent ev) 

    { 

        int action = ev.getAction(); 

        switch (action) 

        { 

        case MotionEvent.ACTION_DOWN: 

            return true ;  

        case MotionEvent.ACTION_MOVE: 

            return true ;  

        case MotionEvent.ACTION_UP: 

            return true ;  

        } 

         

        return false; 

    } 

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

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

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

拦截move事件,子view会处理cancel事件,父view也不会处理,

 

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

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

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

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。