ViewGroup事件分发

上次已经从源码角度分析了View的事件分发,如果对View的事件分发不熟悉请先阅读:
View事件分发
今天要说的是ViewGroup事件分发。

1.ViewGroup是什么

从源码中可以看到,她是一个abstract的类,父类是View,头部注释:

    * <p>
     * A <code>ViewGroup</code> is a special view that can contain other views
     * (called children.) The view group is the base class for layouts and views
     * containers. This class also defines the
     * {@link android.view.ViewGroup.LayoutParams} class which serves as the base
     * class for layouts parameters.
     * </p>
注释告诉我们,ViewGroup是一个特殊的View。哪里特殊呢?

第一:她可以包含多个子View和子ViewGroup,是所有布局的直接或间接父类。
第二:她定义了布局参数类:ViewGroup.LayoutParams。这两个特殊点也就是和View的区别。
常用的LinearLayout、RelativeLayout都是继承自ViewGroup。

2.深入

上次先分析了View的onTouch和onClick方法,这次同样用这两个方法来分析ViewGroup。

为了方便我们打log,我们不能直接用ViewGroup或者LinearLayout,我们先自定义一个布局TestDispatchEventLayout,继承LinearLayout,如下:

    public class TestDispatchEventLayout extends LinearLayout {

      public TestDispatchEventLayout(Context context, @Nullable AttributeSet attrs) {
          super(context, attrs);
        }
    }

然后在Activity的布局中使用我们自定义的布局,并且在其中加两个Button(为什么用Button,因为Button是默认可点击的,对onTouchEvent事件有影响,具体参考上一篇文章)如图所示:

    <?xml version="1.0" encoding="utf-8"?>
    <com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/main_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Button
            android:id="@+id/button1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Button1" />

        <Button
            android:id="@+id/button2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Button2" />
</com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout>

在Activity中给TestDispatchEventLayout和两个Button分别注册监听:

    layout.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ALog.d("layout  onTouch");
            return false;
        }
    });
    button1.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ALog.d("button1  onTouch  action = " + event.getAction());
            return false;
        }
    });
    button1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ALog.e("button1  onClick");
        }
    });
    button2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ALog.e("button2  onClick");
        }
    });

我们给button1加了一个onTouch监听,来回顾一下上一次说的View点击事件:事件先传递给onTouch,如果onTouch返回false(事件没有被onTouch消费掉),则事件继续传递给onClick。

运行程序,点击Button1,打印结果如下:

    button1  onTouch  action = 0
    button1  onTouch  action = 2
    button1  onTouch  action = 1
    button1  onClick

如果把onTouch返回true,理论上应该只有onTouch的打印,没有onClick的打印,重新运行后点击Button1,打印结果如下:

    button1  onTouch  action = 0
    button1  onTouch  action = 2
    button1  onTouch  action = 1

打印正确。把onTouch返回改回false。
分别点击Button2和空白区域,打印结果如下:

    button2  onClick
    layout  onTouch

结合以上所有打印结果,我们可以发现几个现象:
1、点击按钮的时候,layout的onTouch没有执行。
2、点击空白区域的时候,layout的onTouch执行。

暂时得出假设:onTouch先传递子View,再传递给ViewGroup,并且子View会把事件消费掉。

验证假设:
首先,要知道ViewGroup中的一个方法:onInterceptTouchEvent,看源码:

    /**
 * Implement this method to intercept all touch screen motion events.  This
 * allows you to watch events as they are dispatched to your children, and
 * take ownership of the current gesture at any point.
 *
 * <p>Using this function takes some care, as it has a fairly complicated
 * interaction with {@link View#onTouchEvent(MotionEvent)
 * View.onTouchEvent(MotionEvent)}, and using it requires implementing
 * that method as well as this one in the correct way.  Events will be
 * received in the following order:
 *
 * <ol>
 * <li> You will receive the down event here.
 * <li> The down event will be handled either by a child of this view
 * group, or given to your own onTouchEvent() method to handle; this means
 * you should implement onTouchEvent() to return true, so you will
 * continue to see the rest of the gesture (instead of looking for
 * a parent view to handle it).  Also, by returning true from
 * onTouchEvent(), you will not receive any following
 * events in onInterceptTouchEvent() and all touch processing must
 * happen in onTouchEvent() like normal.
 * <li> For as long as you return false from this function, each following
 * event (up to and including the final up) will be delivered first here
 * and then to the target's onTouchEvent().
 * <li> If you return true from here, you will not receive any
 * following events: the target view will receive the same event but
 * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
 * events will be delivered to your onTouchEvent() method and no longer
 * appear here.
 * </ol>
 *
 * @param ev The motion event being dispatched down the hierarchy.
 * @return Return true to steal motion events from the children and have
 * them dispatched to this ViewGroup through onTouchEvent().
 * The current target will receive an ACTION_CANCEL event, and no further
 * messages will be delivered here.
 */
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;
}

