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

Android事件分发机制

时间:2016-07-06 15:10:37      阅读:231      评论:0      收藏:0      [点我收藏+]

标签:

        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)

 

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

        

   

        



Android事件分发机制

标签:

原文地址:http://blog.csdn.net/meijian531161724/article/details/51833154

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