Android事件分发机制

UI界面组成

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
    正如API所讲,Window是一个抽象基类,是WindowManager最顶层的试图,它负责背景、Title之类的标准的UI元素,整个Android系统中PhoneWindow是Window的唯一实现类。

    在Activity类中有一个Window类的引用mWindow,并且可以通过getWindow()分发来获取这个Window对象,在attach()方法里面对mWindow进行初始化,如下:

    final void attach(......) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        ......

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        ......
    }

    mWindow通过PhoneWindow的构造方法创建对象。

    当调用了attach方法后就会调用onCreate方法了,在onCreate方法中我们首先要调用setContentView方法,如下:

    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     */
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
  方法定位到了Window的setContentView,那就看看PhoneWindow类的setContentView的实现。该类聚合了两个View对象,一个是mDecor,该对象是FrameLayout的子类;另一个是mContentParent对象;

    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            ......
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
    这个方法先判断mContentParent是否为null,如果为null的情况下调用installDecor()方法。该方法里面主要做了哪些事情呢?

public class PhoneWindow extends Window implements MenuBuilder.Callback {  
    ......  
  
    // This is the top-level view of the window, containing the window decor.  
    private DecorView mDecor;  
    ......  
  
    // This is the view in which the window contents are placed. It is either  
    // mDecor itself, or a child of mDecor where the contents go.  
    private ViewGroup mContentParent;  
    ......  
  
    private TextView mTitleView;  
    ......  
  
    private CharSequence mTitle = null;  
    ......  
  
}     

    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            ......
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            ......
            } else {
                mTitleView = (TextView) findViewById(R.id.title);
                if (mTitleView != null) {
                    ..........
                }
            }

            ........
        }
    }
    在installDecor()方法中主要完成了mDecor以及mContentParent初始化工作,为Window添加DecorView界面。

    该方法先调用generateDecor()方法对mDecor来进行初始化;然后初始化mContentParent,在generateLayout方法里面有这么一句话可以看出mDecor跟mContentParent的关系:

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
.............
return contentParent;
    而这个findViewById方法是父类Window提供的。

 public View findViewById(int id) {
        return getDecorView().findViewById(id);
    }
    可以看出来的是从mDecor获取一个View对象来赋值给mContentParent,所以mDecor和mContentParent是父View与子View关系。mContentParent有什么用么?继续观察 setContentView方法:

mLayoutInflater.inflate(layoutResID, mContentParent);
    也就是说mContentParent这View是作为你的xml文件的父View,xml通过解析生成的View对象被作为mContentParent的子View通过addView添加到mContentParent上来;而由于mConentParent是mDecor的子View,所以我们说这个页面的父View或者根View就是mDecor。

    最后简单的用图表示一下Activity、PhoneWindow和DecorView的关系,如下:


事件分发概述

1.当用户触摸屏幕时(View或ViewGroup派生的控件),将产生点击事件(Touch事件)

    Touch事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象。

    主要发生的Touch事件有四种,如下:

  •     MotionEvent.ACTION_DOWN:按下View(所有事件的开始);
  •     MotionEvent.ACTION_MOVE:滑动View;
  •     MotionEvent.ACTION_CANCEL:非人为原因结束本次事件;
  •     MotionEvent.ACTION_UP:抬起View(与ACTION_DOWN对应)。
2. 事件分发的本质

    当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理,这个事件传递的过程就是分发过程。因此,Android事件分发机制的本质是要解决:点击事件由哪个对象发出,经过哪些对象,最终到达哪个对象并得到处理。


3. 事件分发过程有哪些方法协作完成?

    事件分发过程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三个方法协助完成。

  • public boolean dispatchTouchEvent(MotionEvent ev)
    用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前的View的onTouchevent和下级的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

  • public boolean onInterceptTouchEvent(MotionEvent ev)
    用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件的序列中,此方法不会被再次调用,返回值表示是否拦截当前事件。

  • public boolean onTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。