注释告诉我们这个方法用来拦截所有的触摸屏幕动作事件。并且允许你在事件被分配给你的子view或者子viewgroup时进行监视,你可以在任何时候去拦截掉事件。
打个比方:父控件把up事件拦截,子控件会处理到down和move,但是没有up触发不了onClick。

既然这个方法是有返回值的,那么我们重写onInterceptTouchEvent方法,改变返回值来看一下效果,先改为返回false,如下:

    public class TestDispatchEventLayout extends LinearLayout {

public TestDispatchEventLayout(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}
}

运行程序,分别点击Button1、Button2和空白区域。打印如下:

    button1  onTouch  action = 0
    button1  onTouch  action = 1
    button1  onClick
    button2  onClick
    layout  onTouch

再把onInterceptTouchEvent返回值改为true,运行程序,分别点击Button1、Button2和空白区域。打印如下:

    layout  onTouch
    layout  onTouch
    layout  onTouch

可以看出,不管我们点击哪里,都只触发layout的onInterceptTouchEvent,按钮的onTouch和onClick被屏蔽了。这说明事件应该是先传递给ViewGroup,再传递给View,和我们上面的假设相反,说明上面的假设是错误的。

重新假设:事件先传递给ViewGroup,再传递给View。如果ViewGroup不拦截事件,事件将由View消费。

验证假设:
上次文章中我们说了只要我们触摸了控件,一定会调用该控件的dispatchTouchEvent方法,那么既然ViewGroup也是集成View的,它肯定也会执行dispatchTouchEvent方法。让我们来看一下ViewGroup中dispatchTouchEvent源码:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    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.
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        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.
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                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;
        }

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

        // Check for cancelation.
        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.
                removePointersFromTouchTargets(idBitsToAssign);

                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.
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        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.
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }

                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        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);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                // childIndex points into presorted list, find original index
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            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();
                }

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

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            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;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // Update list of touch targets for pointer up or cancel, if needed.
        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;
}

可以看出这个方法是重写父类View中的dispatchTouchEvent。方法很长只看关键部分:

    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.
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

首先,当事件是ACTION_DOWN的时候,ViewGroup的dispatchTouchEvent中会清除所有的上一个手势的状态。
然后是事件拦截:

    // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                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;
        }

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

先判断是否是ACTION_DOWN事件或者有首次触摸目标,
false:表示没有首次触摸目标,或者这个事件不是初始的DOWN事件,那么只能说明DOWN已经被自己消费了,所以ViewGroup继续拦截事件,于是intercepted = true;
true:继续判断是否允许拦截(ViewGroup中disallowIntercept默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改),不允许intercepted = false; 允许拦截intercepted = onInterceptTouchEvent(ev);
可以看到ViewGroup的onInterceptTouchEvent方法在这里被调用了,也就是说我们之前在onInterceptTouchEvent返回true,这里的intercepted就会被赋值为true。

image.png

继续往下看:if (!canceled && !intercepted),如果条件满足:不是取消并且不拦截,进入到条件内部,
继续判断:当事件是按下或者多点按下或者鼠标在View上时,会倒序遍历所有的子View,(为什么要倒序,因为安卓的布局机制是后加进来的view会显示在上层,可以想象一下圆筒包装的薯片,先装进去的薯片在底层,后装进去的薯片在上层。如果点击的位置有很多层view,那么最上层的薯片来响应事件。)
然后通过getTouchTarget拿到的触摸目标对应的子View,如果不为空,则跳出循环遍历。否则继续向下执行:if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // If for some reason we ended up in an inconsistent state where it looks like we
    // might produce a motion event with no pointers in it, then drop the event.
    if (newPointerIdBits == 0) {
        return false;
    }

    // If the number of pointers is the same and we don't need to perform any fancy
    // irreversible transformations, then we can reuse the motion event for this
    // dispatch as long as we are careful to revert any changes we make.
    // Otherwise we need to make a copy.
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}

