Android自定义控件系列九:从源码看Android触摸事件分发机制
请尊重原创劳动成果,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45071069,非允许请勿用于商业或盈利用途,违者必究。
Android触摸事件,网上也有很多文章来讲了,今天在这里想使用例子和源码相结合的方式,可能会看的更清晰一些。
在讲例子和源码之前,还是先把结论讲一下,这样可能会比较好,因为很多朋友时间都很宝贵,而研究源码可能会要花费不少时间,可以先初步理解事件的分发机制,等有时间再来慢慢琢磨源码。
触摸事件的传递机制:
首先是最外层的viewgroup接收到事件,然后调用会调用自己的dispatchTouchEvent方法。如果在ACTION_DOWN的时候dispatchTouchEvent返回false则后续的ACTION_MOVE和ACTION_UP都接收不到了,如果在ACTION_DOWN的时候dispatchTouchEvent返回true,则在后续的动作都可以继续分发下去;
dispatchTouchEvent方法的调用过程中先会经过onInterceptTouchEvent方法判断,如果onInterceptTouchEvent返回true则会拦截,最终传递给本viewgroup的onTouchEvent方法;如果返回false,则不拦截,传递给子view/viewgroup的dispatchTouchEvent;而这个传递给子view/viewGroup的过程是这样的:
先会遍历所有的直属子view/ViewGroup ,看看点击事件是发生在哪个直属子view/ViewGroup上,确定之后,将事件传递给对应的直属直属子view/ViewGroup,直属子view/ViewGroup再调用自己的dispatchTouchEvent进行分发。。。这样循环,直到某一级ViewGroup的onInterceptTouchEvent进行拦截,onInterceptTouchEvent返回true传递给自己的onTouchEvent方法或者一直没有任何ViewGroup的onInterceptTouchEvent方法拦截事件,而事件传递到最终(最底层)的子view的onTouchEvent方法的时候,这时候如果onTouchEvent方法返回true,则会消费掉本次事件, 如果这时候onTouchEvent方法返回false。。,则事件会依次向上传递,先传递给自己的上一级的view/viewGroup的onTouchEvent方法,然后依次上传,直到某一级的onTouchEvent方法返回true,消费掉本次事件,或者没有任何一个onTouchEvent方法方法消费掉本次事件,最后事件在最上一层onTouchEvent方法返回false 之后消失掉。
onInterceptTouchEvent方法可以提供一个拦截能力,但是onInterceptTouchEvent方法只有在viewGroup里面才有,所以只有 viewGroup才有拦截事件的能力。
对于dispatchTouchEvent和onInterceptTouchEvent可以这样理解,dispatchTouchEvent方法是一个快递员,onInterceptTouchEvent方法是公司的门卫,快递员要给公司送的每批快递就是一个完整的触摸事件,每一批快递有一个为首的物品:Down事件;送货有一个规定:如果这批快递的为首的这个物品(Down)被门卫(onInterceptTouchEvent)给拦截了,那么这批快递之后的其他物品(Move,Up等)都不能通过门卫,而只有在第一个物品(Down)事件被门卫(onInterceptTouchEvent)放行的情况下,这批快递之后的其他物品才有可能投递成功。
一些要点:
1、Touch事件是由硬件捕获到触摸后由系统传递给应用的ViewRoot,再由ViewRoot往下一层一层传递.
2、处理过程都是自上而下的分发,可以看成是由布局的“包含”关系,自顶向下的方式
3、事件存在消耗,事件的处理方法都会返回一个boolean值,如果该值为true,则本次事件下发将会被消费掉,而不会继续往下一层传递.
4、Touch事件从ACTION_DOWN开始,也就是按下的这个action开始算起,到ACTION_UP抬起时结束;但是如果在ACTION_DOWN的时候,没有接受事件,那么后续的其他动作也将不会被接受
5、dispatchTouchEvent方法,是用来分发上层传递过来的事件的,它在View和ViewGroup中都有实现
6、onInterceptTouchEvent方法,是用来拦截事件传递的,它只在ViewGroup中有实现,在View中没有
7、view对象的TouchLitener中的onTouch方法总是会先于view自己的onTouchEvent(event)方法被执行,这是由View中的dispatchEvent方法决定。
8、Activity中的onTouchEvent只会在能响应的所有view都响应完事件,且都没有消费掉事件之后才会被调用。
9、如果一个ViewGroup被点击的地方,有多个子View/ViewGroup可以响应点击事件,那么它们响应的顺序是:后addView进来的子view/ViewGroup先响应事件或者是xml布局文件中后被添加的view先 响应触摸事件
触摸事件例子:
先来看一个简单的例子:这是一个底层布局为FrameLayout,其中又有一个RelativeLayout,RelativeLayout中又有一个TextView;这里说的“中”是指addView的关系。我们这里都使用自定义view的形式来实现,然后分别在MyFrameLayout,MyRelativeLayout和MyTextView中实现dispatchTouchEvent方法,并打印相关信息:
三层结构代码:
package com.example.eventdispatch; import android.app.Activity; import android.graphics.Color; import android.os.Bundle; import android.view.Gravity; import android.view.View; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; import android.widget.RelativeLayout; public class MainActivity extends Activity { View view = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(initView()); } private View initView() { // 初始化三个控件 MyFrameLayout mFrameLayout = new MyFrameLayout(this); MyRelativeLayout mRelativeLayout = new MyRelativeLayout(this); MyTextView myTextView = new MyTextView(this); // 分别设置LayoutParams LayoutParams mFrameLayoutParams = new FrameLayout.LayoutParams(200, 200); android.widget.RelativeLayout.LayoutParams mRelativeLayoutParams = new RelativeLayout.LayoutParams( 100, 100); // 将RelativeLayout添加到FrameLayout中 mFrameLayout.addView(mRelativeLayout, mFrameLayoutParams); // 将TextView添加到RelativeLayout中 mRelativeLayout.addView(myTextView, mRelativeLayoutParams); // 设置Gravity,居中 mRelativeLayout.setGravity(Gravity.CENTER); mFrameLayoutParams.gravity = Gravity.CENTER; // 设置三个控件的颜色 mFrameLayout.setBackgroundColor(Color.RED); mRelativeLayout.setBackgroundColor(Color.GREEN); myTextView.setBackgroundColor(Color.BLUE); //将FrameLayout返回,作为Activity显示的View return mFrameLayout; } }
FrameLayout:
package com.example.eventdispatch; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.FrameLayout; /** * @author : 苦咖啡 * * @version : 1.0 * * @date :2015年4月15日 * * @blog : http://blog.csdn.net/cyp331203 * * @desc : */ public class MyFrameLayout extends FrameLayout { public MyFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public MyFrameLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public MyFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); } public MyFrameLayout(Context context) { super(context); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { System.out.println("--MyFrameLayout-->dispatchTouchEvent-->start"); boolean b = super.dispatchTouchEvent(ev); System.out.println("--MyFrameLayout-->dispatchTouchEvent-->end-->" + ev.getAction() + "-->" + b); return b; } }
MyRelativeLayout:
package com.example.eventdispatch; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.RelativeLayout; /** * @author : 苦咖啡 * * @version : 1.0 * * @date :2015年4月15日 * * @blog : http://blog.csdn.net/cyp331203 * * @desc : */ public class MyRelativeLayout extends RelativeLayout { public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); // TODO Auto-generated constructor stub } public MyRelativeLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // TODO Auto-generated constructor stub } public MyRelativeLayout(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub } public MyRelativeLayout(Context context) { super(context); // TODO Auto-generated constructor stub } @Override public boolean dispatchTouchEvent(MotionEvent ev) { System.out.println("--MyLinearLayout-->dispatchTouchEvent-->start"); boolean b = super.dispatchTouchEvent(ev); System.out.println("--MyLinearLayout-->dispatchTouchEvent-->end-->" + ev.getAction() + "-->" + b); return b; } }
MyTextView:
package com.example.eventdispatch; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.TextView; /** * @author : 苦咖啡 * * @version : 1.0 * * @date :2015年4月15日 * * @blog : http://blog.csdn.net/cyp331203 * * @desc : */ public class MyTextView extends TextView { public MyTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public MyTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); } public MyTextView(Context context) { super(context); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { System.out.println("--MyTextView-->dispatchTouchEvent-->start"); boolean b = super.dispatchTouchEvent(ev); System.out.println("--MyTextView-->dispatchTouchEvent-->end-->" + ev.getAction() + "-->" + b); return b; } }
运行界面结构和UI关系如图:
红色为MyFrameLayout,绿色为MyRelativeLayout,蓝色为MyTextView
下面我们分别触摸红色(MyFrameLayout),绿色(MyRelativeLayout)和蓝色区域(MyTextView):
触摸红色(MyFrameLayout)打印信息:
触摸绿色(MyRelativeLayout)打印信息:
触摸蓝色区域(MyTextView)打印信息:
我们会发现三者的事件分发是包含关系:
MyTextView的dispatchToutchEvent方法是在MyRelativeLayout的dispatchToutchEvent方法调用的过程之中被执行完毕的,而MyRelativeLayout的dispatchToutchEvent方法是在MyFrameLayout的dispatchToutchEvent方法执行过程之中被执行完毕的。
下面我们将addView的方式改变一下,让MyTextView作为MyFrameLayout的直接子View,而不再是MyRelativeLayout的子view,而且让MyTextView在MyRelativeLayout之后被addView添加进MyFrameLayout中:
android.widget.FrameLayout.LayoutParams mFrameLayoutParams2 = new FrameLayout.LayoutParams(100, 100); // 将RelativeLayout添加到FrameLayout中 mFrameLayout.addView(mRelativeLayout, mFrameLayoutParams); // 将TextView添加到FrameLayout中 mFrameLayout.addView(myTextView, mFrameLayoutParams2);
这时,界面UI关系如图:
然后我们再一次点击蓝色区域,打印的信息如下:
我们发现,这一次打印的信息与之前点击蓝色MyTextView区域时的打印信息不一样,MyTextView的dispatchTouchEvent方法并没有在MyRelativeLayout的dispatchTouchEvent方法内被调用,而是在MyFrameLayout的dispatchToutchEvent方法执行过程之中被执行完毕的;而且MyTextView的dispatchTouchEvent方法在MyRelativeLayout的dispatchTouchEvent方法开始之前执行就已经执行完毕了。
这是为什么呢?我们暂且留下这个问题
层级关系:
下面,我们就从UI层级结构和源码出来,来一步步搞清楚这几个问题。
先来看看第一个例子的UI的层级关系图,为了简明起见,我们在setContentView之前加上一句:requestWindowFeature(Window.FEATURE_NO_TITLE);不显示ActionBar,这样会更清晰一些,层级图如下:
上图中的LineareLayout和FrameLayout以及ViewStub本来是与ActionBar相关的组件,由于我们添加了requestWindowFeature(Window.FEATURE_NO_TITLE);不显示ActionBar,所以变成了现在的这个布局。
我们可以看到我们自己写的MyFrameLayout、MyRelativeLayout和MyTextView并不是直接挂载在view树的根节点上,根节点是一个PhoneWindow类中的内部类DecorView对象,这是个什么玩意儿呢?我们可以从Activity的源码来看看:
在MainActivity中,我们调用setContentView来设置我们自己定义的布局的根View/ViewGroup,Activity中的setContentView是这样的:
public void setContentView(View view) { getWindow().setContentView(view); initActionBar();//初始化ActionBar,这一句忽略,今天关注Touch事件 }
我们可以看到它实际上是调用getWindow()方法的返回值的setContentView方法;
再来看看getWindow()方法:
public Window getWindow() { return mWindow; }
发现返回的是一个mWindow,而这个mWindow是一个Activity类中 Window类型的成员变量:
private Window mWindow;
可能你已经在猜测这个window和PhoneWindow的关系了,Window是一个抽象类,其中的setContentView方法也是一个抽象方法,并没有实现。来看看Window类的注释:
The only existing implementation of this abstract class is android.policy.PhoneWindow, which you should instantiate when needing a Window.
意思是说:Window类只有一个实现类,那就是PhoneWindow。
这下明白了,我们再去看看PhoneWindow类的源码,这个类我们并不能直接使用,它位于:Android源码目录/frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java
在PhoneWindow中,有一个成员变量:DecorView mDecor,和一个内部类DecorView。那么这个DecorView和本文关注的触摸事件分发有什么联系呢?
系统有一个线程在循环收集屏幕硬件信息,当用户触摸屏幕时,该线程会把从硬件设备收集到的信息封装成一个MotionEvent对象,然后把该对象存放到一个消息队列中。
系统的另一个线程循环的读取消息队列中的MotionEvent,然后交给WMS去派发,WMS把该事件派发给当前处于活动的Activity,即处于活动栈最顶端的Activity.
这就是一个先进先出的消费者和生产者的模板,一个线程不停的创建MotionEvent对象放入队列中,另一个线程不断的从队列中取出MotionEvent对象进行分发.
当用户的手指从接触屏幕到离开屏幕,是一个完整的触摸事件,在该事件中,系统会不断收集事件信息封装成MotionEvent对象.收集的间隔时间取决于硬件设备,例如屏幕的灵敏度以及cpu的计算能力.目前的手机一般在20毫秒左右.
这里有一个和其他事件传递不同的地方,DecorView会优先传递给Activity,而不是它的子View.而Activity如果不处理又会回传给DecorView,DecorView才会再将事件传给子View.
dispatchTouchEvent就是触摸事件传递的对外接口,无论是DecorView传给Activity,还是ViewGroup传递给子View,都是直接调用对方的dispatchTouchEvent方法,并传递MotionEvent参数.
从源码看触摸事件分发:
由于专栏关注自定义控件,所以关于系统如何从硬件获取触摸事件以及传递到Activity的dispatchTouchEvent就不详细分解,下面将从Activity的dispatchTouchEvent方法来一步步看事件是如何被分发传递的:
Activity中的dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
其中onUserInteraction();是一个空实现,是系统留给我们的一个修改事件分发的一个方法,这里可以忽略。
所以实际上Activity的dispatchTouchEvent方法是调用的PhoneWindow的superDispatchTouchEvent方法,如果superDispatchTouchEvent返回false,没有消费掉事件,那么才会再交给activity的onTouchEvent方法去处理,从这个角度来讲,如果所有地方都没有消费掉事件,最后接收事件的会是Activity的onTouchEvent方法。
那么下面我们来看看PhoneWindow中的superDispatchTouchEvent方法:
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
发现实际上调用的是DecorView对象mDecor的superDispatchTouchEvent方法,来看看DecorView的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
所以调用的是FrameLayout中的dispatchTouchEvent方法,而FrameLayout并没有重写dispatchTouchEvent方法,所以实际调用的是FrameLayout的父类 ---> ViewGroup中的dispatchTouchEvent方法,下面这个图描述了从系统得到MotionEvent实际到传递给DecorView的super.dispatchTouchEvent的过程:
ViewGroup中的dispatchTouchEvent:
下面就来分析一下ViewGroup中的dispatchTouchEvent源码,这个是比较重要的部分:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { // 调试作用,忽略 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } boolean handled = false;// handled相当于最后的返回值,初始是false // onFilterTouchEventForSecurity(ev)使用安全机制来过滤事件,true的话则继续,false则过滤掉事件 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 如果接收到的action是DOWN操作,则重置之前的状态,重新开始一个新的触摸事件 // 这样不会被之前的事件影响 if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } final boolean intercepted;// intercepted是决定是否要拦截事件的标志 // 下面这一段if/else实际上就是说,如果第一次DOWN操作的时候,被拦截了,那么之后的UP,MOVE等操作,都会被拦截 // 注意这里,如果这里是按下的操作,那么代表是第一次触发本次的触摸事件,这时候mFirstTouchTarget应该是等于null的 // mFirstTouchTarget 代表处理触摸事件的第一个目标 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 如果是down事件或者已经有触摸事件的目标view,才考虑是否要拦截的问题 // 读取是否禁止拦截,disallowIntercept为true,表示禁止拦截;false表示允许拦截 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 如果允许拦截,则获取拦截的标志intercepted的值,来判断是不是要真的拦截 // 从onInterceptTouchEvent()方法中获取intercepted intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was // changed } else { // 如果不允许拦截,则拦截标志intercepted当然是要设置成false intercepted = false; } } else { // mFirstTouchTarget=null且不是ACTION_DOWN事件,代表不是首次按下,而且也没有一个目标对象来处理这个action,则肯定要拦截掉 intercepted = true; } // 获取是否要取消事件 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // split默认为 true ,表示是否把事件分发给多个子View final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; // newTouchTarget代表本次将要分发事件的目标,初始设置为null TouchTarget newTouchTarget = null; // 是否已经分发到新触摸目标标志位,初始设置为false boolean alreadyDispatchedToNewTouchTarget = false; // 开始响应触摸 if (!canceled && !intercepted) { // 如果不cancel也不被拦截,则进入到里面 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { // 如果是action_down,则进来 final int actionIndex = ev.getActionIndex(); // always 0 for // down // 如果split==true,则把pointerId与事件代码actionIndex关联起来 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) { // 拿到对应action的x,y的坐标,以便后面判断x,y是否落在子view范围内 final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // 拿到所有的子view集合 final View[] children = mChildren; final boolean customOrder = isChildrenDrawingOrderEnabled(); // i 从 childrenCount - 1开始,遍历到 0; // 倒序遍历所有的子view,这是有原因的,这里的children中的顺序,实际上是按照addView或者XML布局文件中的顺序来的, // 后addView添加的子View,会因为Android的UI后刷新机制,显示在上层;假如点击的地方,有两个子View都包含的点击的坐标,那么后被添加 // 到布局中的那个子view,会先响应事件;这样其实也是符合人的思维方式的,因为后被添加的子view会浮在上层,所以我们去点击的时候,一般 // 都会希望点击最上层的那个组件,先去响应事件 for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder( childrenCount, i) : i; final View child = children[childIndex]; // 判断一下子view是否能够接收到这个事件,这个事件的x,y坐标,是否落在子view上 // 如果不能,则continue,继续遍历下一个 // canViewReceivePointerEvents()方法实际上会去判断这个子view是否可见或者在播放动画 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { // 如果这个子view,接收不到事件,那么就continue,查询下一个子view continue; } // 通过getTouchTarget去查找View是否在mFirstTouchTarget.next这条target链中的某一个targe中了 // 如果在则返回这个target,否则返回null newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // 如果返回的newTouchTarget不为null,则表示当前子view已经接收当前事件了,则不需要再继续遍历寻找,break掉。 // Child is already receiving touch within its // bounds. // Give it the new pointer in addition to the // ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; // 找到了接收了事件的子view了,所以这里break掉循环 break; } resetCancelNextUpFlag(child); // 如果上面没有break,只有newTouchTarget为null,说明上面我们找到的子view和之前的肯定不是同一个了,是新增的, // 比如多点触摸的时候,两个手指分别按在不同的子view上的情况 // 这时候我们就看子view上是否分发该事件。 // 在这里dispatchTransformedTouchEvent实际上就是做了个判断:如果child==null, // 则调用super.dispatchTouchEvent,也就是view中的方法,如果chile!=null,则调用child.dispatchTouchEvent if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 如果这个dispatchTransformedTouchEvent方法返回true,意味着在child这一条事件线上,事件被接收且消费掉了, // 那么就更新状态信息,把当前newTouchTarget设置成当前的子view,然后break掉循环 mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = childIndex; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } }//遍历children的for循环的结束括号 }//if (newTouchTarget == null && childrenCount != 0) 的结束括号 // newTouchTarget == null表示没有找到一个能够接收事件的子view, //如果这时mFirstTouchTarget不为空,那么我们可以顺着mFirstTouchTarget.next的链,去找最后一个不为空的target 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进入的结束括号 }//if (!canceled && !intercepted) {的结束括号 if (mFirstTouchTarget == null) { // 这种情况一般发生在在Down事件的时候就被onIntercept方法拦截掉,所以mFirstTouchTarget还是null // 该viewGroup里,没有touch目标,则当成一个普通view处理 // 这里第三个参数,本来是响应事件的view,这里传一个null进去, // 则会调用super.dispatchTouchEvent,也就是当成一个view来处理 // 实际上就是调用这个view的onTouchListener中的onTouch方法(如果设置了监听) // 如果没有设置监听,或者监听的onTouch方法返回false,则会调用view的onTouchEvent方法 // 由于ViewGroup没有重写onTouchEvent方法,所以这个View的onTouchEvent方法也可以说是ViewGroup的onTouchEvent方法 // 而且这里也要依赖canceled的值来决定是否cancel事件 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 这个else里的情况,有可能是在Down事件时没有被拦截,而在之后跟随的其他Action时被拦截 // 所以mFirstTouchTarget不为null的情况 // 也有可能是没有被拦截,但是找不到一个子view/viewGroup来接收事件的情况 // Dispatch to touch targets, excluding the new touch target if // we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; // 从mFirstTouchTarget开始遍历 TouchTarget target = mFirstTouchTarget; // 遍历所有target进行dispatch分发 // 这里的遍历跟之前children的遍历不一样,那里第二个参数直接是false,而这里需要考虑是否cancel // dispatchTransformedTouchEvent(ev, false, child, // idBitsToAssign) while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { // 找到了新的子 View,并且这个是新加的对象,上面已经处理过了。 handled = true; } else { // 如果不是接收目标 // 则判断是否要cancel该target的事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 如果这个cancelChild=true,则在dispatchTransformedTouchEvent会有 // event.setAction(MotionEvent.ACTION_CANCEL);这一句,然后再调用dispatchTouchEvent的时候 // 就会走cancel的流程了 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { // 如果这里条件成立,则表示target.child接收了这个事件,则handled = true handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } // 记录当前target,然后继续下一个target predecessor = target; target = next; } } // Update list of touch targets for pointer up or cancel, if needed. // 返回之前的善后工作 if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { // 手指抬起的时候,清除掉相关数据 final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
View中的dispatchTouchEvent:
我们再来看看View的dispatchTouchEvent方法:
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; }
View的dispatchTouchEvent方法比较简单,就是view如果设置了onTouchListener的话,就执行onTouchListener.onTouch方法,如果这个方法返回false或没有设置Listener的话,那么就执行view的onTouchEvent方法。如果上面两个方法都返回false,则返回false,否则返回true。
结合上面ViewGroup和view的事件分发代码,给出一个事件分发的主干流程,略去了中间一些细节和判断(newTouchTarget,cancel等):
那么到这里,可以给出前面留下的问题的答案了,因为事件分发总是一级一级的往下分发,每一级都会遍历自己所有的子view/viewGroup,然后这其中能够响应事件的子ViewGroup再调用自己的dispatchTouchEvent方法,继续遍历自己所有的子view/viewGroup,所以在最开始的那种情况中,MyFrameLayout的dispatchTouchEvent会包含MyRelativeLayout的dispatchTouchEvent方法调用,而MyRelativeLayout的dispatchTouchEvent方法调用会包含MyTextView的dispatchTouchEvent方法的调用。
而在改为将MyTextView作为MyFrameLayout的子view之后,在调用MyFrameLayout的dispatchTouchEvent时,会遍历它的所有的子view/viewGroup,这就包含了MyTextView和MyRelativeLayout,而这个遍历是倒序遍历,也就是说后addView进来的子view会先被遍历到,先响应触摸事件,而代码里MyTexitView是后被添加进来的,所以会在MyRelativeLayout的dispatchTouchEvent方法调用之前先执行完MyTexitView的dispatchTouchEvent方法。
一些总结:
Down事件:
通过onInterceptTouchEvent方法判断是否要拦截事件,默认fasle根据scroll换算后的坐标找出所接受的子View。有动画的子View将 不接受触摸事件。
找到能接受的子View后把event中的坐标转换成子View的坐标
调用子View的dispatchTouchEvent把事件传递给子View。
如果子View消费了该事件,则把target记录为子View,方便后面的Move和Up事件的传递。
如果子View没有消费,则继续寻找下一个子View。
如果没找到,或者找到的子View都不消费,就会调用View的dispatchTouchEvent的逻辑,也就是判断是否有触摸监听,有的话交给监听的onTouch处理,没有的话交给自己的onTouchEvent处理
Move和Up事件:
判断事件是否被取消或者事件是否要拦截住,是的话,给Down事件找到的target发送一个取消事件。如果不取消,也不拦截,并且Down已经找到了target,则直接交给target处理,不再遍历子View寻找合适的View了。
这种处理事件是正确的,我们用手机经常可以体会到,当我手指按在一个拖动条上之后,在拖动的时候手指就算移出了拖动条,依然会把事件分发给拖动条控制它的拖动。
View的onTouchEvent:
从View的dispatchTouchEvent可以看出,事件最终的处理无非是交给TouchListener的onTouch方法或者是交由onTouchEvent处理,由于onTouch默认是空实现,由程序员来编写逻辑,那么我们来看看onTouchEvent事件。View只能响应click和longclick,不具备滑动等特性。
onIntercept方法返回true,事件被拦截之后,去了哪里?
对于Down事件的时候,被Intercept方法拦截之后,这时候mFirstTouchTarget肯定是=null的,所以这时候会调用handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);方法,这里由于传入的view对象=null,所以会导致直接调用super.dispatchTouchEvent方法,所以Touch事件被拦截之后,会转到View的事件分发中去了,而在View.dispatchTouchEvent中,如果当前view/viewGroup设置了onTouchListener,则会调用TouchListener.onTouch方法,如果没设置Listener或者TouchListener.onTouch返回false,则会调用View.onTouchEvent方法,如果View.onTouchEvent也返回false,那么事件会依次往上传递,这与一开始描述的一样。
以上内容都是自己琢磨源码和查阅资料得来,难免会有纰漏和错误,欢迎指正,谢谢!
请尊重原创劳动成果,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45071069,非允许请勿用于商业或盈利用途,违者必究。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。