欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android事件分发处理机制源码分析与知识点总结

程序员文章站 2022-07-03 09:18:14
在Android开发中,事件分发与处理机制是一个重要知识点,在自定义View、处理滑动冲突中具有重要的意义。本文在总结了其机制的基础上,对其源码进行了分析。事件既然是事件分发和处理,那么首先来说说事件。根据面向对象的思想(万物皆对象),在手指接触屏幕产生了触摸事件时,它自然就被封装成了一个对象,这个对象对应的类是MotionEvent。最常见的事件类型有以下3种:ACTION_DOWN —— 手指刚触摸屏幕产生的事件ACTION_MOVE —— 手指在屏幕上滑动产生的事件ACTION_UP——...

在Android开发中,事件分发与处理机制是一个重要知识点,在自定义View、处理滑动冲突中具有重要的意义。本文在总结了其机制的基础上,对其源码进行了分析。

事件

既然是事件分发和处理,那么首先来说说事件。根据面向对象的思想(万物皆对象),在手指接触屏幕产生了触摸事件时,它自然就被封装成了一个对象,这个对象对应的类是MotionEvent。最常见的事件类型有以下3种:

  1. ACTION_DOWN —— 手指刚触摸屏幕产生的事件
  2. ACTION_MOVE —— 手指在屏幕上滑动产生的事件
  3. ACTION_UP—— 手指离开屏幕的一瞬间产生的事件
  4. ACTION_CANCEL —— 事件被上层拦截时产生,正常情况下是不会产生的

MotionEvent类提供了getAction()方法来获取到事件的类型。正常情况下用户手指触摸屏幕会触发一系列事件,我们称之为事件序列,常见的情况有以下两种:

  1. 手指点击屏幕后松开(例如用户点击一个按钮):这个事件的序列为ACTION_DOWN→ACTION_UP
  2. 手指点击屏幕滑动一段时间后在松开(例如用户拖动拖拽条控件):这个事件的序列为ACTION_DOWN→ACTION_MOVE→ACTION_MOVE→……→ACTION_UP

既然提到了事件序列,需要要强调的是:在分析源码时一定要把握住事件序列这个概念。事件不是单个蹦出来的,一来就是一串儿。也就是说,在分析分发或者处理一个事件的同时,要考虑下一个事件到来时,和当前这个事件的流程会有什么区别。

事件分发和处理概述

有了事件的概念之后,就可以说说它的分发和处理了。事件的分发和处理顾名思义分为两个部分:分发处理。分发指的是当一个MotionEvent产生了之后,系统要把它传递给一个具体的View。那么为什么要分发呢?因为一个界面中的布局是由多个View和ViewGroup嵌套的,总不至于当事件产生之后所有的View都要响应这个事件吧,肯定是要把它交给具体的一个View来进行处理。比如说,用户在屏幕上点击了一个按钮,那就要把这个事件分发到按钮这个View中,而不是发给界面中的其他控件。处理就没什么好说的了,就是最终被分发到的View对事件响应的具体方式。

了解了事件分发和处理的概念之后,再来说说它们的规则。

事件分发的规则是自上而下的,它的传递遵循如下顺序:Activity→Window→*ViewGroup(也就是DecorView)→中间层的ViewGroup(如果有的话)→View。这一点是非常好理解的,可以用生活中的例子来说明:比如说公司要接一个新的项目,谁来接啊?肯定是大老板接过来,之后老板将其交给了公司的技术总监,技术总监再交给手下的员工,这样一步一步的往下传,直到传给最底层。那么老板接过来项目之后可不可以不传了,自己接手这个项目呢,或者技术总监接过项目之后不给员工,自己来接手呢?当然也是可以的,但是在大多数情况下却没有必要,有手下为啥要自己搞呢?

事件处理的规则与其恰好相反,是自下而上的,也就是说将上面的顺序反过来。这一点也是很好理解的,比方说你在饭店吃饭,汤里面吃到了一条虫子,这时候你肯定要跟服务员说:去把你们老板叫过来!这是因为服务员没有权力去处理这个意外事件,只有老板才有权决定到底是赔钱还是说再给你做一碗新的。当然这只是一种意外情况(很少发生的),如果你想往火锅里面加点汤,像这种小事就不用麻烦老板,服务员就可以处理了。也就是说,下级处理了也就处理了,处理不了就交给上级处理,这种思想跟Java异常捕捉机制是很像的。