上述方法之间的关系可以用下面的伪代码来描述:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

    通过上面的伪代码,我们可以大致的了解点击事件的传递规则:

对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,首先调用dispatchTouchEvent(),如果它的onInterceptTouchEvent()返回true就是拦截当前事件,事件交给ViewGroup处理;如果返回false,表示不拦截当前事件,继续传递给它的子元素,接着子元素的dispatchTouchEvent()就会被调用,如此反复直到事件结束。

当一个View需要处理事件时,如果设置了OnTouchListener ,那么OnTouchListener 中的onTouch方法会被回调。事件如何处理还要看onTouch 的返回值,如果返回false,则当前View的onTouchEvent 会被调用;如果返回true,那么onTouchEvent 将不会被调用。


4. 事件在哪些对象间传递?

    Android的UI界面是由Activity、ViewGroup、View及其派生类组成。View是所有UI组件的基类。ViewGroup是容纳UI组件的容器,即一组View的集合(包含很多子View和子ViewGroup)。

    因此,一个点击事件产生后,传递顺序是:Activity(Window)->ViewGroup->View。

    1)android对事件分发的顺序为:Activity-->PhoneWindow->DecorView->yourView;

    2)android控件对事件处理的优先级:onTouch>onTouchEvent>onClick
5. 结论

  • 正常情况下,一个事件序列只能被一个View拦截并且消耗。一个事件序列中的事件不能分别由两个View同时处理,但是一个View可以将本该自己处理的是将通过onTouchEvent强行传递给其它View处理。
  • 一个View一旦决定拦截,那么一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再次调用。
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchevent返回了false),那么同一事件序列中的其它事件都不会再交给它来处理,并且事件将交由它的父元素去处理。
  • 如果View不消耗除ACTION_DOWN意外的其他事件,那么这个点击事件会消失,此时父元素的onTouchevent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
  • ViewGroup默认不拦截任何事件,View没有onInterceptTouchEvent方法。
  • View的onTouchevent默认都会消耗事件,除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable都为false,而clickable则分情况,一般Button默认为true,TextView默认为false。
  • View的enable属性不影响onTouchEvent的默认返回值。
  • onClick()会发生的前提是View是可以点击的,并且收到了down和up的事件。
  • 事件传递过程由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent 方法可以再子元素中干预父元素的事件分发过程,但是ACTION_DOWN除外。

事件分发机制方法及流程

Activity事件分发机制

源码分析

当一个点击事件发生时,事件最先传到Activity的dispatchTouchEvent()进行事件分发。

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        .........

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();//空方法,忽略
        }
        if (getWindow().superDispatchTouchEvent(ev)) {//把事件交给PhoneWindow处理
            return true;//表明整个事件到此结束,处理完毕
        }
        return onTouchEvent(ev);//说明该事件在所有子view中没有处理,由Activity自己处理
    }
  在dispatchTouchEvent()方法中, 先把事件分发给Window,由于Window类是抽象类,且PhoneWindow是Window类的唯一实现类。所以这里调用 PhoneWindow的superDispatchTouchEvent()方法,如下:

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);//mDecor是DecorView的实例,DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类
    }
    很简单,就是直接把该事件 传递给DecorView,这个view是所有视图的根试图,Activity界面中你能见到的各个View都是DecorView的子View。

    到此为止事件已经分发到View上面,View获取到事件后有两个选择:处理和不处理该事件。如果处理该事件,那事件就不会继续向其子View分发下去;否则就继续分发下去交给子View对该事件做同样的判断,其实就是个递归的过程

    继续向下跟踪代码,上面调用到DecorView类的superDispatchTouchEvent()方法,如下:

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    DecorView继承自FrameLayout,FrameLayout继承自ViewGroup。上面的代码通过super.dispatchTouchEvent(ev)调用了DecorView的父类FrameLayout,FrameLayout类没有重写dispatchTouchEvent()方法,而是由它的 父类ViewGroup实现

