Android的事件分发

1. Touch事件和绘制事件的异同之处

Touch事件和绘制事件非常相似,都是由ViewRoot派发下来的,可是不同之处在绘制事件是由应用中的某个View发起请求,一层一层上传到ViewRoot。再有ViewRoot下发绘制,传递canvas给全部子View让其绘制自身,绘制好后,再通知WMS进行画到屏幕上。而Touch事件是由硬件捕获到触摸后由系统传递给应用的ViewRoot,再由ViewRoot往下一层一层传递。

他们的处理过程都是自上而下的分发,可是绘制多了一层自下往上的请求。

事件存在消耗。事件的处理方法都会返回一个boolean值。假设该值为true,则本次事件下发将会终止。

2. MotionEvent

2.1 MotionEvent对象的产生

系统有一个线程在循环收集屏幕硬件信息。当用户触摸屏幕时,该线程会把从硬件设备收集到的信息封装成一个MotionEvent对象,然后把该对象存放到一个消息队列中。

系统的还有一个线程循环的读取消息队列中的MotionEvent。然后交给WMS去派发,WMS把该事件派发给当前处于活动的Activity,即处于活动栈最顶端的Activity。

这就是一个先进先出的消费者和生产者的模板。一个线程不停的创建MotionEvent对象放入队列中。还有一个线程不断的从队列中取出MotionEvent对象进行分发。

当用户的手指从接触屏幕到离开屏幕,是一个完整的触摸事件。在该事件中。系统会不断收集事件信息封装成MotionEvent对象。

收集的间隔时间取决于硬件设备,比如屏幕的灵敏度以及cpu的计算能力。眼下的手机一般在20毫秒左右。

MotionEventCompat.getActionMasked()

2.2 MotionEvent对象具体解释

MotionEvent对象包括了触摸事件的时间、位置、面积、压力、以及本次事件的Dwon发生的时间。

MotionEvent经常使用的Action分为5种:Down 、Up、Move、Cancel、OutSide

MotionEvent中我们经常使用的方法就是获取点击的坐标,由于这是与我们操作息息相关的。获取坐标有两种方式:

  • getX和getY用于获取以该View左上角为坐标原点的坐标
  • getRowX和getRowY用于获取以屏幕左上角为坐标原点的坐标

2.3 5种Touch事件

  • Down:一次触摸事件的第一个MotionEvent对象,即手指初次接触屏幕。
  • Up:通常为一次触摸事件的最后一个MotionEvent对象,即手指离开屏幕。
  • Move:通常多次发生在一次触摸事件之中。表示触摸点发生了移动,我们通常把手指放到屏幕上,实际也会触发该事件。由于人手总是在轻微抖动的。
  • Cancel:经常使用于取消某个触摸事件,通常是由程序逻辑来指定该事件,用于取消某次触摸事件。
  • OutSide:当触摸点发生在响应事件的View之外时,传递的事件,通常由程序逻辑来指定。

在上面5种事件中。Down为最重要的事件,由于这是一个触摸事件的起始点。程序的非常多逻辑推断。都须要依据该事件做处理,比如分发拦截。一次触摸事件必须要有Down事件,这也是MotionEvent对象中都包括了本次触摸事件的Down事件发生的时间点这个属性。其次是Move和Up,通过这3个事件的逻辑处理,就构建出来滑动,点击,长按,双击等多种效果。

2.4 创建一个MotionEvent对象

public static MotionEvent obtain(
        long downTime,    //当用户最初按下開始一连串的位置事件。这必须得到SystemClock.uptimeMillis()
        long eventTime,   //当这个特定的事件是生成的。这必须得到SystemClock.uptimeMillis()            
        int action,       //该次事件的Action                       
        float x,          //该次事件的x坐标        
        float y,          //该次事件的y坐标         
        float pressure,   //该次事件的压力,通常感觉标准压力,从0-1取值     
        float size,       //点击的区域大小,通常依据特定标准范围从0-1取值     
        int metaState,    //一个修饰性的状态。好像一直都是0          
        float xPrecision, //x坐标的准确度           
        float yPrecision, //y坐标的准确度                   
        int deviceId,     //触屏设备id,假设是0,说明这个事件不是来自物理设备      
        int edgeFlags     //系统默认都是返回0,程序在传递时,能够通过逻辑推断加入方向位置 
)