与事件的分发相关的比较重要的方法有以下两个:

  1. ViewGrouppublic boolean dispatchTouchEvent(MotionEvent ev) 方法:用来进行事件分发的方法,当事件传到一个ViewGroup时,此方法一定会被执行,返回结果表示这个View是否消耗当前传来的事件。
  2. ViewGrouppublic boolean onInterceptTouchEvent(MotionEvent ev) 方法:在第一个方法内部调用。用来判断是否拦截事件,返回值表示当前ViewGroup是否拦截这个事件。ViewGroup的onInterceptTouchEvent默认不拦截事件,也就是默认返回false。

这里要注意,以上两个方法都是ViewGroup提供的,这一点是必然的。因为ViewGroup可能有子元素,所以它才有分发事件的资本;而View没有子元素,事件传到View时,根本不用分发,直接进行处理

那么什么叫拦截呢?简单粗暴的解释就是:本来接到的项目是要分发给下级的,但是领导发现这个项目油水很大,那么领导完全可以把它自己私吞了,也就是说不传给下级,下级根本就不知道有这个项目。既然选择拦截,那么ViewGroup当然也就自己处理这个事件了。

与事件处理相关的方法也有两个:

  1. Viewpublic boolean dispatchTouchEvent(MotionEvent ev) 方法:返回结果表示事件是否被消耗。
  2. Viewpublic boolean onTouchEvent(MotionEvent ev) 方法:在第一个方法的内部调用,用来处理事件,返回结果表示事件是否被消耗。

以上几个方法的关系可以用下面的图表示:
Android事件分发处理机制源码分析与知识点总结
总结一下就是:当ViewGroup接收到事件时可以选择拦截,若拦截ViewGroup则自己处理该事件,若不拦截事件就被分发到子元素(如果有子元素的话)。那么子元素就任人宰割了?只要父容器拦截,子元素就永远无法接受到事件了吗?也不是的。子元素可以通过一个标记位mGroupFlags来干涉父容器对事件的拦截,这一点在后面会分析到。

相信说到这里大家可能会有三个疑问:

(1)dispatchTouchEvent(MotionEvent ev)不是用来分发事件的吗,为什么又跟处理相关了呢?

细心一点就会发现,这里的dispatchTouchEvent方法是由View提供的,而View没有子元素,当然不能分发事件,这一点上面已经说过了。不管是View还是ViewGroup,想要处理事件调用的都是View的dispatchTouchEvent方法,这一点如果不理解没关系,暂且当作结论记住好了,在后面的源码分析中就能看出来确实是这样了。

(2)为什么View会有dispatchTouchEvent方法?

我们知道一个View可以注册很多事件监听器,比如onClickListener、onLongClickListener、onTouchListener,并且View自身有onTouchEvent方法,而View的dispatchTouchEvent方法就是用来管理这些与事件处理相关方法的方法。

(3)事件的消耗和处理是一回事儿吗?

乍一听好像没什么区别,其实它们的意义并不完全相同。处理指的是onTouch或者onTouchEvent方法中的代码被执行了,而消耗指的是方法返回值为true,简单说就是:处理不一定消耗。用代码解释就是View虽然执行了onTouchEvent或者onTouch方法中的内容,但是在方法的最后返回了false。

接下来通过一个例子在宏观层面上描述事件分发和处理的流程。
Android事件分发处理机制源码分析与知识点总结
还是之前提到那个公司接项目的例子,上面的图片描述了公司里面的职工等级制度。当老板接到一个项目发给总经理之后,总经理要把这个项目往下级分发(相当于调用了DecorView的superDispatchTouchEvent方法,其实最后调用的就是ViewGroup的dispatchTouchEvent方法),这个时候总经理要考虑一下了,发还是不发。如果他觉得这个项目油水很大那就不发(也就是拦截掉了)自己私吞(处理),总经理处理完这个项目后给老板一个反馈(dispatchTouchEvent返回值),这种情况他的下级压根就不知道有这个项目,也就是说事件根本就没有传到下级。

