Android中事件的分发与处理流程

来源:互联网 发布:微信无法连接网络 编辑:程序博客网 时间:2024/06/06 02:35
基本原则:分发由上往下,消费由下往上
由上往下分别为 Activity -> PhoneWindow -> DecorView (contentview的父容器) -> ContentView(一般为ViewGroup) -> View
事件的分发:
触摸事件是被包装成MotionEvent进行传递的,而该对象是继承了Parcelable接口,正因为如此,才可以从系统中传递到我们的应用中。系统通过AIDL跨进程调用了应用的Activity的dispatchTouchEvent方法,并把MotionEvent对象作为参数传递过来。 
dispatchTouchEvent就是触摸事件传递的对外接口,无论是系统传给Activity,还是Activity传递给ViewGroup,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,并把该方法的返回值作为结果返回给上层。 
接下来我们来研究ViewGroup的dispatchTouchEvent,这是稍微复杂的分发逻辑,上面的根View也就是一个ViewGroup。
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处理的原则。}再来看看View中的dispatchTouchEvent是如何分发事件的: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);}

再来看看View中的dispatchTouchEvent是如何分发事件的:
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);}

View中的处理相当简单明了,因为不涉及到子View,所以只在自身内部进行分发。
首先判断是否设置了触摸监听,并且可以响应事件,就交由监听的onTouch处理。
如果上述条件不成立,或者监听的onTouch事件没有消费掉该事件,则交由onTouchEvent进行处理,并把返回结果交给上层。
 
事件的消费:
    从View的dispatchTouchEvent可以看出,事件最终的处理无非是交给TouchListener的onTouch方法或者是交由onTouchEvent处理,由于onTouch默认是空实现,由程序员来编写逻辑,那么我们来看看onTouchEvent事件。 
首先我们来看一个比较简单的onTouchEvent的处理,那就是View,我们知道,View只能响应click和longclick,不具备滑动等特性。
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,可以保障在这个代码块执行完毕之后才执行                        }                    } (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只具备消费点击事件

从上面的代码我们总结一下View对触摸事件的处理: 
1、是否为diabale,如果是,直接根据是否设置了click和longclick来返回。 
2、是否设置了触摸代理对象,如果有,把事件传递给触摸代理对象,交由其处理,如果消费了,直接返回 
3、是否为click或者longclick的,如果是,返回true,不是返回false。 
而View对click和longclick的处理如下: 
Down: 
1.判断是否可以触摸上下文菜单。 
2.是否在可以滑动的容器中,如果是先设置临时按压,再发送一个延迟消息把临时按压改为按压,并发送一个延迟500毫秒的事件去执行长按代码 
3.如果不在滚动容器中,直接设置按压状态,并发送一个延迟500毫秒的事件去执行长按代码。 
Move: 
1、取触摸点坐标判断是否在View中(额外增加了8像素的范围) 
2、如果在,不用做任何事。 
3、如果不在,取消临时按压到按压回调,取消长按延迟回调,设置为非按压状态 
Up 
1、判断是否为按压或者临时按压状态 
2、如果不是,不做任何处理 
3、如果是先判断其是否可以获取焦点,然后请求焦点。 
4、如果是临时按压状态,设置临时按压状态为按压状态。保证界面被绘制成按压状态,让用户可以看见。 
5、如果长按回调还未触发,取消长按回调,如果不是焦点状态,触发click事件。 
6、如果是临时按压状态,发送一个延迟取消按压状态的,保证按压状态持续一段时间,让用户可见。 
7、如果不是临时按压状态,直接发送消息取消按压状态。发送失败,直接取消按压状态。 
8、取消把临时按压设置按压的回调。** 
从中我们知道View的onTouchEvent主要处理了click和longclick事件,当按下时,向消息机制发送一个延迟500毫秒的长按回调事件,当移动时候判断是否移出了View的范围,超出则取消事件。当离开时,判断长按事件是否触发了,如果没触发且不是焦点,就触发click事件。 
在这里最绕的就是临时按压和按压状态,临时按压是为了处理滑动容器的,让处于滑动容器中,按下时,我们先设置的是临时按压,持续64毫秒,是为了判断接下来的时间内是否发生了move事件,如果发生了,将不会再出发按压状态,这样不会让用户看到listView滚动时,item还处于按压状态。在离开时,我们再次判断是否处于临时按压,如果是在64毫秒内触发了down和up,说明按压状态还没来得急绘制,则强制设置为按压状态,保证用户能看到,并在取消回调的方法上加上64毫秒的延迟 

简单总结:
viewgroup
      down move up
     核心方法:dispatchtouchevent:
                   根据触摸点的坐标和onintercepttouchevent的返回值,遍历所有子view分发事件
              onintercepttouchevent:
                   是否中断向下传递事件
              ontouchevent:
                    是否消费事件
view 
              dispatchtouchevent:
                  根据是否有监听对象和ontouchevent的返回值判读是否消费事件
                  如果有监听对象,则事件交给监听对象处理,重写的ontouchevent方法无效
                  没有则交给ontouchevent方法处理
                  其返回值为是否消费事件
              ontouchevent:
                    是否消费事件
原创粉丝点击