或者一个更简单的方式:

public static MotionEvent obtain(
            long downTime,
            long eventTime,
            int action,
            float x,
            float y,
            int metaState)

也能够通过一个MotionEvent来创建一个新的

public static MotionEvent obtain(MotionEvent event)

通过以上的方式。我们知道。我们也能够通过代码来构建一个虚假的MotionEvent。并分发下去。

view.dispatchTouchEvent(
            MotionEvent.obtain(SystemClock.uptimeMillis(),
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_DOWN,100,100,0));

然后通过延迟以此往下派发Move和Up时间,形成一个完整的触摸操作。

3. dispatchTouchEvent触摸事件分发

之前我们知道触摸事件是被包装成MotionEvent进行传递的,而该对象是继承了Parcelable接口,正由于如此,才干够从系统中传递到我们的应用中。系统通过跨进程通知ViewRoot,ViewRoot会调用DecorView的dispatchTouchEvent下发。

这里有一个和其它事件传递不同的地方。DecorView会优先传递给Activity。而不是它的子View。

而Activity假设不处理又会回传给DecorView。DecorView才会再将事件传给子View。

dispatchTouchEvent就是触摸事件传递的对外接口,不管是DecorView传给Activity,还是ViewGroup传递给子View,都是直接调用对方的dispatchTouchEvent方法,并传递MotionEvent參数。

我们首先来看看Activity中的dispatchTouchEvent逻辑:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
        //这是一个空实现的方法,以便子类实现。该方法在Key事件和touch事件的dispatch方法中都被调用,
        // 就是方便用户在事件被传递之前做一下自己的处理。
    }
    //这才是事件真正的分发
    if (getWindow().superDispatchTouchEvent(ev)) {
        //superDispatchTouchEvent是一个抽象方法,可是getWindow()获取的对象实际是FrameWork层的
        // PhoneWindow,该对象实现了这种方法,内部是直接调用DecorView的superDispatchTouchEvent
        // 是直接调用dispatchTouchEvent,这样就传递到子View中了   
        return true;
    }
    //假设上面事件没有被消费掉。那么就调用Activity的onTouchEvent事件。
    return onTouchEvent(ev);
}
//PhoneWindow的superDispatchTouchEvent方法直接调用了mDecor的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
//mDecor即为Activity真正的根View。我们通过setContentView所加入的内容就是加入在该View上,
// 它实际上就是一个FrameLayout
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);//FrameLayout.dispatchTouchEvent
}

至此我们已经至少明确了以下几点:

1、我们能够重载Activity的onUserInteraction方法,在Down事件触发传递前,实现我们的一些需求。实际上源代码中有非常多这种方法,再某个方法体的第一行提供一个空实现的回调方法,在某个方法的最后一行提供一个空实现的回调方法,以便子类去实现自己的逻辑。比如AsyncTask就有相似的方式。这些技巧都能非常好的提高我们代码的扩展性。

2、Activity会间接的调用根View的dispatchTouchEvent,并通过if推断返回值,假设为true,即向上层返回true。也就是调用Activity的dispatchTouchEvent的WMS。即操作系统。

3、假设if推断为false。即根View和根View下的全部子View均为消费掉该事件,那么以下的代码就有运行机会,即Activity的onTouchEvent,并把该方法的返回值作为结果返回给上层。

3.1 View的dispatchTouchEvent

View中的处理相当简单明了,由于不涉及到子View,所以仅仅在自身内部进行分发。首先推断是否设置了触摸监听,而且能够响应事件,就交由监听的onTouch处理。假设上述条件不成立,或者监听的onTouch事件没有消费掉该事件。则交由onTouchEvent进行处理,并把返回结果交给上层。

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        //推断mOnTouchListener是否存在,而且控件可点的情况下。运行onTouch,假设onTouch返回true。就消耗该事件
        return true;
    }
    //假设以上条件都不成立。则把事件交给onTouchEvent来处理
    return onTouchEvent(event);
}

