美文网首页
ViewGroup事件分发

ViewGroup事件分发

作者: 许先森的许 | 来源:发表于2018-04-21 18:41 被阅读26次

    上次已经从源码角度分析了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
    

    相关文章

      网友评论

          本文标题:ViewGroup事件分发

          本文链接:https://www.haomeiwen.com/subject/degtlftx.html