• 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

  • 相关阅读:
    win10 安装cmake报错: "Installation directory must be on a local drive"
    python量化笔记16之夏普比率计算公式
    python机器量化之十七:混淆矩阵(confusion_matrix)含义
    使用Aspose.word (Java) 填充word文档数据
    python笔记(十八)机器量化分析—数据采集、预处理与建模
    js 使FORM表单的所有元素不可编辑的示例代码 表
    Web前端面试题:写一个mul函数
    vue + electron 快速入门
    Java新特性stream流
    mycat中间件进行MySQL数据表的水平拆分
  • 原文地址:https://www.cnblogs.com/qhyuan1992/p/6071981.html
Copyright © 2020-2023  润新知