3.2 ViewGroup的dispatchTouchEvent

3.3 Down事件

  • 通过onInterceptTouchEvent方法推断是否要拦截事件,默认fasle
  • 依据scroll换算后的坐标找出所接受的子View。

    有动画的子View将不接受触摸事件。

  • 找到能接受的子View后把event中的坐标转换成子View的坐标
  • 调用子View的dispatchTouchEvent把事件传递给子View。
  • 假设子View消费了该事件,则把target记录为子View。方便后面的Move和Up事件的传递。
  • 假设子View没有消费,则继续寻找下一个子View。
  • 假设没找到,或者找到的子View都不消费,就会调用View的dispatchTouchEvent的逻辑,也就是推断是否有触摸监听,有的话交给监听的onTouch处理,没有的话交给自己的onTouchEvent处理

接下来我们来研究ViewGroup的dispatchTouchEvent。这是略微复杂的分发逻辑。

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();//获取事件
    final float xf = ev.getX();//获取触摸坐标
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;//获取当前须要偏移的偏移量量
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;    //当前ViewGroup的视图矩阵

    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//是否禁止拦截

    if (action == MotionEvent.ACTION_DOWN) {//假设事件是按下事件
        if (mMotionTarget != null) {    //推断接受事件的target是否为空
            //不为空肯定是不正常的,由于一个事件是由DOWN開始的。而DOWN还没有被消费,所以目标也不是不可能被确定,
            //造成这个的原因可能是在上一次up事件或者cancel事件的时候,没有把目标赋值为空
            mMotionTarget = null;    //在此处拯救
        }
        //不同意拦截,或者onInterceptTouchEvent返回false,也就是不拦截。

注意,这个推断都是在DOWN事件中推断 if (disallowIntercept || !onInterceptTouchEvent(ev)) { //从新设置一下事件为DOWN事件,事实上没有必要,这仅仅是一种保护错误,防止被篡改了 ev.setAction(MotionEvent.ACTION_DOWN); //開始寻找能响应该事件的子View final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; for (int i = count - 1; i >= 0; i--) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {//假设child可见,或者有动画,获取该child的矩阵 child.getHitRect(frame); if (frame.contains(scrolledXInt, scrolledYInt)) { // 设置系统坐标 final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); if (child.dispatchTouchEvent(ev)) {//调用child的dispatchTouchEvent //假设消费了,目标就确定了,以便接下来的事件都传递给child mMotionTarget = child; return true; //事件消费了,返回true } } } } //能到这里来。证明全部的子View都没消费掉Down事件,那么留给以下的逻辑进行处理 } } //推断是不是up或者cancel事件 boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { //假设是取消,把禁止拦截这个标志位给取消 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } final View target = mMotionTarget; if (target == null) { //推断该值是否为空。假设为空,则没找到能响应的子View,那么直接调用super的dispatchTouchEvent,也就是View的dispatchTouchEvent ev.setLocation(xf, yf); return super.dispatchTouchEvent(ev); } //能走到这里来,说明已经有target,那也说明。这里不是DOWN事件。由于DOWN事件假设有target,已经在前面返回了,运行不到这里 if (!disallowIntercept && onInterceptTouchEvent(ev)) {//假设有目标,又非要拦截。则给目标发送一个cancel事件 final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setAction(MotionEvent.ACTION_CANCEL);//该为cancel ev.setLocation(xc, yc); if (!target.dispatchTouchEvent(ev)) { //调用子View的dispatchTouchEvent,就算它没有消费这个cancel事件,我们也无能为力了。

} //清除目标 mMotionTarget = null; //有目标。又拦截,自身也享受不了了。由于一个事件应该由一个View去完毕 return true;//直接返回true,以完毕这次事件,好让系统開始派发下一次 } if (isUpOrCancel) {//取消或者UP的话。把目标赋值为空。以便下一次DOWN能又一次找,此处就算不赋值。下一次DOWN也会先把它赋值为空 mMotionTarget = null; } //又不拦截,又有目标,那么就直接调用目标的dispatchTouchEvent final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); return target.dispatchTouchEvent(ev); //也就是说,假设是DOWN事件,拦截了,那么每次一次MOVE或者UP都不会再推断是否拦截,直接调用super的dispatchTouchEvent //假设DOWN没拦截。就是有其它View处理了DOWN事件,那么接下来的MOVE或者UP事件拦截了。那么给目标View发送一个cancel事件,告诉它touch被取消了,而且自身也不会处理。直接返回true //这是为了不违背一个Touch事件仅仅能由一个View处理的原则。 }

3.4 Move和Up事件

推断事件是否被取消或者事件是否要拦截住,是的话,给Down事件找到的target发送一个取消事件。假设不取消,也不拦截,而且Down已经找到了target,则直接交给target处理。不再遍历子View寻找合适的View了。

这种处理事件是正确的,我们用手机经常能够体会到,当我手指按在一个拖动条上之后,在拖动的时候手指就算移出了拖动条,依旧会把事件分发给拖动条控制它的拖动。

4. onInterceptTouchEvent

ViewGroup的方法。事件拦截,return true表示拦截触摸事件,事件就不往下传递

子View能够调用getParent().requestDisallowInterceptTouchEvent( true ) 请求父控件不拦截touch事件

5. View的onTouchEvent

从View的dispatchTouchEvent能够看出。事件终于的处理无非是交给TouchListener的onTouch方法或者是交由onTouchEvent处理,由于onTouch默认是空实现,由程序猿来编写逻辑,那么我们来看看onTouchEvent事件。View仅仅能响应click和longclick,不具备滑动等特性。

Down时,设置按压状态,发送一个延迟500毫秒的长按事件。


Move时。推断是否移出了View,移出后移除按压状态,长按事件。
Up时,取消按压,并推断它能否够通过触摸获取焦点。是的话设置焦点。推断长按事件是否运行了,假设还没运行。就删除,并运行点击事件。

public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;
    //先推断标示位是否为disable,也就是无法处理事件。

if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); }//假设是UP事件,而且状态为按压,取消按压。 //系统源代码解释:尽管是disable,可是还是能够消费掉触摸事件,仅仅是不触发不论什么click或者longclick事件。 //依据是否可点击,可长按来决定是否消费点击事件。 return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } if (mTouchDelegate != null) { //先检查触摸的代理对象是否存在,假设存在。就交由代理对象处理。

