Android View(二)——View的事件分发机制

一.Activity的层级结构

在了解View的事件分发之前,先了解下Activity的层级结构,便于更好的理解事件的传递顺序。
 
层级结构图
在这里插入图片描述

二.事件分发的基础认识

1.事件分发是什么

事件分发就是对MotionEvent事件进行分发的过程,即当一个MotionEvent产生后,系统需要把这个事件传递(处理)给一个具体的View,这个过程就是分发过程。

2.事件分发过程中的三个重要方法

方法 作用 调用时刻 返回值
dispathchTouchEvent 进行事件的分发 事件传递给当前View时调用 是否消耗当前事件
onInterceptTouchEvent 判断是否拦截某个事件 在ViewGroup的dispatchTouchEvent()内部调用 表示是否拦截当前事件
onTouchEvent 处理点击事件 在dispathchTouchEvent内部调用 表示是否消耗当前事件

三者之间的关系用伪代码表示

public boolen dispatchTouchEvent(){
    
    
	boolen consume = falseif(onInterceptTouchEvent){
    
      
		consume = onTouchEvent(ev); //如果被拦截,调用当前viewGroup的onTouchEvent
	}else{
    
    
		consume = child.dispatchTouchEvent(); //如果未被拦截,调用当前的子view的dispatchTouchEvent,即事件传递给子view
	}
	return consume;
}

二.事件分发的过程了解

事件是用户与屏幕发生交互时产生的,而Activity则是Android中负责与用户发生交互的组件。所以事件的传递,首先是到达Activity,再通过内部传递之后,到达我们的布局文件中的layout和View。事件发生之后,需要进行响应处理,再传递的过程中,由上往下,都有可能有机会处理一个事件序列。如果从Activity往下,到最终的View,事件都没有得到处理,则事件又从下往上,回到Activity,如果回到Activity之后,Activity没有处理这个事件,那么这个事件就会自动结束。
 

1.图解:

1.1基本过程图解

在这里插入图片描述
对上图的小总结:

  1. 当事件传递到一个ViewGroup上面时,ViewGroup会触发dispatchTouchEvent方法,随后调用onInterceptTouchEvent方法确认是否拦截此事件,最后如果事件是自己来处理的话,则调用onTouchEvent方法。
  2. 在ViewGroup类中,onInterceptTouchEvent方法总是返回false,表示默认是不拦截事件的,除非去重写ViewGroup类来返回true。而onTouchEvent方法的返回值表示是否消费(返回true则消费)此事件,消费的意思就是说ViewGroup自己处理了这个事件,不再传递到上一层的onTouchEvent去。
  3. 在View中,与ViewGroup相比,同样有dispatchTouchEvent方法和onTouchEvent方法。但是没有onInterceptTouchEvent这个方法,因为在一个View中,已经是View树的叶子节点,它没有下一级的视图嵌套,所以不需要决定是否拦截事件,它自己就可以处理事件了。
  4. 在View类中,只要该View是可以点击的,那么默认都会在onTouchEvent返回true,表示自己消费了这个事件,不再传递到上一级ViewGroup去。

1.2 关于ACTION_MOVE 和 ACTION_UP

上面的讲解都是针对的ACTION_DOWN,ACTION_MOVE和ACTION_UP和ACTION_DOWN在传递过程中和ACTION_DOWN并不相同。简单来说,只有前一个事件返回了true时,才会收到ACTION_MOVE和ACTION_UP的事件。并且而最终会将ACTION_MOVE和ACTION_UP分发到消费到ACTION_DOWN的View手中。在分发的过程中,ACTION_MOVE和ACTION_UP与ACTION_DOWN分发的路线可能不回完全相同。

例如:
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
总结:如果在同一个事件序列里面,如果ACTION.DOWN事件不被这个View做出消耗,则后面陆续的事件序列则不会传递到这个View来

2. demo验证上述过程

我们分别创建RelativeLayoutA、RelativeLayoutB,都继承自RelativeLayout,也等同于是ViewGroup,再创建一个MyView继承自Button类,也等同于是继承View。
在RelativeLayoutA类,RelativeLayoutB类中重写上面提到的三个方法,分别打印出他们的方法名,在MyView类中重写dispatchTouchEvent方法和onTouchEvent方法,打印他们的方法名。