所以getWindow.superDispatchTouchEvent(ev)实际上是执行了ViewGroup.dispatchTouchEvent(event)方法。

    我们再回头看下Activity的dispatchTouchEvent()方法:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        .........

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();//空方法,忽略
        }
        if (getWindow().superDispatchTouchEvent(ev)) {//把事件交给PhoneWindow处理
            return true;//表明整个事件到此结束,处理完毕
        }
        return onTouchEvent(ev);//说明该事件在所有子view中没有处理,由Activity自己处理
    }
    由于一般事件都是从ACTION_DOWN开始,基本上都会进入getWindow.superDispatchTouchEvent()方法,返回true。所以, 执行Activity.dispatchTouchEvent()实际上是执行了ViewGroup.dispatchTouchEvent()。这样事件就从Activity传递到了ViewGroup。

执行流程

    当一个点击事件发生时,调用顺序如下:

    1. 事件最先传到Activity的dispatchTouchEvent()方法中进行事件分发;

    2. 调用Window类的实现类PhoneWindow的superDispatchTouchEvent()方法;

    3. 调用DecorView的superDispatchTouchEvent()方法;

  4. 最终调用DecorView父类ViewGroup的dispatchTouchEvent();如果ViewGroup的dispatchTouchEvent()返回true,就不执行Activity的onTouchEvent()方法;如果返回false,就执行Activity自己的onTouchEvent()方法。

疑问

    那么,问题来了:ViewGroup的dispatchTouchEvent()方法什么时候返回true,什么时候返回false呢?

    带着这个问题,我们继续分析ViewGroup的事件分发机制。

ViewGroup事件分发机制

源码分析

public boolean dispatchTouchEvent(MotionEvent ev) {
    //1.用于测试,可直接忽略
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    // If the event targets the accessibility focused view and this is it, start
    // normal event dispatch. Maybe a descendant is what will handle the click.
    //2.[辅助功能] 事件将会第一个派发给开启了accessibility focused的view
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

    boolean handled = false;
//3.表示窗口是否为模糊窗口(FILTER_TOUCHES_WHEN_OBSCURED),如果是窗口则表示不希望处理改事件(如dialog后的窗口)
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        //4.过滤字段的最后8bit,也就是指只关心是ACTION_DOWN、ACTION_UP等事件,而不关心是哪个手指引起的。
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Throw away all previous state when starting a new touch gesture.
            // The framework may have dropped the up or cancel event for the previous gesture
            // due to an app switch, ANR, or some other state change.
//5.初始化相关状态
//清空mFirstTouchTarget链表("接受触摸事件的View"所组成的单链表),并设置mFirstTouchTarget为null。
//清空mGroupFlags的FLAG_DISALLOW_INTERCEPT标记,如果设置了FLAG_DISALLOW_INTERCEPT,ViewGroup对触摸事件进行拦截。
//清空mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVEN标记,作用是将下一个时间变为Cancel
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // Check for interception.
        final boolean intercepted;
        //6.如果为DOWN事件,或者mFirstTouchTarget为null(那么事件直接给到自己),就没必要执行拦截。
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //查看是否设置了,禁止拦截的标记
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //ViewGroup的onInterceptTouchEvent不执行拦截,除非子类重写了该方法(如listview)
                intercepted = onInterceptTouchEvent(ev);//false
                //仅仅是避免action被篡改过。
                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;
        }

        // If intercepted, start normal event dispatch. Also if there is already
        // a view that is handling the gesture, do normal event dispatch.
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        //查看是否被标记了PFLAG_CANCEL_NEXT_UP_EVENT 或者 当前是一个Cancel事件
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // Update list of touch targets for pointer down, if needed.
        //比如我们多个手指放到了屏幕上,是否要将第二个手指的事件分发下去
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {

            // If the event is targeting accessiiblity focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.清除Targets中相应的pointer ids 
                removePointersFromTouchTargets(idBitsToAssign);

                //遍历所有的child,将事件派发下去
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    //以 1)Z轴(5.0系统引入) 2)draw的顺序 进行排序
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    //可以理解为,是否按照draw的顺序(因为,buildOrderedChildList在都没有设置Z的情况下返回null)
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                    //这里两端代码,简单的理解根据不同的排列选项(1、view添加到 2、view的draw顺序 3、viewZ 轴顺序)
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        // If there is a view that has accessibility focus we want it
                        // to get the event first and if not handled we will perform a
                        // normal dispatch. We may do a double iteration but this is
                        // safer given the timeframe.如果存在开启了AccessibilityFocus 的view
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            //如果正是当前的childView开启了AccessibilityFocus,直接将i指向最后一个元素
                            //和 break的区别是,还将执行后面的代码,但是不会再进行循环了
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }

//canViewReceivePointerEvents判断child是否为visiable或者是否有动画
//isTransformedTouchPointInView 判断x, y是否在view的区域内(如果是执行了补间动画 则x,y会通过获取的matrix变换值
                        //换算当相应的区域,这也是为什么补间动画的触发区域不随着动画而改变)
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        //getTouchTarget 查找child是否已经记录在mFirstTouchTarget这个单链表中
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // Child is already receiving touch within its bounds.
                            // Give it the new pointer in addition to the ones it is handling.
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        //简单的理解,dispatchTransformedTouchEvent就是将相应的事件传递下去
                        //不过需要注意一点的就是,event被传递给child的时候将会做相应偏移,如下
                        //final float offsetX = mScrollX - child.mLeft;
                        //final float offsetY = mScrollY - child.mTop;
                        //event.offsetLocation(offsetX, offsetY);
                        //为什么要做偏移呢?因为event的getX得到的值是childView到parentView边境的距离,是一个相对值
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                //找到childIndex所代表的child的最原始的index【?】看代码,children和mChildren指向同一链表
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            //将相应该事件的child包装成一个Target,添加到mFirstTouchTarget的链表中
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        // The accessibility focus didn't handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                //如果没有child相应该事件,则将此事件交给最近加入的target
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

//mFirstTouchTarget == null表示,没有能相应该事件的child,那么就调用父类(也就是View)的dispatchTouchEvent
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 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;
                //表示在Down事件处理中,已经将这个事件交给newTouchTarget处理过了,就不重复处理了
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    //再次判定是否需要cancel(被标记为cancel 或者 事件被拦截)
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
//问:难道看到这里你们会不会产生一个疑问,如果parent在ACTION_MOVE过程中拦截了该事件,哪里还处理呢?
//答:如果拦截了该事件,还是需要自身 dispatchTransformedTouchEvent 函数将事件交个自己的onTouchEvent
//此外dispatchTransformedTouchEvent完成上述操作需要一个条件,也就是child形参数为null
//问:那么怎么为null呢?
//答:往上面看10几行,不就是了吗?
//那么如何达到,其实条件就是 mFirstTouchTarget == null, 请看下面的分析
//如果intercepted == true的情况下, cancelChild == true, predecessor == null
//从而使得mFirstTouchTarget 一直 -> next,当target遍历到最后的时候,next == null,从而使得mFirstTouchTarget == null。
//问: 稍等,这里仅仅做了将mFirstTouchTarget 设置了为null,那么如何派发给自己的onTouchEvent呢?
//这个只能等下一个事件过来了
//结论【事件拦截,拦截了该事件,并没有将本次这个事件传递给自身的onTouchEvent,而需要等到下次】
//问:如何验证
//答:重新FrameLayout的 onInterceptTouchEvent 和 onTouchEvent 将相应的event.getEventTime打印出来,
//将会发现拦截的事件和传递到onTouchEvent的时间不是一个时间。
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        //  cancel ACTION_UP  ACTION_HOVER_MOVE(表示鼠标滑动)等,清理状态
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    // 调试使用,可以忽略
    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

    ViewGroup的dispatchTouchEvent()方法比较繁琐,下面提炼下关键点。