// 触摸代理对象是能够进行设置的。一般用于当我们手指在某个View上。而让另外一个View响应事件。另外一个View就是该View的事件代理对象。

if (mTouchDelegate.onTouchEvent(event)) {//假设代理对象消费了,则返回true消费该事件 return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { //假设是可点击或者长按的标识位运行以下的逻辑。这些标志位能够设置,也能够设置了相应的listener后自己主动加入 //由于作为一个View,它仅仅能单纯的接受处理点击事件,像滑动之类的复杂事件普通View是不具备的。

switch (event.getAction()) { case MotionEvent.ACTION_UP://处理Up事件 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;//是否包括暂时按压状态 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {//假设本身处于被按压状态或者暂时按压状态 //暂时按压状态会在以下的Move事件中说明 boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { //假设它能够获取焦点,而且能够通过触摸来获取焦点,而且如今不是焦点。则请求获取焦点,由于一个被按压的View理论上应该获取焦点 focusTaken = requestFocus(); } if (prepressed) { //假设是暂时按压,则设置为按压状态,PFLAG_PREPRESSED是一个非常短暂的状态。用于在某些时候短时间内表示Pressed状态,但不须要绘制 setPressed(true);//设置为按压状态。是由于暂时按压不会绘制。这个时候强制绘制一次。确保用户能够看见按压状态 } if (!mHasPerformedLongPress) { //是否运行了长按事件,还没有的话,这个时候能够移除长按的回调了。由于UP都已经触发,说明从按下到UP的时间不足以触发longPress //至于longPress,会在Down事件中说明 removeLongPressCallback(); if (!focusTaken) {//假设是焦点状态。就不会触摸click,这是为什么呢?由于焦点状态通常是交给按键处理的, //pressed状态才是交给触摸处理,假设它是焦点。那么它的click事件应该由按键来触发 if (mPerformClick == null) { //封装一个Runnable对象。这个对象中实际就调用了performClick(); mPerformClick = new PerformClick(); } if (!post(mPerformClick)) {//向消息队列发生该runnabel。假设发送不成功。则直接运行该方法。

performClick();//这种方法内部会调用clickListner } //为什么不直接运行呢?假设这个时候直接运行,UP事件还没运行完,发送post。能够保障在这个代码块运行完毕之后才运行 } } if (mUnsetPressedState == null) {//仍旧是创建一个Runnabel对象,运行setPressed(false) mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { //假设是暂时按压状态。之前的Down和move都还未触发按压状态,仅仅在up时设置了,这个状态才刚刚绘制。为了保证用户能看到,发生一个64秒的延迟消息,来取消按压状态。 postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); //这是一个64毫秒的短暂时间。这是为了让这个按压状态持续一小段时间,以便手指离开时候,还能看见View的按压状态 } else if (!post(mUnsetPressedState)) {//假设不是暂时按压,则直接发送,发送失败,则直接运行 mUnsetPressedState.run(); } removeTapCallback(); //移除这个callBack,这个callBack内部就是把暂时按压状态设置成按压状态。由于这个已经不是必需了,手指已经up了 } break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; //按下事件把长按事件运行的变量设置为false,代表还没运行长按。由于才按下,表示新的一个长按事件能够開始计算了 if (performButtonActionOnTouchDown(event)) { //先把这个事件交由该方法。该方法内部会推断是否为上下文的菜单按钮,或者是否为鼠标右键,假设是就弹出上下文菜单。 //如今有些手机的上下文菜单按钮也是在屏幕触屏上的 break; } //这种方法会一直往上找父View,推断自身是否在一个能够滚动的容器中 boolean isInScrollingContainer = isInScrollingContainer(); //假设是在一个滚动的容器中,那么按压事件将会被推迟一段时间,假设这段时间内,发生了Move。那么按压状态讲不会被显示,直接滚动父视图 if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; //先加入暂时的按压状态,该状态表示按压,但不会绘制 if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); //创建一个runnable对象,这个runnable内部会取消暂时按压状态,设置为按压状态,并启动长按的延迟事件 } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); //向消息机制发生一个64毫秒的延迟时间,该事件会取消暂时按压状态,设置为直接按压,并启动长按时间的计时 } else { //假设不在一个滚动的容器中,则直接设置按压状态,并启动长按计时 setPressed(true); checkForLongClick(0); //长按事件就是向消息机制发送一个runnable对象,封装的就是我们在lisner中的代码。延迟500毫秒运行。也就是说长按事件在我们按下的时候发送,在up的时候检查一下运行了吗?假设没运行。就取消,并运行click } break; case MotionEvent.ACTION_CANCEL: //假设是取消事件,那就好办了,把我们之前发送的几个延迟runnable对象给取消掉 setPressed(false); //设置为非按压状态 removeTapCallback(); //取消mPendingCheckForTap。也就是不用再把暂时按压设置为按压了 removeLongPressCallback(); //取消长按事件的延迟回调 break; case MotionEvent.ACTION_MOVE: //move事件 final int x = (int) event.getX(); //取触摸点坐标 final int y = (int) event.getY(); // 用于推断是否在View中,为什么还要推断呢? //这是由于父View是在Down事件中推断是否在该View中的。假设在。以后的Move和up都会传递过来。不再进行范围推断 if (!pointInView(x, y, mTouchSlop)) { //mTouchSlop是一个常量。数值为8,也就是说。就算你的落点超出了View的8像素位置。也算在View中。

//是由于人的手指触摸点比較大,有可能你感觉点在某个控件的边缘。可是实际落点已经超出这个View,所以这里给了8像素的范围 removeTapCallback();//假设在范围外,就移除这些runnable回调 if ((mPrivateFlags & PFLAG_PRESSED) != 0) { //假设是按压状态,就取消长按。设置为非按压状态,为什么这个时候取消呢,由于在Down的时候,我们能够知道,仅仅有是按压状态,才会设置长按 removeLongPressCallback(); setPressed(false); } } break; } return true; //至此,能够返回true。消费该事件 } return false; //假设不可点击。也不可长按,则返回false,由于View仅仅具备消费点击事件 }

从上面的代码我们总结一下View对触摸事件的处理:

1、是否为diabale。假设是。直接依据是否设置了click和longclick来返回。


2、是否设置了触摸代理对象,假设有。把事件传递给触摸代理对象。交由其处理,假设消费了,直接返回
3、是否为click或者longclick的,假设是,返回true。不是返回false。

而View对click和longclick的处理例如以下:

Down:

  • 推断能否够触摸上下文菜单。
  • 是否在能够滑动的容器中,假设是先设置暂时按压,再发送一个延迟消息把暂时按压改为按压。并发送一个延迟500毫秒的事件去运行长按代码
  • 假设不在滚动容器中,直接设置按压状态,并发送一个延迟500毫秒的事件去运行长按代码。

Move:

  • 取触摸点坐标推断是否在View中(额外添加了8像素的范围)
  • 假设在,不用做不论什么事。

  • 假设不在,取消暂时按压到按压回调。取消长按延迟回调,设置为非按压状态

Up

  • 推断是否为按压或者暂时按压状态
  • 假设不是,不做不论什么处理
  • 假设是先推断其能否够获取焦点。然后请求焦点。
  • 假设是暂时按压状态。设置暂时按压状态为按压状态。保证界面被绘制成按压状态,让用户能够看见。

  • 假设长按回调还未触发。取消长按回调。假设不是焦点状态。触发click事件。

  • 假设是暂时按压状态,发送一个延迟取消按压状态的,保证按压状态持续一段时间。让用户可见。

  • 假设不是暂时按压状态,直接发送消息取消按压状态。发送失败。直接取消按压状态。

  • 取消把暂时按压设置按压的回调。

从中我们知道View的onTouchEvent主要处理了click和longclick事件,当按下时,向消息机制发送一个延迟500毫秒的长按回调事件。当移动时候推断是否移出了View的范围,超出则取消事件。当离开时,推断长按事件是否触发了,假设没触发且不是焦点,就触发click事件。

在这里最绕的就是暂时按压和按压状态,暂时按压是为了处理滑动容器的。让处于滑动容器中,按下时。我们先设置的是暂时按压,持续64毫秒,是为了推断接下来的时间内是否发生了move事件。假设发生了,将不会再出发按压状态,这样不会让用户看到listView滚动时,item还处于按压状态。在离开时,我们再次推断是否处于暂时按压,假设是在64毫秒内触发了down和up。说明按压状态还没来得急绘制。则强制设置为按压状态。保证用户能看到,并在取消回调的方法上加上64毫秒的延迟

6. onTouch与onClick

ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
iv_image.setOnTouchListener(new OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("iv_image---onTouch--" + event.getAction());
        return false;
    }
});