//RelativeLayoutA的代码,RelativeLayoutB和MyView与这类似,这里不展示
public class RelativeLayoutA extends RelativeLayout {
    private String TAG = "RelativeLayoutA";
    public RelativeLayoutA(Context context) {
        super(context);
    }
    public RelativeLayoutA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public RelativeLayoutA(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG, "dispatchTouchEvent: ");
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: ");
        return super.onTouchEvent(event);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent: ");
        return super.onInterceptTouchEvent(ev);
    }
}

布局文件:
在RelativeLayoutA中嵌套RelativeLayoutB,RelativeLayoutB中嵌套MyView

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.example.three_viewtext.RelativeLayoutA
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:id="@+id/RelativeLayoutA"
        android:layout_centerInParent="true"
        android:background="#ffff00">
        <com.example.three_viewtext.RelativeLayoutB
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:id="@+id/RelativeLayoutB"
            android:layout_centerInParent="true"
            android:background="#00ff00">
            <com.example.three_viewtext.MyView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/Button"
                android:text="按钮"
                android:layout_centerInParent="true"
                />
        </com.example.three_viewtext.RelativeLayoutB>
    </com.example.three_viewtext.RelativeLayoutA>
</RelativeLayout>
  1. 第一种情况,直接运行程序点击按钮,打印日志如下:
    在这里插入图片描述
    在点击按钮时的事件列是:DOWN->IP,如上图,上半部分为DOWN的事件调用顺序,下半部分为UP的事件调用顺序。
    因为Button默认是可以点击的(即使我们并没有设置点击监听事件),所以MyView打印出了onTouchEvent,随后返回了true,这个ACTION.DOWN事件就被MyView消耗掉了。
     

  2. 第二种情况, 把MyView的onTouchEvent事件返回false,编译运行后点击中间的按钮,再看下打印:
    在这里插入图片描述
    可以看到,从Activity->view事件都没有得到处理,则事件又从下往上,回到Activity,log只是打印出了ACTION.DOWN的打印,并没有像上面的log一样打印出ACTION.UP。UP事件没有传递到ViewGroup和View中
    在这个Log里面由于MyView不处理事件,而RelativeLayoutB和RelativeLayoutA其实也是不处理自己事件的,最后交由了更高级别的ViewGroup(Activity)去响应了,所以后面的ACTION.UP不会再传递到这几个控件上来了。

  3. 在RelativeLayoutB中让onInterceptTouchEvent返回true,表示RelativeLayoutB会拦截事件自己处理,不分发给下一级View树处理,编译运行后点击中间的按钮,我们再来看看Log:在这里插入图片描述从这个Log中可以看出,MyView并没有打印出来,说明他没有接收到事件,因为RelativeLayoutB已经把事件给拦截了,就不再分发给MyView,而RelativeLayoutB把事件拦截了后自己调用onTouchEvent,默认是没有消耗事件的,所以才会再调用RelativeLayoutB的onTouchEvent方法。同样,和2一样,UP事件没有传递到ViewGroup和View中。

注意事件拦截和事件消费是两回事,事件拦截说的是不把事件发给下一级View,而事件消费说的是处理完这个事件还要不要让上一级也处理,如果消费了事件那么就不会再让上一级处理这个事件。

三.onTouch、onClick、onLongClick的调用顺序

setOnTouchListener方法,通过设置监听后可以在触摸的时候回调onTouch方法

在刚才的demo中,设置View的onTouch、onClick监听,,这里只做一个总结:

  • 在日常中我们给View设置点击事件其实响应优先级是最低的,因为他需要同时接收到ACTION_DOWN和ACTION_UP事件后才会触发,而onTouch方法则是在设置监听后,只要有事件到来,则会触发一次,它比onTouchEvent优先被响应。
  • onTouch比onClick方法多了一个返回值,其返回值也表示了是否消耗事件,如果返回了true则不会再调用onTouchEvent方法
  • onClick方法是在onTouchEvent里面被回调的,如果onTouch返回了true,onTouchEvent不会被调用,那么onClick也就不会被调用。
  • 当View接收到ACTION_DOWN的时候,并且不松开大概0.5s的时候(log从onTouchEvent到onLongClick的执行时间差大概就是0.5s)会执行onLongClick,当接收到ACTION_UP的时候再执行onClick,而如果onLongClick方法中返回了true,则onClick就不会再执行。