如果总经理决定分发(不拦截),他手下有3个小弟发给谁呢?这时候他就给下级排个序,然后按排好的顺序依次判断小弟有没有资格接这个项目,根据什么依据呢?当然根据项目的性质了。比如说这个项目是一个技术类项目那么组织部长自然就没资格接手了,虽然他的顺序是第一个(图中从左往右的顺寻)。这样一来技术部长当然有资格接这个项目,那么项目就到了技术部长手里来了,他处理完再给总经理一个反馈,总经理根据技术部长的反馈在给老板反馈,这样一个事件也就处理完了。这个过程宣传部长从始至终都不知道有项目这回事儿,除非技术部长没有消耗掉这个项目,那么总经理才要在宣传部长这里判断一次。

看似简单的流程,可是仔细想想还有一些细节值得推敲:

(1)技术部长处理好了这个项目是一个宏观的说法,真的是技术部长处理的吗?不一定,可能是他处理的也可能是他的手下处理的。事件到了他的手里也要先判断是否要拦截,拦截自己处理不拦截就分发,这个过程和总经理分发项目是一样的,因为不管是总经理还是技术部长他们都是ViewGroup(DecorView也是ViewGroup),调用的都是ViewGroup的dispatchTouchEvent方法。如果技术部长下面还有小头目(ViewGroup),那么分发流程还是一样的,也就是一个递归的过程。从宏观的角度上来讲,不管是谁处理,最后都要给一个反馈,比如说职员3最后处理了这个事件,那么职员3反馈给技术部长,技术部长再向上级反馈,这样一步一步的反馈到老板(Activity)。

(2)考虑这样一种情况,如果总经理的3个小弟都不接这个项目的话,总经理要么自己处理,要么交给老板处理。这样我们得出了一个结论:对于一个ViewGroup来说,如果事件传到了它的手里,那么它有两次机会决定是否消耗掉这个事件:第一次机会是事件刚传到它手里的时候可以选择拦截;第二次机会是如果它的子元素都不处理,那么还可以选择自己消耗。如果这两次机会都放弃了,事件将被它的上级处理。这一点至关重要,它是用内部拦截法解决滑动冲突的前提。

(3)以上讨论的都是处理单独一个事件的,然而事件是以序列的形式存在的,这就相当于老板接的项目是分期的,同一个项目但是要分几期去做,那么如果第1期做完了之后,之后几期处理的流程跟第一期还一样吗?这一点尤为重要,在后面要重点进行分析。

View对事件的处理过程

额,事件不是应该先分发后处理吗,为什么要先说处理呢?这是因为事件的分发机制实在是太复杂了,而且还会在内部多次调用处理的方法;而事件处理的逻辑就显得很清晰,先明白事件处理的流程再看分发的流程会让思路更清晰一些。

不管是View还是ViewGroup,处理事件调用的都是View提供的dispatchTouchEvent方法,这一点在上面已经提过了,所以我们直接在View源码中找到dispatchTouchEvent方法:

	public boolean dispatchTouchEvent(MotionEvent event) {
	
        ……
        
        boolean result = false;
        
    	……

		//注释1***********************************************************************
        if (onFilterTouchEventForSecurity(event)) {
           	……
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            //4个条件
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&li.mOnTouchListener.onTouch(this,event)) {
                result = true;
            }
			//注释2***********************************************************************
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ……
        
        return result;
    }

在注释1处首先会这个事件是否是安全的,一般来说都是安全的所以if里面的语句会被执行,接下来的if判断了4个条件。第一个条件是 mListenerInfo!=null,那么这个mListenerInfo是从哪来的呢?我们看一下View的setOnTouchListener方法:

	public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }
    
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

从代码中可以发现,一旦我们在外部调用了setOnTouchListener方法给View绑定监听器,mListenerInfo就不为空(而且是个单例),而且mOnTouchListener也不为空了(第二个条件也满足了)。也就是说,我们在外部绑定了OnTouchListener监听器之后前两个条件都满足了。第三个条件是判断控件是否使能(默认都是使能的所以这个条件也满足),第四个条件里面调用了监听器里面的onTouch方法。如果onTouch方法返回了true,那么4个条件全满足直接将result置为true,这里就涉及到了Java 中 与运算(&&) 的一个知识点了:result为true,则!result为false,if在判断完 !result为false 之后还会判断注释2下面的onTouchEvent(event)的结果吗?