点击ImageView的时候仅仅会打印一次。由于onTouch()返回false,仅仅传递down事件,不会传递up事件

System.out: iv_image---onTouch--0
// ImageView天生不能被点击,没有点击事件
ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
iv_image.setOnTouchListener(new OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("iv_image---onTouch--" + event.getAction());
        return true; // 把返回值改为true 
    }
});

把onTouch()方法返回值改为true,点击ImageView会打印两次(down and up)

System.out: iv_image---onTouch--0
System.out: iv_image---onTouch--1
ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
iv_image.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("iv_image---onTouch--" + event.getAction());
        return true;
    }
});
//加入click事件
iv_image.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println("iv_image---onClick");
    }
});

还是打印两次,onTouch()返回true,click事件并不会得到运行

ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
iv_image.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("iv_image---onTouch--" + event.getAction());
        return false;
    }
});

iv_image.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println("iv_image---onClick");
    }
});

打印三次。两次touch事件(down and up)和一次click事件

Button button = (Button) findViewById(R.id.button);
button.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("button---onTouch--" + event.getAction());
        return false;
    }
});

点击Button会打印两次


Button button = (Button) findViewById(R.id.button);
button.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("button---onTouch--" + event.getAction());
        return true;
    }
});

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println("button---onClick");
    }
});

打印两次,由于onTouch()返回true,不会运行onTouchEvent(),而click事件是在onTouchEvent()中运行,所以也不会运行click事件