四.事件分发源码分析

事件分发其实包含了三部分的事件分发,即:

  • Activity的事件分发
  • ViewGroup的事件分发
  • View的事件分发。

1.Activity的事件分发

当一个点击操作发生时,事件最先是传递给当前的Activity,由Activity的dispatchTouchEvent进行事件委派

//Activity的dispatchTouchEvent源码
 public boolean dispatchTouchEvent(MotionEvent ev) {
    
    
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    
    
            onUserInteraction();		//空方法
        }
        //委派给DecorView
        if (getWindow().superDispatchTouchEvent(ev)) {
    
    
        	//如果下层处理了事件,返回true结束	
            return true;
        }
        //事件没有得到处理,调用Activity的onTouchEvent
        return onTouchEvent(ev);
    }



/*
  getWindow()获得是Window的抽象类,而window的唯一实现就PhoneWindow,下面是PhoneWindow.superDispatchTouchEvent源码
        	*/
public boolean superDispatchTouchEvent(MotionEvent event) {
    
    

        //Decor即是DecorView,所以phoneWindow将事件传递给了DecorView
        return mDecor.superDispatchTouchEvent(event);
       
    }
/*
	DecorView.superDispatchTouchEvent源码
*/

 public boolean superDispatchTouchEvent(MotionEvent event) {
    
    
        return super.dispatchTouchEvent(event);
        //等于开始调用ViewGroup的dispatchTouchEvent,即到此为止,
        //事件从Activity传到了ViewGroup
    }

/*
Activity.onTouchEvent
*/
public boolean onTouchEvent(MotionEvent event) {
    
    
        if (mWindow.shouldCloseOnTouch(this, event)) {
    
    
            finish();
            return true;
        }

        return false;
    }

总结:在这里插入图片描述

2.ViewGroup的事件分发机制

这里只展示其中一些关键代码

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
    
        ......
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
    
    
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

           
            final boolean intercepted;// 检查是否要拦截
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
    
    
             /*
             FLAG_DISALLOW_INTERCEPT设置后,ViewGroup无法栏除ACTION_DOWN之外的其他点击直接。
             原因:在ViewGroup分发事件时,如果是ACTION_DOWN,会重置这个标志位   
             设置方法: requestDisallowInterceptTouchEvent
             */
                
                
                if (!disallowIntercept) {
    
    // 只有允许拦截才执行onInterceptTouchEvent方法
                    intercepted = onInterceptTouchEvent(ev);//调用onInterceptTouchEvent方法
                    ev.setAction(action); // restore action in case it was changed
                } else {
    
    
                //不允许拦截,直接设为false
                    intercepted = false;
                }
            } else {
    
    
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                /*
                在这种情况下,actionMasked != ACTION_DOWN && mFirstTouchTarget == null
                说明没有一个子View要去处理ACTION_DOWN事件,导致mFirstTouchTarget还是空的,
                没有指向要处理事件的子View,所以接下来的其他事件,都不再继续分发下去了,而且拦截了事件让自己处理。
                */
                intercepted = true;
            }

         

  1. FLAG_DISALLOW_INTERCEPT标志位
//源码2662行
 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

FLAG_DISALLOW_INTERCEPT是一个标记位,通过 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 可以设置它,一般是用在子View里面。如果FLAG_DISALLOW_INTERCEPT被设置了后,ViewGroup就无法拦截ACTION_DOWN以外的其他事件,这是因为ACTION_DOWN事件会重置FLAG_DISALLOW_INTERCEPT标记位

			//源码(2650行),
            if (actionMasked == MotionEvent.ACTION_DOWN) {
    
    
            //down事件,做重置状态的操作,
                cancelAndClearTouchTargets(ev);
                resetTouchState();	//会对FLAG_DISALLOW_INTERCEPT进行重置
            }

