Android的事件分发机制,对于事件的分发的了解是非常重要的;如果你不清楚具体的原理,那么你将会很迷茫,遇到问题时,无从下手。这里,我将个人对Android事件分发机制的理解,描述出来,希望能对大多数伙伴的有所裨益。

1.触摸事件的开始

触摸事件,来自触摸屏。从触摸屏硬件产生事件信号到Activity开始接收这个事件,就不做分析了,因为具体的我也不清楚。因此,这里主要分析Activity中,事件的分发过程。

首先由Activity进行分发,具体的分发方法如下:

  public boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            onUserInteraction();        }        if (getWindow().superDispatchTouchEvent(ev)) {            return true;        }        return onTouchEvent(ev);    }
直接从getWindow().superDispatchTouchEvent(ev)开始,如果这个方法返回true,那么这个方法结束,不会调用return onTouchEvent(ev),这是什么意思呢?这个意思就是如果事件被window消费了,Activity就不再对事件进行处理。如果事件并没有被window消费掉,那么事件能被onTouchEvent()方法处理,这个具体的处理方式,我们可以在Activity中进行重写。很多人不明白什么叫做事件被消费掉,所谓事件被消费掉,其实是事件得到了处理,这个事件不再进行传递,不会再传递到其他控件。

接下来我们分析getWindow().superDispatchTouchEvent(ev),这个方法,很多人不知道具体的实现在哪里,这个是和Activity的启动过程有关系,在Activity的创建过程中,会通过PhoneWindow初始化window,因此这个方法,其实是PhoneWindow的superDispatchTouchEvent

   @Override    public boolean superDispatchTouchEvent(MotionEvent event) {        return mDecor.superDispatchTouchEvent(event);    }
这里,我们能看到其实最终调用的是DecorView的superDispatchTouchEvent。由于DecorView继承LinearLayout,最后,其实还是ViewGroup的dispatchTouchEvent方法。

总结上面所说的,Activity中,对事件的分发,主要是通过ViewGroup的dipatchTouchEvent方法来执行的。接下来我们着重分析ViewGroup中的这个方法。

2.事件分发的核心

2.1.ACTION_DOWN的向下传播,找到事件触发坐标所在的最里面的一个控件。