Button button = (Button) findViewById(R.id.button);
button.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("button---onTouch--" + event.getAction());
        return false;
    }
});

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println("button---onClick");
    }
});

打印三次

public boolean dispatchTouchEvent(MotionEvent event) {
    if (!onFilterTouchEventForSecurity(event)) {
        return false;
    }

    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

a 推断mOnTouchListener是否为null
b 推断当前的控件是否可用
c 推断view的onTouch。
d 假设以上一个返回为false。

那么就会调用onTouchEvent

首先推断mOnTouchListener不为null。而且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件假设都满足。直接return true ; 也就是以下的onTouchEvent(event)不会被运行了。

假设我们设置了setOnTouchListener,而且return true。那么View自己的onTouchEvent就不会被运行了

onTouch是优先于onClick运行, onClick的调用在onTouchEvent(event)方法中

view的事件分发

  1. 返回true。说明能够响应down事件和up事件
  2. 返回false,仅仅会响应down事件。不会响应up事件。

    在down事件假设能消费(处理)当前事件。

    那么在up的时候也会把事件传递给当前的view,在down事件处理不了当前事件。那么在up的时候。也不会把事件传递给当前的view

Android的事件分发实例分析

7. ScrollView的onTouchEvent

普通的ViewGroup并没有对onTouchEvent事件做处理,仅仅有能够滚动的才有,我们能够分析一下ScrollView

  • Down时,推断落点是否在子View中,不再就不处理,由于ScrollView仅仅有一个子View。

  • Move时,通过对照本次手指的位置和上一次的位置的距离,计算出Y方向的差值,然后用scorllBy进行滚动视图

  • Up时,通过速度进行fling,这里利用了两个帮助类,一个是计算速度的帮助类VelocityTracker,一个是滚动的帮助类Scroller

public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
        //假设是down事件。而且触摸到边缘,就不处理EdgeFlags代表是否为边缘,其值是1/2/4/8。代表上下左右
        return false;
    }
    if (mVelocityTracker == null) {
        //这是一个追踪触摸事件。并计算速度的帮助类,实现原理就是用三个数组分别记录每次触摸的x/y和时间
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);
    final int action = ev.getAction();
    switch (action & MotionEvent.ACTION_MASK) {//与上ff,去掉高位有关多点的信息
        case MotionEvent.ACTION_DOWN: {//假设是down
            final float y = ev.getY();//获取y坐标
            if (!(mIsBeingDragged = inChild((int) ev.getX(), (int) y))) {//推断是否開始拖动
                //原理就是推断落点是否在child中,ScrollView仅仅能由一个child,假设在。返回true。反之false
                //也就是说落点在child中,就是准备開始拖动,不在,就直接返回,这可能是由于设置了padding之类的缘故造成的
                return false;
            }
            if (!mScroller.isFinished()) {//推断滚动是否完毕
                mScroller.abortAnimation();//假设没完毕,停止滚动
                //相应上一次用户手指离开时候处理fling状态,这次按下手指,直接停止滚动
            }
            //记录y坐标,以便下次事件来对照
            mLastMotionY = y;
            mActivePointerId = ev.getPointerId(0);//记住多点的id,下次取值时仅仅取该点的
            break;
        }
        case MotionEvent.ACTION_MOVE:
            if (mIsBeingDragged) {//能够看出,假设down的时候落点在child外。则以后就算滑进了child也不处理
                //依据上次记录的多点id,找到相应的点,取y值
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                final float y = ev.getY(activePointerIndex);
                final int deltaY = (int) (mLastMotionY - y);//计算位移
                mLastMotionY = y;//又一次记录y值
                scrollBy(0, deltaY);//滚动指定的距离,这也说明了ScrollView仅仅具备纵向滑动
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mIsBeingDragged) {//假设是离开事件
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//计算最后1秒钟内的速度,并给定一个最大速度进行限制
                //这个最大速度是依据屏幕密度不同而不同的。所以大家也没事别使劲滑动屏幕,由于有这个最大速度限制
                //获取y方向的速度
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) {
                    //假设有子View,而且计算出来的y的速度比最小速度要大,运行fling状态
                    //手指滑动的方向和屏幕移动的方向是相反的,所以这里加-
                    fling(-initialVelocity);
                }
                mActivePointerId = INVALID_POINTER;//给mActivePointerId又一次赋值为-1,防止下次事件找到了错误的点
                mIsBeingDragged = false;//恢复默认值
                if (mVelocityTracker != null) {//清空速度计算帮助类
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
            }
            break;
        case MotionEvent.ACTION_CANCEL:
            if (mIsBeingDragged && getChildCount() > 0) {//推断条件,仅仅有这2个条件成立,才会发生滚动事件。以下的值才会被改变。才须要恢复默认
                mActivePointerId = INVALID_POINTER;
                mIsBeingDragged = false;
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
            }
            break;
        case MotionEvent.ACTION_POINTER_UP://多点触摸时。不是最后一个点离开
            onSecondaryPointerUp(ev);
            break;
    }
    return true;
}
//用于应对先按下1点,然后按下2点,1点离开后,2点仍能继续滑动的逻辑
private void onSecondaryPointerUp(MotionEvent ev) {
    final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
            MotionEvent.ACTION_POINTER_INDEX_SHIFT;//首先对高位进行与操作。然后右移8位,获取其高位代表index的值
    final int pointerId = ev.getPointerId(pointerIndex);//取出该点的id
    if (pointerId == mActivePointerId) {//假设这个id相应的就是第一个按下的点
        //理论上pointerIndex应该是0,所以用第二个按下的点,即1index的点取代
        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
        mLastMotionY = ev.getY(newPointerIndex);//取出新点的y坐标
        mActivePointerId = ev.getPointerId(newPointerIndex);//记录新点的id
        if (mVelocityTracker != null) {//清空之前存入的MotionEvent,也就是说最后的速度仅仅计算该点产生的
            mVelocityTracker.clear();
        }
    }
}

