标签:
Android的事件分发机制,对于事件的分发的了解是非常重要的;如果你不清楚具体的原理,那么你将会很迷茫,遇到问题时,无从下手。这里,我将个人对Android事件分发机制的理解,描述出来,希望能对大多数伙伴的有所裨益。
1.触摸事件的开始
触摸事件,来自触摸屏。从触摸屏硬件产生事件信号到Activity开始接收这个事件,就不做分析了,因为具体的我也不清楚。因此,这里主要分析Activity中,事件的分发过程。
首先由Activity进行分发,具体的分发方法如下:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }直接从getWindow().superDispatchTouchEvent(ev)开始,如果这个方法返回true,那么这个方法结束,不会调用return onTouchEvent(ev),这是什么意思呢?这个意思就是如果事件被window消费了,Activity就不再对事件进行处理。如果事件并没有被window消费掉,那么事件能被onTouchEvent()方法处理,这个具体的处理方式,我们可以在Activity中进行重写。很多人不明白什么叫做事件被消费掉,所谓事件被消费掉,其实是事件得到了处理,这个事件不再进行传递,不会再传递到其他控件。
接下来我们分析getWindow().superDispatchTouchEvent(ev),这个方法,很多人不知道具体的实现在哪里,这个是和Activity的启动过程有关系,在Activity的创建过程中,会通过PhoneWindow初始化window,因此这个方法,其实是PhoneWindow的superDispatchTouchEvent
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }这里,我们能看到其实最终调用的是DecorView的superDispatchTouchEvent。由于DecorView继承LinearLayout,最后,其实还是ViewGroup的dispatchTouchEvent方法。
总结上面所说的,Activity中,对事件的分发,主要是通过ViewGroup的dipatchTouchEvent方法来执行的。接下来我们着重分析ViewGroup中的这个方法。
2.事件分发的核心
2.1.ACTION_DOWN的向下传播,找到事件触发坐标所在的最里面的一个控件。
首先,我们需要知道一个事件序列,ACTION_DOWN—>ACTION_MOVE—>ACTION_UP,这只是其中一个代表,一个触摸事件总是从ACTION_DOWN开始的,然后才会触发后面的事件。ACTION_DOWN这个事件,它承担了查找能够处理事件的目标控件,这个查找的过程,具体看代码分析吧。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { //获取事件的动作 final int action = ev.getAction(); //事件的x,y在当前视图的坐标 final float xf = ev.getX(); final float yf = ev.getY(); //mScrollX和mScrollY是该容器视图的画布的滑动位移, final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; 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 //默认是false,表示可以拦截,可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)设置为true,表示不拦截,那么onInterceptTouchEvent就失效了 if (disallowIntercept || !onInterceptTouchEvent(ev)) { // 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; //开始遍历子view for (int i = count - 1; i >= 0; i--) { final View child = children[i]; //如果子View是可见或者是有动画的,那么获取这个视图的可点击范围 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { //表示的child的原始位置,也就是scroll滑动前的位置,因此上面需要加上mScrollX child.getHitRect(frame); //如果该孩子位置的包含了所触控的点 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; //该孩子是否消费的了事件,如果该孩子消费了,则返回true,这里是递归调用 if (child.dispatchTouchEvent(ev)) { // Event handled, we have a target now. mMotionTarget = child; return true; } // The event didn't get handled, try the next view. // Don't reset the event's location, it's not // necessary here. } } } } }上面是ViewGroup中dispatchTouchEvent的第一部分代码,也是ACTION_DOWN事件的主要处理过程,代码中具体注释了重要的过程。要注意的只有两点:
1.disallowIntercept,这个标志位默认是false,表示可以拦截,主要看onInterceptTouchEvent来控制。如果表示true,表示不能拦截,onInterceptTouchEvent就算拦截了,也是无效的。
2.最后的几句代码中,又调用了child.dispatchTouchEvent,说明这类似一个递归调用。我们可以想象,ViewGroup1的dispatchTouchEvent中调用了ViewGroup2的dispatchTouchEvent,最后调用了view.dispatchTouchEvent方法(最后一个控件很有可能不是ViewGroup)。一直把这个事件传递到最后一个view,如果最后这个view的dispatchTouchEvent返回true。但这一切的前提是,事件的触发坐标落在控件上。
2.2.target为空,ACTION_DOWN事件外层传递,父控件获得处理事件的机会。
上一个过程中,ACTION_DOWN并没有找到能够消费它的控件,因此,遍历控件完成后,进入最后一个控件的父控件的dispatchTouchEvent的这个过程。事件没有消费,交给最后一个控件的父控件的super.dispatchTouchEvent来处理。这个处理,其实是调用的View里面的dispatchTouchEvent,这个和ViewGroup中的是不一样的,View中的这个方法是具体的消费过程,并不分发事件。如果当前父控件(也就是倒数第二个控件)也没有消费这个事件,super.dispatchTouchEvent返回false;事件又交给了当前控件的父控件(倒数第三个控件)同样进行处理。 这里有人不明白,为什么是交给父控件处理;原因是第一步里面,我们是层层调用,父控件调用子控件的dispatchTouchEvent方法。这里是target为空,也就是说第一步的层层调用,没有消费掉事件,还没有返回,因此这里面对这个情况进行处理,开始层层往上返回;父控件得到处理事件的机会,如果父控件没有消费掉事件,就继续往上返回;如果父控件消费掉了事件,那么它的父控件返回true,target为当前这个控件。
/如果target为空,ACION_DOWN事件未消费,ACTION_DOWN事件又开始重新往上分发 final View target = mMotionTarget; 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; } //子View没有消费事件,交给当前的ViewGroup来处理,其实super.dispatchTouchEvent(ev);调用的View的dispatchTouchEvent return super.dispatchTouchEvent(ev); }2.3.找到了target
存在有两种情况:
1.事件没有被拦截,ACTION_DOWN事件顺利找到了目标控件,并且该控件能够对事件进行处理,消费掉。
2.事件中途被拦截,事件交给了中途的一个ViewGroup处理,并且有一个ViewGroup能够消费事件。 比如:事件中途被ViewGroupIntercept(控件别名)被拦截,那么它不会执行2.1的代码,target==null,ACTION_DOWN事件,进入到代码2.2,事件开始往上返回,但是ViewGroupIntercept的父控件并没有拦截,事件执行在2.1的代码,这时候如果ViewGroupIntercept消费掉事件,返回true,那么它的父控件的target就指向这个控件,这样又重新回到了正常的分发流程。
2.4.ACTION_MOVE,ACTION_UP等非ACTION_DOWN事件被拦截。
ACTION_MOVE,ACTION_UP事件被拦截,肯定是事件已经找到了可消费的控件。但是一个事件序列中,除ACTION_DOWN的事件,被动态拦截了,这里的事件将做如下处理:
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); //给目标传递个cancel事件 if (!target.dispatchTouchEvent(ev)) { // target didn't handle ACTION_CANCEL. not much we can do // but they should have. } // clear the target //当前控件的target清空,下一次的事件判断,进入到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; }事件被拦截后,先给目标控件传递一个cancel事件,这一个事件序列中目标控件的事件结束。接下来,当前控件的分发方法返回true,表示当前控件可以消费事件,那么他的父控件的分发方法进入到了流程2.3,也就是target不为空,target不为空,事件就是正常分发(什么是正常分发?后面会讲)。
2.5.事件的正常分发
事件由父控件的target传递到子控件的target,这就是事件的正常分发。如果事件是ACTION_UP或者是ACTION_CANCEL,将会清空target,下一次的触摸事件,又是如此重新开始。
if (isUpOrCancel) { mMotionTarget = null; } ...... return target.dispatchTouchEvent(ev);
上面多次提到View.dispatchTouchEvent,现在我们来看一下:
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }先会判断控件是否设置了onTouchListener,如果设置了,并且控件是enable,那么事件酱油OnTouchListener的onTouch方法处理。 否则事件交给默认的处理方法onTouchEvent来处理。
看看onTouchEvent的处理方法,onTouchEvent的处理,主要逻辑是这样的,先判断控件是否是clickable,如果不可以,直接返回false,事件没有消费掉。如果clickable为true或者longClckable为true,事件会被消费掉,具体就会回调onClick方法或者是onLongClick方法。
总结:
1.正常不拦截事件,由action_down查找对应能消费的target,如果存在target能消费事件,则最后事件由该target全部处理。
2.不拦截事件,没有target处理事件,则事件逐步往上由onTouchEvent方法处理,但是控件非clickable,则不能处理事件。如果存在控件是clickable的,那么事件会被消费掉。
3.事件分发的过程中,如果事件被拦截,则下一个事件交给拦截事件的onTouchEvent控件处理。
4.requestDisallowInterceptTouchEvent可以用来控制事件的分发。
Android滑动冲突
多个可以滑动控件之间的嵌套很容易引起滑动冲突,解决的方法分为两种:
1.从外部拦截机制考虑
外部控件重写onInterceptTouchEvent处理,通过计算dx和xy的进行处理,在onMove事件中动态控制
2.内部控件调用
requestDisallowInterceptTouchEvent来控制,原理其实是一样的。requestDisallowInterceptTouchEvent能够控制事件能否往下传递,前面事件分发机制已经分析了。(往下的意思:View是树形结构,最顶层是最底部的View,最下面是子View)
到这里,整个的事件分发机制就基本结束了,希望能对大家有所帮助。若有什么分析不当的地方,望大家指出。
标签:
原文地址:http://blog.csdn.net/meijian531161724/article/details/51833154