首先,我们需要知道一个事件序列,ACTION_DOWN—>ACTION_MOVE—>ACTION_UP,这只是其中一个代表,一个触摸事件总是从ACTION_DOWN开始的,然后才会触发后面的事件。ACTION_DOWN这个事件,它承担了查找能够处理事件的目标控件,这个查找的过程,具体看代码分析吧。

 @Override    public boolean dispatchTouchEvent(MotionEvent ev) {//获取事件的动作        final int action = ev.getAction();//事件的x,y在当前视图的坐标        final float xf = ev.getX();        final float yf = ev.getY();//mScrollX和mScrollY是该容器视图的画布的滑动位移,        final float scrolledXFloat = xf + mScrollX;        final float scrolledYFloat = yf + mScrollY;        final Rect frame = mTempRect;        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//如果是ACTION_DOWN事件        if (action == MotionEvent.ACTION_DOWN) {            if (mMotionTarget != null) {                // this is weird, we got a pen down, but we thought it was                // already down!                // XXX: We should probably send an ACTION_UP to the current                // target.                mMotionTarget = null;            }            // If we're disallowing intercept or if we're allowing and we didn't            // intercept//默认是false,表示可以拦截,可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)设置为true,表示不拦截,那么onInterceptTouchEvent就失效了            if (disallowIntercept || !onInterceptTouchEvent(ev)) {                // reset this event's action (just to protect ourselves)                ev.setAction(MotionEvent.ACTION_DOWN);                // We know we want to dispatch the event down, find a child                // who can handle it, start with the front-most child.                final int scrolledXInt = (int) scrolledXFloat;                final int scrolledYInt = (int) scrolledYFloat;                final View[] children = mChildren;                final int count = mChildrenCount;//开始遍历子view                for (int i = count - 1; i >= 0; i--) {                    final View child = children[i];//如果子View是可见或者是有动画的,那么获取这个视图的可点击范围                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE                            || child.getAnimation() != null) {//表示的child的原始位置,也就是scroll滑动前的位置,因此上面需要加上mScrollX                        child.getHitRect(frame);//如果该孩子位置的包含了所触控的点                        if (frame.contains(scrolledXInt, scrolledYInt)) {                            // offset the event to the view's coordinate system                            final float xc = scrolledXFloat - child.mLeft;                            final float yc = scrolledYFloat - child.mTop;                            ev.setLocation(xc, yc);                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;//该孩子是否消费的了事件,如果该孩子消费了,则返回true,这里是递归调用                            if (child.dispatchTouchEvent(ev))  {                                // Event handled, we have a target now.                                mMotionTarget = child;                                return true;                            }                            // The event didn't get handled, try the next view.                            // Don't reset the event's location, it's not                            // necessary here.                        }                    }                }            }        }
上面是ViewGroup中dispatchTouchEvent的第一部分代码,也是ACTION_DOWN事件的主要处理过程,代码中具体注释了重要的过程。要注意的只有两点:

1.disallowIntercept,这个标志位默认是false,表示可以拦截,主要看onInterceptTouchEvent来控制。如果表示true,表示不能拦截,onInterceptTouchEvent就算拦截了,也是无效的。

2.最后的几句代码中,又调用了child.dispatchTouchEvent,说明这类似一个递归调用。我们可以想象,ViewGroup1的dispatchTouchEvent中调用了ViewGroup2的dispatchTouchEvent,最后调用了view.dispatchTouchEvent方法(最后一个控件很有可能不是ViewGroup)。一直把这个事件传递到最后一个view,如果最后这个view的dispatchTouchEvent返回true。但这一切的前提是,事件的触发坐标落在控件上。


2.2.target为空,ACTION_DOWN事件外层传递,父控件获得处理事件的机会。

上一个过程中,ACTION_DOWN并没有找到能够消费它的控件,因此,遍历控件完成后,进入最后一个控件的父控件的dispatchTouchEvent的这个过程。事件没有消费,交给最后一个控件的父控件的super.dispatchTouchEvent来处理。这个处理,其实是调用的View里面的dispatchTouchEvent,这个和ViewGroup中的是不一样的,View中的这个方法是具体的消费过程,并不分发事件。如果当前父控件(也就是倒数第二个控件)也没有消费这个事件,super.dispatchTouchEvent返回false;事件又交给了当前控件的父控件(倒数第三个控件)同样进行处理。 这里有人不明白,为什么是交给父控件处理;原因是第一步里面,我们是层层调用,父控件调用子控件的dispatchTouchEvent方法。这里是target为空,也就是说第一步的层层调用,没有消费掉事件,还没有返回,因此这里面对这个情况进行处理,开始层层往上返回;父控件得到处理事件的机会,如果父控件没有消费掉事件,就继续往上返回;如果父控件消费掉了事件,那么它的父控件返回true,target为当前这个控件。

/如果target为空,ACION_DOWN事件未消费,ACTION_DOWN事件又开始重新往上分发        final View target = mMotionTarget;        if (target == null) {            // We don't have a target, this means we're handling the            // event as a regular view.            ev.setLocation(xf, yf);            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {                ev.setAction(MotionEvent.ACTION_CANCEL);                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;            }//子View没有消费事件,交给当前的ViewGroup来处理,其实super.dispatchTouchEvent(ev);调用的View的dispatchTouchEvent            return super.dispatchTouchEvent(ev);        }
2.3.找到了target

存在有两种情况:

1.事件没有被拦截,ACTION_DOWN事件顺利找到了目标控件,并且该控件能够对事件进行处理,消费掉。

2.事件中途被拦截,事件交给了中途的一个ViewGroup处理,并且有一个ViewGroup能够消费事件。 比如:事件中途被ViewGroupIntercept(控件别名)被拦截,那么它不会执行2.1的代码,target==null,ACTION_DOWN事件,进入到代码2.2,事件开始往上返回,但是ViewGroupIntercept的父控件并没有拦截,事件执行在2.1的代码,这时候如果ViewGroupIntercept消费掉事件,返回true,那么它的父控件的target就指向这个控件,这样又重新回到了正常的分发流程。

2.4.ACTION_MOVE,ACTION_UP等非ACTION_DOWN事件被拦截。

ACTION_MOVE,ACTION_UP事件被拦截,肯定是事件已经找到了可消费的控件。但是一个事件序列中,除ACTION_DOWN的事件,被动态拦截了,这里的事件将做如下处理:

if (!disallowIntercept && onInterceptTouchEvent(ev)) {            final float xc = scrolledXFloat - (float) target.mLeft;            final float yc = scrolledYFloat - (float) target.mTop;            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;            ev.setAction(MotionEvent.ACTION_CANCEL);            ev.setLocation(xc, yc);//给目标传递个cancel事件            if (!target.dispatchTouchEvent(ev)) {                // target didn't handle ACTION_CANCEL. not much we can do                // but they should have.            }            // clear the target//当前控件的target清空,下一次的事件判断,进入到target为空的情况,也就是由当前控件自己处理。            mMotionTarget = null;            // Don't dispatch this event to our own view, because we already            // saw it when intercepting; we just want to give the following            // event to the normal onTouchEvent().            return true;        }
事件被拦截后,先给目标控件传递一个cancel事件,这一个事件序列中目标控件的事件结束。接下来,当前控件的分发方法返回true,表示当前控件可以消费事件,那么他的父控件的分发方法进入到了流程2.3,也就是target不为空,target不为空,事件就是正常分发(什么是正常分发?后面会讲)。

2.5.事件的正常分发

事件由父控件的target传递到子控件的target,这就是事件的正常分发。如果事件是ACTION_UP或者是ACTION_CANCEL,将会清空target,下一次的触摸事件,又是如此重新开始。

 if (isUpOrCancel) {            mMotionTarget = null;        }...... return target.dispatchTouchEvent(ev);

3.View的dispatchTouchEvent方法

上面多次提到View.dispatchTouchEvent,现在我们来看一下:

 public boolean dispatchTouchEvent(MotionEvent event) {        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&                mOnTouchListener.onTouch(this, event)) {            return true;        }        return onTouchEvent(event);    }
先会判断控件是否设置了onTouchListener,如果设置了,并且控件是enable,那么事件酱油OnTouchListener的onTouch方法处理。 否则事件交给默认的处理方法onTouchEvent来处理。

看看onTouchEvent的处理方法,onTouchEvent的处理,主要逻辑是这样的,先判断控件是否是clickable,如果不可以,直接返回false,事件没有消费掉。如果clickable为true或者longClckable为true,事件会被消费掉,具体就会回调onClick方法或者是onLongClick方法。



总结:

1.正常不拦截事件,由action_down查找对应能消费的target,如果存在target能消费事件,则最后事件由该target全部处理。
2.不拦截事件,没有target处理事件,则事件逐步往上由onTouchEvent方法处理,但是控件非clickable,则不能处理事件。如果存在控件是clickable的,那么事件会被消费掉。
3.事件分发的过程中,如果事件被拦截,则下一个事件交给拦截事件的onTouchEvent控件处理。
4.requestDisallowInterceptTouchEvent可以用来控制事件的分发。

Android滑动冲突
多个可以滑动控件之间的嵌套很容易引起滑动冲突,解决的方法分为两种:
1.从外部拦截机制考虑
外部控件重写onInterceptTouchEvent处理,通过计算dx和xy的进行处理,在onMove事件中动态控制
2.内部控件调用
requestDisallowInterceptTouchEvent来控制,原理其实是一样的。requestDisallowInterceptTouchEvent能够控制事件能否往下传递,前面事件分发机制已经分析了。(往下的意思:View是树形结构,最顶层是最底部的View,最下面是子View)

到这里,整个的事件分发机制就基本结束了,希望能对大家有所帮助。若有什么分析不当的地方,望大家指出。



更多相关文章

  1. 快速开发框架Afinal的使用(数据库操作,HTTP请求,网络图片加载,控件绑
  2. Android(安卓)onTouchEvent, onClick及onLongClick的调用机制
  3. [置顶] Android之高仿手机QQ聊天
  4. Android自定义控件一简介
  5. 每天学习一个Android中的常用框架——12.Handler
  6. Android界面布局的几种常用方式
  7. Android之View篇2————View的事件分发
  8. Android之高仿手机QQ聊天
  9. 【Android(安卓)周末回眸】2011.07.25-2011.07.31

随机推荐

  1. Android(安卓)HAL模块实现
  2. android多线程断点下载——网络编
  3. Android中使用加速度传感器
  4. mono for android 第四课--提示框
  5. 视频教程-TCP/IP/UDP Socket通讯开发实战
  6. Android使用setCustomTitle()方法自定义
  7. android 加载网络图片 SkImageDecoder::F
  8. 一起来学Android(安卓)Studio:(三)导入项目
  9. 在Eclipse中导入新浪微博SDK
  10. Android(安卓)Content Provider的应用