答案是不会的,因为Java是短路与机制,也就是说:如果监听器的onTouch消耗了事件,则View本身的onTouchEvent就不会被执行了。再简单点说:监听器的onTouch方法优先级要比onTouchEvent高

接下来就看一看View的onTouchEvent到底做了什么吧:

	   	public boolean onTouchEvent(MotionEvent event) {
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
			
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                 || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
                 
		    //注释1**************************************************************************
            if ((viewFlags & ENABLED_MASK) == DISABLED) { 
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return clickable;
            }
            
            if (mTouchDelegate != null) {//注释2**************************************
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }

			//注释3**************************************
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();
                        }
                        if (!clickable) {
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        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, x, y);
                            }

                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                                // 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)) {
                                        performClickInternal(); //注释4**************************************
                                    }
                                }
                            }

                            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();
                        }
                        mIgnoreNextUpEvent = false;
                        break;

                    case MotionEvent.ACTION_DOWN:
                        
                        ……
                        
                        break;

                    case MotionEvent.ACTION_CANCEL:
                         
                        ……
                        
                    case MotionEvent.ACTION_MOVE:
                             
                        ……
                        
                        break;
                }

                return true;
            }

            return false;
        } 

注释1处的代码表示的是如果View处于不可用状态下事件的处理过程,其实不用看具体代码,结合着下面的英文注释我们知道尽管它不可用,但是仍然会消耗这个事件

接着在注释2处,如果View设有代理,就会执行代理对象的onTouchEvent方法了,一般来说我们并不会给View设置代理,所以这块就不深入研究了。

在注释3下面的代码就是对事件具体处理的逻辑了,代码很长,不过我们只看有用的部分即可。首先会判断View是不是可以点击的(LONG_CLICKABLE或CLICKABLE有一个为true),如果该控件是可以点击的就会进入到switch判断中去,在ACTION_UP分支中,注释4处调用了performClickInternal方法,其中又调用了performClick方法:

	private boolean performClickInternal() {
        notifyAutofillManagerOnClick();
        return performClick();
    }
    
    public boolean performClick() {
        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;
    }

在performClick方法中判断了如果 li != null && li.mOnClickListener != null的话就执行onClick方法。跟onTouchListener类似,如果在外界通过setOnClickListener方法给View绑定了点击监听器,条件就满足,onClick方法也会被执行。

View的LONG_CLICKABLE属性默认是false,而CLICKABLE跟具体的View相关,像Button这种可以点击的控件CLICKABLE属性是true,像TextView这种不可点击的控件为false。话虽如此,但是在外部调用setOnClickListener方法是会自动将CLICKABLE属性设为true,调用setOnLongClickListener方法时会将LONG_CLICKABLE置为true:

	public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

	public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

有一个细节值得注意:为什么onClick方法没有返回值而onTouch方法要返回布尔类型值呢?

有人会认为,onClick方法说明事件序列结束了(已经到了ACTION_UP),所以就没必要返回值,其实这种说法并不准确。根本的原因是,只要执行到onClick方法,系统默认这个事件就会被消耗,消耗了别的View就拿不到这个事件;而ACTION_UP虽然表示事件序列结束,但是并不代表这个ACTION_UP会被处理掉。其实,在严谨一点说,只要View的onTouchEvent被执行,而且View是可以点击的,那么此方法就会返回true,这一点从源码就可以看出来。

这样,处理事件的流程也就说完了,接下来就要说说分发的流程了。

事件分发流程分析

当用户触摸到屏幕时,最先响应的是硬件层,之后在一步一步的到系统层(也就是Activity),作为Android开发者,我们的分析也就从这里开始。

Activity对事件分发过程

当触摸事件发生时,Activity中的dispatchTouchEvent方法会被调用,那我们就来看看它的源码吧:

	public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

在第一个if语句中,调用了onUserInteraction()方法。如果当事件开始传递前,我们需要额外处理一些操作,可以重写onUserInteraction()并在其中完成处理,一般来说我们使用不到这个方法的。

在第二个if语句中调用了getWindow()的superDispatchTouchEvent(ev),getWindow获取到的自然是PhoneWindow了,关于PhoneWindow我不想再多说了,就是一个用于管理的类,也就是说,Activity将这个事件交给了PhoneWindow,如果它的superDispatchTouchEvent(ev)方法返回了false(表示PhoneWindow下面所有的View都不处理),那么只能由Activiy的onTouchEvent方法来处理这个事件;返回了true表示这个事件由下面处理了(不用关心是谁处理的反正有人处理),这时候这个事件就结束了。