首先cancelAndClearTouchTargets方法会遍历清除所有的target,导致mFirstTouchTarget=null,然后在调用resetTouchState,重置触摸状态
resetTouchState的源码:

 private void resetTouchState() {
    
    
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        //重置标志位
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

分析到这里可得出如下结论:

  • 当ViewGroup要拦截事件后,那么后续到来的事件会直接交给他处理,而不会再调用onInterceptTouchEvent询问是否拦截。
  • 当调用requestDisallowInterceptTouchEvent方法设置了不允许拦截的标记位后,在ACTION_DOWN事件的时候会被重置掉而不起作用,也就是说requestDisallowInterceptTouchEvent方法针对的是ACTION_DOWN以外的其他事件,并且是在不拦截ACTION_DOWN事件的情况下才会起作用。

 
3. ViewGroup不拦截事件时,事件会向下分发交给他的子View进行处理

	//源码2722行
                       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);
        /*
          判断子元素是否能接收到点击事件,两个衡量标准
          child.canReceivePointerEvents():是否在播动画
          isTransformedTouchPointInView()点击事件的坐标是否落在子元素区域内
        */
        if(!child.canReceivePointerEvents()
                ||!isTransformedTouchPointInView(x,y,child,null)){
    
    
            continue;
        }

        newTouchTarget=getTouchTarget(child);// 查找child对应的TouchTarget
        if(newTouchTarget!=null){
    
    
            // 比如在同一个child上按下了多跟手指
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            //子View已经在自己的范围内得到了触摸。
            //除了它正在处理的那个,给它一个新的指针。
            newTouchTarget.pointerIdBits|=idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
        //dispatchTransformedTouchEvent()实际上调用子元素的dispatchTouchEvent
        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();
            // 如果处理掉了的话,将此child添加到touch链的头部
             // 会更新 mFirstTouchTarget
            newTouchTarget=addTouchTarget(child,idBitsToAssign);
            alreadyDispatchedToNewTouchTarget=true;// 记录ACTION_DOWN事件已经被处理了。
            break;
        }
    }
                          

总结:
在这里插入图片描述

3.View的事件分发机制

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

       
        if (onFilterTouchEventForSecurity(event)) {
    
    
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
    
    
                result = true;
            }
            //首先判断是否设置OnTouchListener,如果OnTouchListener中的onTouch方法中返回true,那么onTouchEvent就不会被调用,
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
    
    
                result = true;// 如果被onTouch处理了,则直接返回true
            }

            if (!result && onTouchEvent(event)) {
    
    
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
    
    
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
    
    
            stopNestedScroll();
        }

        return result;
    }

总结:
在这里插入图片描述

五.事件分发的总结

  1. 同一事件序列是指从手指接触到屏幕的那一刻起,到手指离开的屏幕的那一瞬间结束,在这个过程中所产生的一系列的事件,这个事件以down事件开始,中间含有数量不等的move事件,最终以up事件结束
  2. 正常情况下,一个事件序列只能被一个View拦截且消耗,这一条的原因可以参考(3),因为一旦一个元素拦截了此事件,那么同一事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但通过特殊手段可以做到,比如同一个事件序列中的事件不能分别由两个View同时处理,但通过特殊手段/可以做到,比如一个View将本该自己处理的事情通过onTouchEvent强行传递给其他View处理。
  3. 某个View一旦决定拦截,那么这一个事件序列都只能由他处理,并且onInterceptTouchEvent都不会在被调用。
  4. 某个View如果不消耗ACTION_DOWN事件。那么同一事件序列的其他事件也不会交给他处理,并且把事件重新交给它的父元素处理,即调用父元素的onTouchEvent。
  5. ViewGroup默认不拦截任何事件。即源码中onInterceptTouchEvent方法中默认 返回false
  6. View没有onIntercept方法,一旦有点击事件传递给他,那么他的onTouchEvent就会被调用
  7. View的onTouchEvent默认都会消耗事件,除非他是不可点击事件。(clickbale和longClickable同时为false),Veiw的longClickable默认属性都是false,chickable属性要分情况,比如button的clicjable属性默认是true,而TextView的clickanle的默认属性为fasle。

六.参考资料

View的事件传递及分发机制
Android之View篇2————View的事件分发
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(上)
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(中)
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(下)

猜你喜欢

转载自blog.csdn.net/haazzz/article/details/114435490