1. 如果收到的ACTION_DOWN,那么清除所有和时间相关的状态。

2. 判断是否拦截事件。

    (1).如果在子View中调用了getParent().requestDisallowInterceptTouchEvent(true),那么将不会拦截该事件。

问:有什么用呢?

答:在listView或其他srcollView都会在onInterceptTouchEvent进行拦截判断,只要距离或者速度达到一定要求,就会拦截该事件。那就做不到一view在listView中上下滑动。那么如何才能实现这个需求?那就需要使用getParent().requestDisallowInterceptTouchEvent(true)不要拦截该事件,将事件传递下去。

    (2).onInterceptTouchEvent()函数这是一个很重要的函数,是在当前的事件达到一定要求之后才进行拦截处理。有了这个,listView才能实现item可点击,而item上下滑动。onInterceptTouchEvent()源码分析如下:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)//鼠标操作
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

        这里返回false,表示不拦截,允许事件继续往下传递(向子View传递)。如果返回true,表示拦截事件,即自己处理该事件(执行自己的onTouchEvent()分发),事件就不会向下传递。

3.如果事件是ACTION_DOWN,并且没有被拦截,将会执行向子View的分发处理。

    (1).分发顺序

5.0之前,基本按照子View被添加的顺序。

5.0之后,还需要考虑Z轴、Drawing顺序、添加的顺序。重点关注Z轴顺序,值最大的将会最优先,这是得到所有的子view,那么如何判断是否点击到子View区域内呢?

    (2).判断子View是否可见或者存在动画,否则不处理。

    (3).事件是否点击到子View的区域内。

    (4).派发该事件给子View,看子View是否要处理。

    (5).将相应该事件的子view加入到mFirstTouchTarget的单链表中(接下来的事件都会直接派发给其中的view)

4. 如果没有一个子View响应Action_DOWN 怎么办?

    那么直接将事件交个父类View的dispatchTouchEvent(),再根据条件分发给自己。

5. 其他事件如 ACTION_MOVE, ACTION_UP, ACTION_CANCEL,则直接照着mFirstTouchTarget链表派发就行。

最后:

  讲讲拦截,如果ViewGrop拦截该事件,那么将立马给之前相应事件的子View派发一个ACTION_CANCEL事件。另外,还需要注意的是,如果ViewGrop拦截该事件,并不会立马将该事件给自己的onTouchEvent,只有等下次事件过来才可能,所以在onInterceptTouchEvent()中需要对ACTION_UP和ACTION_CANCEL也做相应处理。

6. 在ViewGroup中,分发事件调用的是dispatchTransformedTouchEvent(),在这个方法内部调用了子视图的dispatchTouchEvent()。

结论

    1. Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View。

    2. 在ViewGroup中通过onInterceptTouchEvent()对事件传递进行拦截。

    3. ViewGroup的onInterceptTouchEvent()返回false,不拦截事件,允许事件继续向子View传递;子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。

View事件分发机制

源码分析