这个方法中存在递归调用dispatchTouchEvent方法,如果child是ViewGroup递归调用dispatchTouchEvent,如果child是View,默认会调用其onTouchEvent()。
如果dispatchTransformedTouchEvent返回true说明子View消费掉了该事件,同时进入该if判断,执行下面操作:

    newTouchTarget = addTouchTarget(child, idBitsToAssign);
     alreadyDispatchedToNewTouchTarget = true;
    break;

给newTouchTarget赋值,mFirstTouchTarget = newTouchTarget,alreadyDispatchedToNewTouchTarget设置为true,并且跳出循环。
如果dispatchTransformedTouchEvent返回false,说明没有子View消费该事件,mFirstTouchTarget为空,递归后就会让intercepted为true,事件将被ViewGroup拦截,不会再传递给子View,那么该子View就无法继续处理ACTION_MOVE事件和ACTION_UP事件。

     if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null)

可以看到,如果一开始事件不是ACTION_DOWN,就不会经过上面的流程,直接执行下面的流程。上面对ACTION_DOWN处理后mFirstTouchTarget可能是空可能不为空,所以下面的流程会有if (mFirstTouchTarget == null),判断了mFirstTouchTarget值是否为null的情况。
当mFirstTouchTarget==null时,也就是事件未被子View消费,则调用super.dispatchTouchEvent处理事件,和普通View一样了。

所以之前点击Button的时候只打印了Button的onTouch和onClick,却没有打印layout的onTouch。因为Button的onTouchEvent返回的是true,所以dispatchTransformedTouchEvent返回true,因此mFirstTouchTarget !=null,导致intercepted为false,ViewGroup不拦截事件,事件由子View Button消费。

当mFirstTouchTarget !=null 时,也就是说找到了消费事件的子View,后续的事件可以继续传递给该子View处理。
到此ViewGroup的dispatchTouchEvent方法分析完毕。

image.png

3.浅出

基于上面的分析,如果我们把TestDispatchEventLayout的继承改为RelativeLayout,并且onInterceptTouchEvent返回false,然后Activity的布局改为Btn1在Btn2的上层:

    <com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<Button
    android:id="@+id/button2"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Button2" />
<Button
    android:id="@+id/button1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Button1" />
</com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout>

其他不变,理论上的结果应该只打印Btn1的事件。 运行程序,点击按钮,打印结果如下:

    button1  onTouch  action = 0
    button1  onTouch  action = 1
    button1  onClick

果然没有button2的打印了。

总结:

1、事件派发由ViewGroup,再传递到View;

2、ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,决定事件是否传递给子View;

3、子View中如果将传递的事件消费掉,ViewGroup中将不会再处理该事件。

这里在捎带提一下View中的dispatchTouchEvent,简单记作:enable决定执行onTouch,onTouch为false决定执行onTouchEvent,onTouchEvent决定dispatchTouchEvent返回值

最后来做几个实验:
1、首先自定义TestDispatchEventLayout 继承 RelativeLayout 代码如下所示:

    public class TestDispatchEventLayout extends RelativeLayout {

        public TestDispatchEventLayout(Context context, @Nullable AttributeSet attrs) {
              super(context, attrs);
        }
}

2、接着,在自定义布局下面加上2个ImageView,id分别为button1和button2(取名button方便改为button,但是记住实际是ImageView,也就是说默认不是可点击的):

    <com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<ImageView
    android:id="@+id/button2"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="@color/colorPrimaryDark"
    android:text="Button2" />


<ImageView
    android:id="@+id/button1"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="@mipmap/ic_launcher"
    android:text="Button1" />

</com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout>

3、然后给layout和2个控件都加上setOnTouchListener监听,并且打印,如下:

     ImageView button1;
ImageView button2;
TestDispatchEventLayout layout;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.test_layout_inflate_view);
    button1 = (ImageView) findViewById(R.id.button1);
    button2 = (ImageView) findViewById(R.id.button2);
    layout = (TestDispatchEventLayout) findViewById(R.id.main_layout);

    ALog.i("layout.getChildCount() = " + layout.getChildCount());

    layout.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ALog.i("layout  onTouch action = " + event.getAction());
            return false;
        }
    });
     button1.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ALog.d("button1  onTouch  action = " + event.getAction());
            return false;
        }
    });
    button2.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ALog.e("button2  onTouch  action = " + event.getAction());
            return false;
        }
    });
    }

因为是继承RelativeLayout,所以button1层级是堆叠在button2之上,如图:


image.png

现在我们点击ImageView,会打印什么?

