码迷,mamicode.com
首页 > 移动开发 > 详细

Android自定义控件系列九:从源码看Android触摸事件分发机制

时间:2015-04-18 13:08:17      阅读:251      评论:0      收藏:0      [点我收藏+]

标签:android   触摸事件   事件分发   dispatchtouchevent   ointercepttouchevent   

请尊重原创劳动成果,转载请注明出处: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);
        }


调用的super.dispatchTouchEvent,而再来看看这个DecorView的继承关系:

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,非允许请勿用于商业或盈利用途,违者必究。


Android自定义控件系列九:从源码看Android触摸事件分发机制

标签:android   触摸事件   事件分发   dispatchtouchevent   ointercepttouchevent   

原文地址:http://blog.csdn.net/cyp331203/article/details/45071069

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!