View中dispatchTouchEvent()的源码分析

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    //判断了当前事件是否可以获取焦点,如果不能获取焦点或者找不到一个view,直接返回false
    if (event.isTargetAccessibilityFocus()) {
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        event.setTargetAccessibilityFocus(false);
    }

    boolean result = false;

    //用于测试,直接忽略
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    //过滤字段的最后8bit,也就是指只关心是ACTION_DOWN、ACTION_UP等事件,而不关心是哪个手指引起的。
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        android.util.SeempLog.record(3);
        // Defensive cleanup for new gesture
        stopNestedScroll();//结束嵌套滑动机制
    }

    //onFilterTouchEventForSecurity判断View是否被遮住
    if (onFilterTouchEventForSecurity(event)) {//安全模式下,忽略和这个事件;否则返回true
        //判断View是否是enabled,以及判断事件是否是滚动条拖动
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        //li.mOnTouchListener是否为null,在于view是否调用了setOnTouchListener(),设置了就不为null
        //li.mOnTouchListener.onTouch(),主要判断onTouch的返回值,true表示消费了事件;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        //如果上面if条件满足,则不执行这里的onTouchEvent();
        //这里可以得出一个结论:在dispatchTouchEvent方法里先执行onTouch()方法
        //如果没有设置OnTouchListener,或者onTouch返回false,或者view不是enabled,执行这里的onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    .........

    return result;
}
    根据代码分析,我们知道ListenerInfo不为空(见下面getListenerInfo方法),同时View调用了setOnTouchListener(),则li.mOnTouchListener不为空,执行OnTouchListener的onTouch方法返回true,则直接返回true,不在执行自身的onTouchEvent响应事件。

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
    所以onTouchEvent的执行与否跟onTouch的返回值有很大关系,有时候我们在对view的onTouch里面返回true,会发现这个view的点击事件没效果,点击事件是不是就在onTouchEvent方法里面?我们接着分析,不过我们可以得出一个结论,就是在 dispatchTouchEvent方法方法里面首先执行onTouch方法。我们接着分析onTouchEvent方法:

public boolean onTouchEvent(MotionEvent event) {
    ........
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    //如果view是DISABLED状态,只要满足下面三种条件之一那么这个view虽然被禁用了,还是会消费这个事件,只是不响应它们而已。
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }
    if (mTouchDelegate != null) {//如果view设置了代理,则执行mTouchDelegate的onTouchEvent方法
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    //一个view是enable且满足三个状态之一则进入switch判断中去,反之则返回false
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                //判断了不是在一个滚动操作的容器中
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    //当前view是否能够获取焦点,触摸是否能够获得焦点,当前view还没有获取焦点,就请求获取一个焦点
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        setPressed(true, x, y);//设置按下状态
                   }

                    //判断用户是否进行了长按,如果没有移除相关检测
                    //mHasPerformedLongPress长按事件标志位,长按事件发生后会将这个标志位设置为true,不是则移除掉长按的延迟消息。
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();

                        //判断有没有重新获取焦点,如果还没有重新获得焦点,那么就已经是一个按下的状态了
                        if (!focusTaken) {
                            //使用post到主线程中执行一个performClick的Runnable,而不是直接执行一个点击事件
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();//见下面源码,这里会执行onClick方法
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    if (prepressed) {//按下的效果显示时间
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration()/*64ms*/);
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }

                    removeTapCallback();
                }
                mIgnoreNextUpEvent = false;//将设置的按下的状态设置为false
                break;

            case MotionEvent.ACTION_DOWN:
                mHasPerformedLongPress = false;//设置长按标记位

                //一般的设备都是false,这是一个处理鼠标右键的事件
                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                //判断当前view是否在一个滚动的view容器内,避免把滑动当成一次点击事件
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();//准备点击的标记位
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    //发送一个延迟100ms的消息确定用户是要滚动还是点击
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    //如果不是在一个滚动容器内,则调用setPressed(true, x, y)设置一个按下状态,然后再检查长按状态;
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                //移除各种延迟发送的消息
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();

                        setPressed(false);
                    }
                }
                break;
        }

        return true;
    }

    return false;
}
     我们看下上面方法中用到的performClick()方法,如下:

/**
 * Call this view's OnClickListener, if it is defined.  Performs all normal
 * actions associated with clicking: reporting accessibility event, playing
 * a sound, etc.
 *
 * @return True there was an assigned OnClickListener that was called, false
 *         otherwise is returned.
 */
public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    //view调用了setOnClickListener时,mOnClickListener不为null
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);//执行Listener的onClick方法,消费了事件
        result = true;
        //至此,也就知道了OnClick方法是在onTouchEvent()里面执行的
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}
 点击事件是在onTouchEvent方法中的ACTION_UP中执行一个click事件,并且我们每次执行一个action,只有前一个action返回true,才会执行下一个action,因为前一个action返回了false,那么 dispatchTouchEvent 将不会派发下一次事件。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值