值得一提的是,根据源码我们发现,Activity是不会拦截事件的,收到事件之后直接往下级分发。

这样我们就分析完了Activity对事件分发的过程,接下来看看PhoneWindow的superDispatchTouchEvent是什么样的

	@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到,PhoneWindow的superDispatchTouchEvent调用了mDecor的superDispatchTouchEvent方法,这个mDecor自然就是DecorView了。也就是说,PhoneWindow将事件分发给了DecorView。由于PhoneWindow只是一个用于管理的类,不属于View,所以它不用处理事件(当然也不用拦截了),只传给DecorView就完事儿了,那么接下来就看看DecorView是怎么分发事件的吧。

ViewGroup对事件分发过程

点开DecorView的superDispatchTouchEvent方法:

	public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

可以发现,内部调用了父类的dispatchTouchEvent方法。我们知道DecorView是继承自FrameLayout的,而FrameLayout没有覆盖dispatchTouchEvent方法,所以DecorView的superDispatchTouchEvent方法最终调用的是ViewGroup的dispatchTouchEvent。

点开ViewGroup的dispatchTouchEvent方法,发现它真的是太长了,但是我们可以把这个方法分为3个大的步骤:

	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    	//默认不拦截
    	boolean handled = false;
    	
    	//ACTION_DOWN到来说明这是一个新的事件序列,要做一些初始化工作
    	if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState(); //重置标志位
        }
    	//步骤1:检查当前ViewGroup是否要拦截这个事件
    	//步骤2:遍历子元素。将ACTION_DOWN事件分发给符合条件的子元素
    	//步骤3:分发事件,可能分发给子元素处理也可能自己处理
    	return handled;
    }

其中,步骤1很简单,就是判断是否要拦截,而步骤2和3里面又要分情况讨论,这是因为事件是以序列的形式存在的,对同一个事件序列的不同事件处理和分发流程可能是不一样的。这一点之前就提过了,分析事件分发的时候,要把握住事件序列的概念,所以要把事件的类型拿出来分别进行分析,在这里我打算先把dispatchTouchEvent对不同类型事件的处理逻辑说清楚之后,再用源码去验证。

首先来看ViewGroup对ACTION_DOWN事件的分发机制:

Android事件分发处理机制源码分析与知识点总结
上面的流程图画的已经很清晰了,值得注意的是mFirstTarget这个标志位,若ViewGroup自己处理则为空,子元素消耗掉了就不为空(指向了处理事件的子元素)。至于说排序的问题就不在这里深入研究了,毕竟我们讨论的是分发的机制,排不排序对其没什么影响,只需要知道有这回事就可以了。ACTION_DOWN处理完之后,由于mFirstTarget的存在,系统便保存了事件分发的传递链。这是什么意思呢?仔细想一想,由于布局是层层嵌套的,那么外层的ViewGroup中mFirstTarget指向了响应事件的子元素,而这个子元素如果也是ViewGroup,里面的mFirstTarget又记录了指向了响应事件的子子元素,这样一层记录着一层,就形成了一个传递链。有了这个传递链,在处理同一事件序列中其它类型的事件时ViewGroup就不用遍历子元素了,直接按照传递链分发,是不是就方便许多了呢?

总结一下,ACTION_DOWN处理完毕之后系统便记住了处理事件的元素,那么同一事件序列的其他事件就交给他处理了,这当然也有个前提,就是事件能传给它。这又是什么意思呢?我们想一下,总经理把第一期的项目交给了他的一个小弟做完之后,发现既轻松钱又多,那么第二期或者接下来的任意期他如果把项目给私吞了,这样小弟就接收不到了。

在这里还要啰嗦一句:有了传递链不代表Activity就直接将事件传给要处理的View了,而是说在有很多子元素的时候不用去遍历寻找,按照传递链一级一级的传过去的。

接下来看看处理ACTION_MOVE的流程

Android事件分发处理机制源码分析与知识点总结

ACTION_MOVE的流程就相对来说简单了许多。要注意的是,ACTION_MOVE到来之时ViewGroup仍然可以对其进行拦截。可以看到,处理ACTION_MOVE时直接跳过了步骤2中遍历的代码,增加了运行效率。