按上面的理论分析:Down事件先到达layout的dispatchTouchEvent,因为没有设置intercepet为true,所以layout不拦截事件,传递给她的子控件,最上层子控件是button1,所以button1拿到down事件进入dispatchTouchEvent,因为我们注册了button1的onTouch,但是没有返回true,所以button1会打印一次onTouch--Down,然后执行button1的onTouchEvent,又因为button1实际是ImageView,默认不可点击,所以onTouchEvent直接返回false,事件继续向下传递给button2,button2同理只会打印一次onTouch--Down,继续把事件传递给layout,layout会打印onTouch--Down,因为没有把onTouch返回true,而且默认也是不可点击,所以也没法继续执行后续的onTouch--Up的事件,事件到此结束。

运行后打印结果为:

    layout.getChildCount() = 2
    button1  onTouch  action = 0
    button2  onTouch  action = 0
    layout  onTouch action = 0

理论分析正确。
用图来表示:


image.png

如果我们把中间的那个ImageView注册onClick事件(setOnClickListener中会把控件改为可点击状态),也就是button2,会怎么打印?

理论分析:Down事件先到达layout的dispatchTouchEvent,因为没有设置intercepet为true,所以layout不拦截事件,传递给她的子控件,最上层子控件是button1,所以button1拿到down事件进入dispatchTouchEvent,因为我们注册了button1的onTouch,但是没有返回true,所以button1会打印一次onTouch--Down,然后执行button1的onTouchEvent,又因为button1实际是ImageView,默认不可点击,所以onTouchEvent直接返回false,事件继续向下传递给button2,button2会打印一次onTouch--Down,然后执行button2的onTouchEvent,这次button2是可点击的,onTouchEvent返回true,事件被button2消费,于是执行后续的Move、Up事件,所以会继续打印button2的onTouch--Move,onTouch--Up,onClick,事件到此结束,不会再传递给layout。

运行后打印结果:

    layout.getChildCount() = 2
    button1  onTouch  action = 0
    button2  onTouch  action = 0
    button2  onTouch  action = 1
    button2  onClick

理论分析正确。
现在我们来把onTouchEvent事件也打印出来,在上面的基础上改动如下:
创建一个自定义ImageView:

    public class MyImageView extends ImageView {

        public MyImageView(Context context, @Nullable AttributeSet attrs) {
              super(context, attrs);
        }

      @Override
        public boolean onTouchEvent(MotionEvent event) {
            ALog.e("onTouchEvent action = " + event.getAction());
            return super.onTouchEvent(event);
        }
}

在onTouchEvent方法中加入打印。
之前的布局最上层的ImageView也就是button1改成用MyImageView:

    <?xml version="1.0" encoding="utf-8"?>
    <com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout            xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/main_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/button2"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/colorPrimaryDark"
            android:text="Button2" />

    <com.example.xuchun.myapplication.view.MyImageView
            android:id="@+id/button1"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@mipmap/ic_launcher"
            android:text="Button1"
            />

</com.example.xuchun.myapplication.viewGroup.TestDispatchEventLayout>

activity中的代码改动如下:

    ALog.i("layout.getChildCount() = " + layout.getChildCount());

    layout.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ALog.i("layout  onTouch action = " + event.getAction());
            return false;
        }
    });
    button1.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ALog.d("button1  onTouch  action = " + event.getAction());
            return false;
        }
    });
    button1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ALog.d("button1  onClick");
        }
    });
    button2.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ALog.e("button2  onTouch  action = " + event.getAction());
            return false;
        }
    });

这时候点击button1会怎么打印?

理论分析:Down事件先到达layout的dispatchTouchEvent,因为没有设置intercepet为true,所以layout不拦截事件,传递给她的子控件,最上层子控件是button1,所以button1拿到down事件进入dispatchTouchEvent,因为我们注册了button1的onTouch,但是没有返回true,所以button1会打印一次onTouch--Down,然后执行button1的onTouchEvent,因为注册了onClick,所以button1是可点击状态,于是onTouchEvent会返回true,消费事件,后续的事件继续执行:onTouch--Move,onTouchEvent--Move,onTouch--Up,onTouchEvent--Up,onClick,事件传递结束,不会再传给button2和layout。

运行程序,打印如下:

    button1  onTouch  action = 0
    onTouchEvent action = 0
    button1  onTouch  action = 2
    onTouchEvent action = 2
    button1  onTouch  action = 1
    onTouchEvent action = 1
    button1  onClick
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容