zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    深入Android 【一】 —— 序及开篇
    Android中ContentProvider和ContentResolver使用入门
    深入Android 【六】 —— 界面构造
    The service cannot be activated because it does not support ASP.NET compatibility. ASP.NET compatibility is enabled for this application. Turn off ASP.NET compatibility mode in the web.config or add the AspNetCompatibilityRequirements attribute to the ser
    Dynamic Business代码片段总结
    对文件的BuildAction以content,resource两种方式的读取
    paraview 3.12.0 windows下编译成功 小记
    百度网盘PanDownload使用Aria2满速下载
    netdata的安装与使用
    用PS给证件照排版教程
  • 原文地址:https://www.cnblogs.com/qhyuan1992/p/6071981.html
Copyright © 2011-2022 走看看