该说的说完了,那么就把把这3个步骤的代码拿出来看一下吧:

步骤1的代码如下:

	// 步骤1:判断是否拦截
    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 {
        intercepted = true;
    }

接着来看步骤2:

			//步骤2
			if (!canceled && !intercepted) {
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

				//步骤2只负责分发ACTION_DOWN事件,原因在这里
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                                
                        //注释1*****************************************************
                        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;
                            }
							
							//注释2*****************************************************
                            if (!canViewReceivePointerEvents(child)
                                    || !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);
                            //注释3*****************************************************
                            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();
                                //注释4****************************************************************************************
                                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);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

再来看一下步骤3:

		if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

细心一点会发现,在步骤2和步骤3中都调用了dispatchTransformedTouchEvent方法,我们先也把它拿出来看一下:

	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;
        }

        ……

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }
        // Done.
        transformedEvent.recycle();
        return handled;
    }

ViewGroup对ACTION_DOWN的分发流程源码验证

之前已经说过ViewGroup对ACTION_DOWN分发流程了,接下来我们在源码层面上验证一下其正确性。

当ViewGroup接受到ACTION_DOWN事件时首先会进到步骤1中判断是否拦截,这里我们先不用管到底是否应该拦截,而是分析拦不拦截之后分别会怎样。

如果ViewGroup选择拦截,则intercepted就为true。根据上面的流程,拦截就直接跳到步骤3让ViewGroup自己处理,为什么呢?这是很显然的,步骤2的第一行有一个if判断:

	//步骤2
	if (!canceled && !intercepted) {
   	     ……
	}

intercepted为false才可能进入步骤2的代码,所以如果ViewGroup选择拦截,步骤2的代码是不会运行的。那么再来看步骤3:

	//步骤3
	if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);        
    } else {
    	……
    }

之前已经说过了,事件若由ViewGroup拦截,那么mFirstTouchTarget ==null就成立,那么就会执行dispatchTransformedTouchEvent方法。这里先剧透一下:dispatchTransformedTouchEvent方法既可以让ViewGroup自己去处理事件,又可以让子元素处理事件,到底让谁处理要看传的参数是什么。这里第3个参数传入的是null,那么我们看一下传null会让谁去处理:

	private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {       
        final boolean handled;
        ……
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            ……
        }
        // Done.
        transformedEvent.recycle();
        return handled;
    }

可以看到child==null的情况下会调用super.dispatchTouchEvent(transformedEvent),而ViewGroup的父类是View,也就是ViewGroup调用了View的dispatchTouchEvent方法自己来处理事件。这句话有点绕口,但是却证实了我们之前提过的结论:不管是View还是ViewGroup,想要处理事件调用的都是View的dispatchTouchEvent方法,而View的dispatchTouchEvent方法在第3节已经详细的分析过了,就不用在废话了。

如果ViewGroup不拦截,步骤1里面就会将intercepted置为false,为false就可以进入步骤2中的if代码块:

```java
			//步骤2
			if (!canceled && !intercepted) {
				……
				 //注释1*****************************************************
				 //步骤2只负责分发ACTION_DOWN原因在这里
                if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                         //注释2*****************************************************
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                                
                        //注释3*****************************************************
                        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;
                            }
							
							//注释4*****************************************************
                            if (!canViewReceivePointerEvents(child) || !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);
                            //注释5*****************************************************
                            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();
                                //注释6****************************************************************************************
                                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);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

因为是ACTION_DOWN事件所以步骤2注释1处的if语句也能进入,接着在注释2处调用了buildTouchDispatchChildList方法对ViewGroup中的子元素进行了排序(有兴趣的话可以研究一下到底是怎么排的)。之后在注释3处便对子元素进行遍历,依次判断它们是否有资格接收到这个事件。判断的依据是:手指触摸屏幕的坐标在不在控件的范围之内和控件是否在播放动画。如果没有资格就continue来判断下一个;如果有资格就在注释5处调用dispatchTransformedTouchEvent将事件传给该子元素。这次调用该方法时,第3个参数为分发的子元素,我们来看看第3个参数不为null会怎样:

	private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {       
        final boolean handled;
        ……
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            ……
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        // Done.
        transformedEvent.recycle();
        return handled;
    }

这次child不为空,则执行else中的代码,可以看到调用的是子元素child的dispatchTouchEvent方法,也就是说事件交给满足条件的子元素去处理了。这时候又分两种情况:

(1)子元素处理后反馈结果为true,也就是说子元素消耗了这个事件,那么步骤2注释5处的if代码块就进去了,然后在注释6中通过addTouchTarget方法将mFirstTouchTarget指向子元素(也就是说这时mFirstTouchTarget不为null了),并将alreadyDispatchedToNewTouchTarget 置为 true;。这里可以看到TouchTarget是一个链表结构,因为它涉及到了多点触摸的情况,我们只讨论单点触摸,所以只需要知道如果子元素消耗了事件,那么mFirstTouchTarget就不为null即可。

	private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

然后步骤2执行结束,到了步骤3处会执行while中的代码,而且while循环只会执行一次,因为第二次target就为null了,while循环里面走的是if里面的代码(因为alreadyDispatchedToNewTouchTarget为true了),简单的将handled设为true,然后步骤3就执行完毕了,dispatchTouchEvent方法返回true。

		//步骤3
		if (mFirstTouchTarget == null) {
            ……
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    ……
                }
                predecessor = target;
                target = next;
            }
        }

(2)如果子元素处理结果返回false(处理但是不消耗),那么步骤2中注释5处的if代码块就不会执行,然后ViewGroup会判断其他子元素能否接收事件,如果都不能的话(mFirstTouchTarget这时为空),到了步骤3执行的是最外层的if代码块,这块内容前面已经分析过了,这种情况ViewGroup会自己处理事件,这是必然的,子元素不消耗掉那就由上级去处理。等ACTION_DOWN消耗之后,下一个事件ACTION_MOVE到来之时由于actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null这两个条件都不满足,所以走的是步骤1的else代码块,也就是将事件拦截下来,从此以后同一序列的其他所有事件都不会交给子元素处理了。这也是很好理解的,你把上级交给你的任务搞砸了,那么短时间内上级就不会把这个任务后续的事情交给你处理。

真不容易,ACTION_DOWN就这样分析完了,再来看看ACTION_MOVE。

ViewGroup对ACTION_MOVE的分发流程源码验证

当ACTION_MOVE到来时,同样也要走步骤1判断是否拦截。

(1)如果不拦截:前面已经说过了,ACTION_DOWN事件结束之后会建立一条传递链,有了传递链便不会运行步骤2中的代码进行遍历了,这是因为步骤2的注释1处的if判断只有ACTION_DOWN才能进去(其他两个条件是多点触碰我们不用管)。那么接着就会执行步骤3:

		//步骤3
		if (mFirstTouchTarget == null) {
            ……
        } else {
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                         handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

在步骤3中因为mFirstTouchTarget 不为 null所以走的是外层的else代码块,紧接着在while循环中走的也是else代码块(每次进到dispatchTouchEvent方法时都会将alreadyDispatchedToNewTouchTarget设为为false),在其中又调用了dispatchTransformedTouchEvent方法,其中的第3个参数我们发现是由target保存下来的,也就是事件分发链中拿到的。

(2)拦截:拦截的话intercepted自然就为true了,这时仍然不会运行步骤2中分发的代码(原因同(1))而来到步骤3。这种情况同样也是走的步骤3中while循环里面的else分支。只不过跟不拦截的情况不同的是,由于intercepted为true则cancleChild就为true,接着调用了dispatchTransformedTouchEvent方法。我们再来看看dispatchTransformedTouchEvent方法第二个参数为true会是什么结果:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {

        final boolean handled;
        
        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;
        }
        ……    
    }

因为cancel为true那么就会进到if代码块中。在if代码块中首先将事件强制设置为ACTION_CANCEL,这也就验证了ACTION_CANCEL事件是在ViewGroup选择拦截时触发的。又因为child不为null所以会执行handled = child.dispatchTouchEvent(event)这句代码,也就是说让子元素去处理一个ACTION_CANCEL(即取消子元素对事件的处理)。再回到步骤3中,因为cancle为true会将mFirstTouchTarget设为next(也就是设为空)。这样一来当下一个ACTION_MOVE到来时步骤1中的actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null条件不满足,走else代码块将intercepted设为true进行拦截,拦截之后,接下来同一序列的后续事件都由它自己处理了。这便从源码的角度上验证了之前提到的流程。

mGroupFlags标记位的作用

之前有提过,如果父容器选择拦截,子元素并不是就束手待毙了,它可以通过mGroupFlags标记位来干涉父容器对事件的拦截。与此相关的方法就是requestDisallowInterceptTouchEvent,我们点进去看一看:

	public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

如果方法传入参数为true,mGroupFlags就会和FLAG_DISALLOW_INTERCEPT常量进行一次按位或运算。这样一来,在步骤1中就会让disallowIntercept变成true,那么将运行else代码块将intercepted设为false,这样父容器就无法将事件拦截下来了。

	// 步骤1:判断是否拦截
    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 {
        intercepted = true;
    }

不过disallowIntercept方法也有一点局限性,就是父容器依然可以拦截掉ACTION_DOWN事件,这是因为ACTION_DOWN到来时ViewGoup会重置这个标记位:

    if (actionMasked == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  //重置标记位
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

这就表示,如果调用requestDisallowInterceptTouchEvent方法,那么ViewGroup将无法拦截除了ACTION_DOWN以外的其他事件。

在requestDisallowInterceptTouchEvent方法的最后,调用了mParent.requestDisallowInterceptTouchEvent(disallowIntercept),也就是说如果requestDisallowInterceptTouchEvent方法被调用,那么它的直接父容器和间接父容器都会调用该方法来阻止事件的拦截。

这个标记位在我们开发中有什么实际作用呢?这就涉及到了滑动冲突的一种解决方案——内部拦截法,这块内容将在下一篇文章中着重分析。

事件分发与处理总结

无聊的的源码分析终于结束了,如果还是不懂也没关系,把总体的流程看懂是最重要的,其他的细节可以随着自己的水平增长慢慢的去推敲,相信总有一天可以把这块硬骨头啃掉的。

最后,根据上面的分析,将几个重要的结论梳理一下:

  1. ViewGroup默认不拦截绝大多数的事件,为什么是绝大多数呢?查看源码可以发现,ViewGroup的onInterceptTouchEvent方法返回true的条件是很苛刻的,也就是说如果不重写onInterceptTouchEvent方法,那么有99.9%的概率不会进行拦截。
  2. View(这里的View不包含ViewGroup)没有onInterceptTouchEvent方法。也就是说View一旦接收事件,就一定会执行dispatchTouchEvent方法,或者是监听器的onTouch方法(如果有绑定的话)。
  3. View的onTouchEvent默认消耗事件(返回true),除非它是不可点击的,并且View的enable属性不影响该方法的返回值。
  4. 对于一个ViewGroup来说,如果事件传到了它的手里,那么它有两次机会决定是否消耗掉这个事件:第一次机会是事件刚传到它手里的时候可以选择拦截;第二次机会是如果它的子元素都不处理,那么还可以选择自己消耗。如果这两次机会都放弃了,事件将被它的上级处理。
  5. View对于几个常见的事件处理的优先级为:onTouch>onTouchEvent>onLongClick>onClick
  6. 如果一个ViewGroup决定拦截事件,那么同一个事件序列的其他事件都只能由它来处理(当然事件得能传到它),也就是说拦截之后,同一个事件序列的其他事件再到它这里时onInterceptTouchEvent方法不会被执行,系统默认拦截。
  7. ACTION_DOWN处理完之后,系统便保存了事件分发的传递链,此后同一序列的其他事件就会根据这个传递链分发到指定的子元素而不用遍历了;等新的一轮事件序列到来时,系统会清空之前的传递链。根据这条结论,我们知道,如果某个控件消耗了一个ACTION_DOWN事件,那么后面即使手指移出了这个控件的范围,这个事件序列的后续事件还是会被该控件处理(如果不被上层拦截的话),因为这时候系统按照之前保存好的传递链进行传递,而不用再判断手指还在不在控件的范围之内了。
  8. 不管是View还是ViewGroup,想要处理事件调用的都是View的dispatchTouchEvent方法
  9. 子元素可以通过requestDisallowInterceptTouchEvent方法来干涉父元素对事件的拦截,不过对ACTION_DOWN除外

本文地址:https://blog.csdn.net/weixin_44965650/article/details/107379808