Android事件分发机制

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

触摸事件的开始

      触摸事件,来自触摸屏。从触摸屏硬件产生事件信号到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中的这个方法。

 事件分发的核心

 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. 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);
        }

   3.找到了target

        存在有两种情况:

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

    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不为空,事件就是正常分发(什么是正常分发?后面会讲)。

 

         5. 事件的正常分发

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

 

 if (isUpOrCancel) {
            mMotionTarget = null;
        }

......
 return target.dispatchTouchEvent(ev);


          6. 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)

 

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

        

   

        

 

 

 

"sgmediation.zip" 是一个包含 UCLA(加利福尼亚大学洛杉矶分校)开发的 sgmediation 插件的压缩包。该插件专为统计分析软件 Stata 设计,用于进行中介效应分析。在社会科学、心理学、市场营销等领域,中介效应分析是一种关键的统计方法,它帮助研究人员探究变量之间的因果关系,尤其是中间变量如何影响因变量与自变量之间的关系。Stata 是一款广泛使用的统计分析软件,具备众多命令和用户编写的程序来拓展其功能,sgmediation 插件便是其中之一。它能让用户在 Stata 中轻松开展中介效应分析,无需编写复杂代码。 下载并解压 "sgmediation.zip" 后,需将解压得到的 "sgmediation" 文件移至 Stata 的 ado 目录结构中。ado(ado 目录并非“adolescent data organization”缩写,而是 Stata 的自定义命令存放目录)目录是 Stata 存放自定义命令的地方,应将文件放置于 "ado\base\s" 子目录下。这样,Stata 启动时会自动加载该目录下的所有 ado 文件,使 "sgmediation" 命令在 Stata 命令行中可用。 使用 sgmediation 插件的步骤如下:1. 安装插件:将解压后的 "sgmediation" 文件放入 Stata 的 ado 目录。如果 Stata 安装路径是 C:\Program Files\Stata\ado\base,则需将文件复制到 C:\Program Files\Stata\ado\base\s。2. 启动 Stata:打开 Stata,确保软件已更新至最新版本,以便识别新添加的 ado 文件。3. 加载插件:启动 Stata 后,在命令行输入 ado update sgmediation,以确保插件已加载并更新至最新版本。4
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值