通过以上分析,我们得出以下知识:

  • 在down事件的时候先推断触摸是否处于边缘,假设是,则不处理
  • 在down事件中推断落点是否在子View中。假设不在,不处理
  • 在down事件中推断是否仍在滑动,假设是,先停止
  • 记录第一个按下点的索引值
  • 每次事件都记录住当前的y值
  • 在move事件中通过记录的索引值找到相应的点,获取y坐标
  • 与上一次y坐标进行比对。scrollBy两次的差值
  • 在up事件的时候计算最后一秒钟的速度。而且有最大速度进行限制,当计算的速度大于系统默认的最小速度时,仅仅想fling
  • up和cancel事件还原变量为默认值
  • 假设为多点离开。进行多点离开的处理
  • 该处理方式时:假设离开的是第一个按下的点,那么由第二个按下的点取代其进行y值偏移计算的基点,并清空速度计算的帮助类。又一次记录MotionEvnet

8. Layout和Scroll的差别

  • Layout中设置的是自身在父View中的显示区域
  • Scroll是调整自己的显示区域
  • 当父View滚动或者layout变化后,自身在屏幕上的位置会发生变化。
    当自身Scroll滚动后。在屏幕上的显示位置是不变的,变的仅仅是自身的显示内容。
  • Scroll滚动不会影响Layout。仅仅是在draw的时候影响画布偏移和触摸时的坐标计算。

原文地址:https://www.cnblogs.com/llguanli/p/7358196.html