对于Android事件分发机制的总结

事件分发机制是Android中的一个重点,也是一个难点。最早接触这一块的时候,也被各种事件搞得晕头转向。网上找了一些资料,也看了一些书籍,终于多少有点心得。现附上找到的对于Android事件分发的资料和个人的一些总结。

1、MotionEvent对象

MotionEvent对象包含了触摸事件的时间、位置、面积、压力、以及本次事件的Dwon发生的时间。
MotionEvent常用的Action分为5种:Down 、Up、Move、Cancel、OutSide。
MotionEvent中我们常用的方法就是获取点击的坐标,因为这是与我们操作息息相关的。获取坐标有两种方式:
(1)getX和getY用于获取以该View左上角为坐标原点的坐标。
(2)getRowX和getRowY用于获取以屏幕左上角为坐标原点的坐标。
2、MotionEvent对象的产生
系统有一个线程在循环收集屏幕硬件信息,当用户触摸屏幕时,该线程会把从硬件设备收集到的信息封装成一个MotionEvent对象,然后把该对象存放到一个消息队列中。
系统的另一个线程循环的读取消息队列中的MotionEvent,然后交给WMS去派发,WMS把该事件派发给当前处于活动的Activity,即处于活动栈最顶端的Activity。
这就是一个先进先出的消费者和生产者的模板,一个线程不停的创建MotionEvent对象放入队列中,另一个线程不断的从队列中取出MotionEvent对象进行分发。
当用户的手指从接触屏幕到离开屏幕,是一个完整的触摸事件,在该事件中,系统会不断收集事件信息封装成MotionEvent对象。收集的间隔时间取决于硬件设备,例如屏幕的灵敏度以及cpu的计算能力。目前的手机一般在20毫秒左右。
3、事件分发的入口

之前我们知道触摸事件是被包装成MotionEvent进行传递的,而该对象是继承了Parcelable接口,正因为如此,才可以从系统中传递到我们的应用中。系统通过跨进程通知ViewRoot,ViewRoot会调用DecorView的dispatchTouchEvent下发。
这里有一个和其他事件传递不同的地方,DecorView会优先传递给Activity,而不是它的子View。而Activity如果不处理又会回传给DecorView,DecorView才会再将事件传给子View。
dispatchTouchEvent就是触摸事件传递的对外接口,无论是DecorView传给Activity,还是ViewGroup传递给子View,都是直接调用对方的dispatchTouchEvent方法,并传递MotionEvent参数。
4、ViewGroup的dispatchTouchEvent(事件分发)以及onInterceptTouchEvent(事件拦截)

4.1、ViewGroup的dispatchTouchEvent方法(事件分发)

dispatchTouchEvent方法返回了一个boolean的值。而返回的这个boolean值所代表的意思是:True if the event was handled by the view, false otherwise

接下来附上ViewGroup的dispatchTouchEvent方法的源码以及注释(不同的Android版本源码可能略有不同,但思路都是相通的):

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;//是否设置了禁止拦截。true为设置了,false为没设置。
    if (action == MotionEvent.ACTION_DOWN) {//如果事件是按下事件
        if (mMotionTarget != null) {    //判断接受事件的target是否为空-----mMotionTarger就是用来记录ViewGroup的子View的。注意,他只有在down的时候,才被强制变成null
            //不为空肯定是不正常的,因为一个事件是由DOWN开始的,而DOWN还没有被消费,所以目标也不是不可能被确定,
            //造成这个的原因可能是在上一次up事件或者cancel事件的时候,没有把目标赋值为空
            mMotionTarget = null;    //在此处挽救
        }
        //不允许拦截,或者onInterceptTouchEvent返回false,也就是不拦截。注意,这个判断都是在DOWN事件中判断-----本ViewGroup不拦截事件,那么交给孩子来处理,但我不知道
        //到底要交给哪一个孩子来处理,所以逆序遍历孩子
        //-----注意,这个判断都是在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的矩阵-----你孩子能响应,必须得是可见的(Visible)
                    child.getHitRect(frame);//这里用了C代码。其实就是把内容传给了frame这个对象。frame代表View的视图矩阵
                    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-----(调用孩子的事件分发方法,如果该方法返回true,本事件分发方法也return)
                            //如果消费了,目标就确定了,以便接下来的事件都传递给该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; 
    }
    //注意,在这里才给target赋值的。只要前面child.dispatchTouchEvent(ev)返回了false,这里的mMotionTarget就是null。target也是null。
    final View target = mMotionTarget;
    if (target == null) {
    //判断该值是否为空,如果为空,则没找到能响应的子View(包含两种情况,1是没有点击到子View上,2是没有dispatchTouchEvent方法返回true的子View)。那么直接调用super的dispatchTouchEvent,也就是View的dispatchTouchEvent(就是走View的事件分发方法,在后面有介绍)
        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);
    //有target,不拦截,而且不是down事件。就走target的dispatchTouchEvent方法。
    return target.dispatchTouchEvent(ev);
    //也就是说,如果是DOWN事件,拦截了,那么每次一次MOVE或者UP都不会再判断是否拦截,直接调用super的dispatchTouchEvent
    //如果DOWN没拦截,就是有其他View处理了DOWN事件,那么接下来的MOVE或者UP事件拦截了,那么给目标View发送一个cancel事件,告诉它touch被取消了,并且自身也不会处理,直接返回true
    //这是为了不违背一个Touch事件只能由一个View处理的原则。
}
从以上源代码中我们其实可以看出 ViewGroup的 dispatchTouchEvent方法 所做的事情:

down事件中判断本ViewGroup是否拦截事件。只要本ViewGroup不拦截事件,就在它的down事件中遍历子view,如果正好也是点到了子View,就调用子View的dispatchTouchEvent方法(事件分发),把MotionEvent传递给子View。如果没找到子View或者子View没有消费掉该事件(即子View的dispatchTouchEvent方法返回了false),直接调用super.dispatchTouchEvent。
如果拦截了事件,直接不遍历子View(你都拦截了还遍历什么),直接调用super.dispatchTouchEvent。

只要父亲记录了儿子,那么down后面的事件就都要判断本父亲有没有拦截。

那么什么时候父View会记录一个子View呢?
答:父亲没有拦截,且点击了子View,且子View的dispatchTouchEvent方法返回了true(即子View可以消费该事件)。这时才记录子View。

4.2、 ViewGroup的onInterceptTouchEvent方法(事件拦截)

该方法默认返回false,即不拦截。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}
4.3、 总结ViewGroup的 dispatchTouchEvent方法

(1)Down事件:
通过onInterceptTouchEvent方法判断是否要拦截事件,默认fasle
根据scroll换算后的坐标找出所接受的子View。有动画的子View将   不接受触摸事件。
找到能接受的子View后把event中的坐标转换成子View的坐标
调用子View的dispatchTouchEvent把事件传递给子View。(注意,这个子View可能只是个View,也可能是ViewGroup)
如果子View消费了该事件,则把target记录为子View,方便后面的Move和Up事件的传递。
如果子View没有消费,则继续寻找下一个子View。
如果没找到,或者找到的子View都不消费,就会调用父类View的dispatchTouchEvent的逻辑(处理方式参见后面的View的dispatchTouchEvent方法),也就是判断是否有触摸监听,有的话交给监听的onTouch处理,没有的话交给自己的onTouchEvent处理。
注意:ViewGroup是View的子类!!!ViewGroup覆写了View的dispatchTouchEvent方法!!!
(2)Move和Up事件:
判断事件是否被取消或者事件是否要拦截住,是的话,给Down事件找到的target发送一个取消事件。
如果不取消,也不拦截,并且Down已经找到了target,则直接交给target处理,不再遍历子View寻找合适的View了。
这种处理事件是正确的,我们用手机经常可以体会到,当我手指按在一个拖动条上之后,在拖动的时候手指就算移出了拖动条,依然会把事件分发给拖动条控制它的拖动。
5、View的dispatchTouchEvent(事件分发)(注意,View没有onInterceptTouchEvent事件拦截方法

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中的处理相当简单明了,因为不涉及到子View,所以只在自身内部进行分发。
首先判断是否设置了触摸监听,并且可以响应事件,就交由监听的onTouch处理。
如果上述条件不成立,或者监听的onTouch事件没有消费掉该事件,则交由onTouchEvent进行处理,并把返回结果交给上层。
先交由监听事件处理,false,再交由onTouchEvent处理。onTouchListener的接口的优先级是要高于onTouchEvent。
6、View的onTouchEvent(事件处理)
从View的dispatchTouchEvent可以看出,事件最终的处理无非是交给TouchListener的onTouch方法或者是交由onTouchEvent处理,由于onTouch默认是空实现,由程序员来编写逻辑,那么我们来看看onTouchEvent事件。View只能响应click和longclick,不具备滑动等特性。
Down时,设置按压状态,发送一个延迟500毫秒的长按事件。
Move时,判断是否移出了View,移出后移除按压状态,长按事件。
Up时,取消按压,并判断它是否可以通过触摸获取焦点,是的话设置焦点,判断长按事件是否执行了,如果还没执行,就删除,并执行点击事件。
View的onTouchEvent方法源码:
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;
    //先判断标示位是否为disable,也就是无法处理事件。如果无法处理事件,走这个if里面的。
    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;
        }
    }
    //走到这里说明当前的View是enable的。接下来判断该View是否是clickable或long_clickable的。是,就走if里的。不是,就返回false。
    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,可以保障在这个代码块执行完毕之后才执行
                        }
                    } (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();
                    if
                    }
                    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是一个常量,不同的手机值不一样,dpi越高,值大,一般数值为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只具备消费点击事件
}
7、onTouchListener的onTouch(监听中的事件处理方法)
如果onTouchListener中的onTouch返回了false。参见View的dispatchTouchEvent方法(3个条件),不会调用onTouch方法了。而是会调用onTouchEvent方法。
如果返回的是true,则继续走onTouch方法。
注意,监听中的down事件,必定会走一次其中的onTouch方法。这个方法不走一次,你怎么知道他返回true还是false。而down事件是所有事件的起点。因此,有onTouchListener,down时必定走一次里面的onTouch方法。

猜你喜欢

转载自blog.csdn.net/ArimaKisho/article/details/51078329
今日推荐