Android|事件分发

533 阅读6分钟

导语

事件分发是一个老生常谈的问题,理解事件分发机制,对于解决日常开发工作中遇到的滑动冲突问题,有着至关重要的作本,这里以阅读源码的方式,对事件分发机制做一个详细的讲解,希望阅读本文后,能对事件分发机制有个深入的理解,解决日常遇到的滑动冲突问题

源码阅读

从Activity开始


//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

//PhoneWindow.java
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

//DecorView.java   super实际是ViewGroup的dispatchTouchEvent方法
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}


下面就进入了View Group的流程


@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    //字段含义,ev事件是否已经被处理(被消费)
    boolean handled = false;
    //简化代码,Down事件来临时,置空mFirstTouchTarget
    if(actionMasked == MotionEvent.ACTION_DOWN){
        mFirstTouchTarget = null;
    }
    // Check for interception.
    //第一步,确定是否需要拦截事件(检查条件:DOWN事件或者mFirstTouchTarget状态)
    final boolean intercepted;
    //mFirstTouchTarget字段含义:有子view能处理事件,
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            //viewGroup 调用自身的onInterceptTouchEvent,决定是否对事件进行拦截,自己处理该事件
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }

    //第二步,不需要拦截场景处理,分发DOWN事件
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    if (!intercepted) {
        //仅处理DOWN事件(由第一步得知,不拦截事件时,ViewGroupu要问问自己的子View能否处理事件)
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            if (newTouchTarget == null && childrenCount != 0) {
                //遍历所有子view
                for (int i = childrenCount - 1; i >= 0; i--) {
                    //内部调用child.dispatchTouchEvent,询问子view是否需要消费事件
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        //若消费事件,则 mFirstTouchTarget从此有了值
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        //局部变量,记录子View已经对事件进行过分发处理
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }

                }
            }

        }
    }

    //第三步真正事件分发,将事件分发给对应View
    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
        //从第二步得知,没有子View处理事件,只能ViewGroup自己处理了,内部会调用自己的onTouchEvent 来决定是否消费事件
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
    } else {
        //从第二步得知,有子view能处理,则交给子view
        // Dispatch to touch targets, excluding the new touch target if we already
        // dispatched to it.  Cancel touch targets if necessary.
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            //子view已经分发过事件(因为第二步中down事件调用过dispatchTransformedTouchEvent进行分发)
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                //子View未分发过事件
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                    handled = true;
                }

            }
            predecessor = target;
            target = next;
        }
    }

    //第四步,将事件处理结果告知上层
    return handled;
}

从源码中可以得知,整体上,事件的分发是由Activity自顶向下逐级传播的,通过Activity的dispatchTouchEvent最终访问到DecorView的dispatchTouchEvent方法,

dispatchTouchEvent是事件分发的入口,这里要重点讲解,从上述代码中标注的内容来看,我们可以得知,任何一个事件,都是从Down事件开始的,从dispatchTouchEvent的源码中我们也可以了解到,有很多针对DOWN事件单独处理的逻辑,因此之类解读源码,也分量两个流程讲解,DOWN事件的流程与其他事件的流程

DOWN事件的流程
  • 第一步:确定是否需要拦截事件 --理解:DOWN事件到来时,确认是否需要自己拦截处理,前置清空mFirstTouchTarget字段,调用自身的onInterceptTouchEvent来确认是否需要自己主动拦截处理该事件
  • 第二步:DOWN事件分发 --若自身不需要拦截DOWN事件,则将事件分发给子VIEW,调用child.dispatchTouchEvent递归询问子view是否能处理该事件,若子view能处理,则记录mFirstTouchTarget,和alreadyDispatchedToNewTouchTarget --若自身拦截,则直接跳过第二步
  • 第三步:真正事件分发,将事件分发给对应View --根据第二步的结果,已经得知事件是自己处理还是子View处理,自身处理最终会调用自身的onTouchEvent来处理事件,若是子VIEw处理,因DOWN事件的特殊性,在第二步已经调用了child.dispatchTouchEvent,第三步则根据第二步记录的alreadyDispatchedToNewTouchTarget 字段,直接返回已经处理的结果
  • 第四步:无
其他事件的分发流程
  • 第一步:确定是否需要拦截事件 --理解:根据mFirstTouchTarget字段,判断,若存在能处理事件的子VIEW,调用自身的onInterceptTouchEvent来确认是否需要自己主动拦截处理该事件
  • 第二步:无
  • 第三步:真正事件分发,将事件分发给对应View --根据DOWN事件时记录的结果,已经得知事件是自己处理还是子View处理,自身处理最终会调用自身的onTouchEvent来处理事件,若是子VIEw处理,则调用了child.dispatchTouchEvent
  • 第四步:返回第三步得到的结果给上层。

需要重点理解几个字段 mFirstTouchTarget:在Down事件时记录,子view能否处理事件,能的话后面的新事件交个子view处理 alreadyDispatchedToNewTouchTarget:为了防止多次触发子view的dispatchTouchEvent,用于记录的一个成员变量

总结

在理解上述流程时,是不是发现了我们在处理滑动冲突时,经常会遇到的几个方法 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 结合源码阅读,我们可以对这几个函数有更深入的理解

/**
 * 事件分发器:在ViewGroup中,dispatchTouchEvent 负责确定事件是自己处理还是子view处理,通知通过返回值告知父view,自己和子view能否处理该事件
 * true:告知父view,事件已经被自己或者子view处理掉
 * false :告知父view,事件自己和自己的子view都不能处理,交由父view处理
 * 分发过程
 * 1、通过事件拦截器,优先确认是否拦截事件让自己处理,若确认拦截则跳过步骤2
 * 2、确认不拦截,则调用子view的事件事件分发器 ,让自view确认自己能否处理
 * 3、根据1、2的结果,若子view确认自己能处理,则交由子view处理,不会调用自己的事件处理器,若子view不能处理,则调用自身的事件处理器
 * 4、将处理结果告知父view
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.e("whqq", "父类dispatchTouchEvent" );
    boolean result = super.dispatchTouchEvent(ev);
    Log.e("whqq", "父类dispatchTouchEvent结果" + result);
    return result;
}
/**
 * 事件拦截器:仅用于拦截触摸事件(ACTION_DOWN)
 * true:拦截当前触摸事件,传递给自己的 onTouchEvent 进行处理
 * false:不拦截当前触摸事件,
 */
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
    Log.e("whqq", "父类onInterceptTouchEvent" );
    boolean result = super.onInterceptTouchEvent(e);
    Log.e("whqq", "父类onInterceptTouchEvent结果" + result);
    return result;
}

/**
 * 事件处理器:处理到来的事件
 * true:事件被处理掉了,告诉父view需要处理了,父view也不会在调用onTouchEvent
 * false: 事件我处理不了,父view会调用自己的onTouchEvent
 */
@Override
public boolean onTouchEvent(MotionEvent e) {
    Log.e("whqq", "父类onTouchEvent" );
    boolean result = super.onTouchEvent(e);
    Log.e("whqq", "父类onTouchEvent结果" + result);
    return result;
}

思考

  • 在解决滑动冲突问题时,最应该复写的是onTouchEvent方法,去处理滑动事件,
  • 若复写了onInterceptTouchEvent方法,也要注意,既然选择拦截了,就代表要么你已经确认要处理该事件在onTouchEvent中返回true,要么代表问题是因为事件交给子view处理引入了问题,才复写onInterceptTouchEvent事件传递给子view,不然都是错误复写
  • 若复写了dispatchTouchEvent方法,记的调用super