ViewGroup 事件传递机制


上一章说了view的触摸事件的传递机制,这一章就讲讲ViewGrou的事件传递机制,ViewGroup 是 View 的子类,也就是说View包含的功能,ViewGroup 都有,并且做了相应的修改和扩展。

ViewGroup 比着 View 多了一个方法 onInterceptTouchEvent(MotionEvent event)。我们平时的焦点触摸事件,都是通过ViewGroup容器传给View的,比较重要的方法有三个,入口也是dispatchTouchEvent(MotionEvent event),用于事件的分发,不管是ViewGroup还是View; onInterceptTouchEvent(MotionEvent event)  这个方法是拦截事件的方法,根据它返回的值决定是否往下一层view中分发,默认是false,不会拦截,如果为true,则不会传递给子view,会把焦点事件由自身处理。view中是没有这个方法的;onTouchEvent(MotionEvent event)这个是处理事件的方法,它在 dispatchTouchEvent() 方法里调用,如果返回true则表示消耗当前事件,如果返回false则是不消耗当前事件。这三个方法的简单关系可以理解如下

    public boolean dispatchTouchEvent(MotionEvent event){
        boolean handle = false;
        if(onInterceptTouchEvent(event)){
            handle = onTouchEvent(event);
        }else{
            handle = child.dispatchTouchEvent(event);
//            if(!handle){
//                 handle = onTouchEvent(event); //   这一部分是隐藏的,为了理解添加的,如果扰乱了思路,可以忽略这部分
 //            }
        }
        return handle;
    }

通俗来说,从它的上一层容器也就是父View中接收到事件,也就是 dispatchTouchEvent(MotionEvent event) 方法为入口,然后再入口中会先判断 onInterceptTouchEvent(MotionEventevent) 中返回的值,如果是true,标识拦截了该事件,会把它交给自身的 onTouchEvent(MotionEvent event) 来处理,onTouchEvent(MotionEvent event) 中返回的值即是dispatchTouchEvent 方法返回的值;如果 onInterceptTouchEvent 方法返回为false,标识不拦截事件,会把它传递给子类view,这里就是上一章中view接收事件的入口,根据子view返回的值决定下一步,如果子view返回的是true,说明消费了,此时它的值就是 dispatchTouchEvent 方法的值,如果子view返回为false,则会把事件交给自身的 onTouchEvent(MotionEvent event) 事件来处理,大体上是这样,但还有一些细节需要处理。 写了个例子

public class TouTestFrameLayout extends FrameLayout {

    private final static String TAG = "TouTestView";

    public TouTestFrameLayout(Context context) {
        this(context, null);
    }

    public TouTestFrameLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TouTestFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initLister();
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "TouTestFrameLayout  dispatchTouchEvent:  " + event.getAction());
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        Log.e(TAG, "TouTestFrameLayout  onInterceptTouchEvent:  "+ event.getAction());
        return super.onInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "TouTestFrameLayout  onTouchEvent:  " + event.getAction());
        return super.onTouchEvent(event);
    }

    private void initLister() {
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "TouTestFrameLayout  onTouchListener:  " + event.getAction() + "  " + isClickable() +"   " + isEnabled());
                return false;
            }
        });
    }

}

public class TouTestView extends View {

    private final static String TAG = "TouTestView";

    public TouTestView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TouTestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent:  " + event.getAction());
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent:  " + event.getAction());
        return super.onTouchEvent(event);
    }

}
        <com.view.TouTestFrameLayout
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:background="@color/main_red_day">

            <com.view.TouTestView
                android:layout_width="200dp"
                android:layout_height="100dp"
                android:background="@color/cardview_dark_background" />
        </com.view.TouTestFrameLayout>

TouTestView 为上一章开头的自定义的布局,我们使用没有添加点击事件和长按点击事件的view,只保留 dispatchTouchEvent 和 onTouchEvent 方法,我们的布局中也可以看出来,TouTestFrameLayout 比着 TouTestView 大,有一部分空余的布局,我们试着点击空余部分和滑动,看一下打印的日志,发现都是一样的

 E/TouTestView: TouTestFrameLayout  dispatchTouchEvent:  0
 E/TouTestView: TouTestFrameLayout  onInterceptTouchEvent:  0
 E/TouTestView: TouTestFrameLayout  onTouchEvent:  0

onInterceptTouchEvent(MotionEvent event) 默认为false,这也验证了上一章的内容,ACTION_DOWN 时,dispatchTouchEvent 方法收到的值为false,事件只触发一次,后面的ACTION_MOVE 和 ACTION_UP 没有触发。如果加入点击事件或者把 onTouch(View v, MotionEvent event) 或 onTouchEvent(MotionEvent event) 返回值为true,则有后续的触摸事件,这一点上,ViewGroup 和 View 是一样的。

我们在如果在 TouTestView 点击一下,或者滑动一下,打印的日志一样

  E/TouTestView: TouTestFrameLayout  dispatchTouchEvent:  0
  E/TouTestView: TouTestFrameLayout  onInterceptTouchEvent:  0
  E/TouTestView: dispatchTouchEvent:  0
  E/TouTestView: onTouchEvent:  0
  E/TouTestView: TouTestFrameLayout  onTouchEvent:  0

由于 TouTestView 的 onTouchEvent 方法返回为false,所以最终把事件交给了 TouTestFrameLayout 的 onTouchEvent 方法,这个与上面咱们的分析也能对上。如果给TouTestView添加一个 setOnClickListener 点击事件的回调,然后在 TouTestView 点击一下,打印的日志如下

  E/TouTestView: TouTestFrameLayout  dispatchTouchEvent:  0
  E/TouTestView: TouTestFrameLayout  onInterceptTouchEvent:  0
  E/TouTestView: dispatchTouchEvent:  0
  E/TouTestView: onTouchEvent:  0
  E/TouTestView: TouTestFrameLayout  dispatchTouchEvent:  1
  E/TouTestView: TouTestFrameLayout  onInterceptTouchEvent:  1
  E/TouTestView: dispatchTouchEvent:  1
  E/TouTestView: onTouchEvent:  1
  
  滑动一下,
  E/TouTestView: TouTestFrameLayout  dispatchTouchEvent:  0
  E/TouTestView: TouTestFrameLayout  onInterceptTouchEvent:  0
  E/TouTestView: dispatchTouchEvent:  0
  E/TouTestView: onTouchEvent:  0
  E/TouTestView: TouTestFrameLayout  dispatchTouchEvent:  2
  E/TouTestView: TouTestFrameLayout  onInterceptTouchEvent:  2
  E/TouTestView: dispatchTouchEvent:  2
  E/TouTestView: onTouchEvent:  2
  E/TouTestView: TouTestFrameLayout  dispatchTouchEvent:  2
  E/TouTestView: TouTestFrameLayout  onInterceptTouchEvent:  2
  E/TouTestView: dispatchTouchEvent:  2
  E/TouTestView: onTouchEvent:  2
  E/TouTestView: TouTestFrameLayout  dispatchTouchEvent:  1
  E/TouTestView: TouTestFrameLayout  onInterceptTouchEvent:  1
  E/TouTestView: dispatchTouchEvent:  1
  E/TouTestView: onTouchEvent:  1

从上面打印的日志,能验证我们最上面简化的那个方法代码,同时也验证了,如果down的时候,子view没有返回true,则没有了move和up的后续事件的触发,注意 onInterceptTouchEvent方法,如果down的时候子view为false,则 onInterceptTouchEvent 方法只会触发一次;如果子view返回为true,则 onInterceptTouchEvent 方法在move和up都会调用。如果把
TouTestFrameLayout 的 onInterceptTouchEvent(MotionEvent event)  方法修改为 return true,我们再次点击 TouTestView 试试

  E/TouTestView: TouTestFrameLayout  dispatchTouchEvent:  0
  E/TouTestView: TouTestFrameLayout  onInterceptTouchEvent:  0
  E/TouTestView: TouTestFrameLayout  onTouchEvent:  0

我们发现,一旦拦截了,本身的 onTouchEvent(MotionEvent event) 返回的是false,导致后续 move 和 up 都没有传递进去,这时候,从焦点事件传递来看,ViewGroup和View是一样的。有兴趣的可以给 TouTestFrameLayout 再添加个 setOnClickListener 点击事件试试日志。下面开始分析ViewGroup的事件传递源码

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
onInterceptTouchEvent(MotionEvent ev) 的默认返回值是false, ViewGroup 中没有重写 onTouchEvent(MotionEvent event) 方法,它用的是父类 View 的 onTouchEvent(MotionEventevent)方法, setOnTouchListener 、 setOnClickListener 方法也都是继承 View的,没有重写,那么咱么就重点看看 dispatchTouchEvent(MotionEvent ev) 方法吧
由于代码太长,我们分段来看代码,我们知道事件一般是 down-up 或者 down-move-up,不管是哪个,肯定是以down开始

            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
resetTouchState() 中会把一个成员变量 mFirstTouchTarget 置空为null,我们先记住这点,继续往下看  重点1

            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 {
                intercepted = true;
            }
如果是 ACTION_DOWN 或 mFirstTouchTarget != null,我们会执行是否拦截的操作, disallowIntercept 是不拦截,如果这个属性时false时,说明允许拦截,这时候会调用onInterceptTouchEvent(ev) 方法,在这个方法中决定是否最终拦截,intercepted 意思是拦截,根据 disallowIntercept 和 onInterceptTouchEvent(ev) 决定 intercepted 的值,disallowIntercept 有什么用,最后再分析。 mFirstTouchTarget 这个变量在这里看不出是干什么的,如果往后看,会发现如果子View的down消费了事件,mFirstTouchTarget会被赋值,不为null,也就是说,想要触发 onInterceptTouchEvent(ev) 方法:1、在down的时候触发;2、down的时候子View消费了事件,返回true,mFirstTouchTarget 被赋值。一旦 mFirstTouchTarget 被赋值,在本次 down-move-up 事件没结束前,不会被指控,它会在下一次事件触发时的开始使被清空重新赋值。 继续往下看  重点2

    if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                   ...
                    if (newTouchTarget == null && childrenCount != 0) {
                        ...
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            ...
                            if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            ...
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                ...
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    ...
                }
            }
简化后的代码, if (!canceled && !intercepted) 意思是没有取消,也没有被拦截才能走到if的判断语句里面,紧接着又是一个if判断,actionMasked == MotionEvent.ACTION_DOWN,常规的 down-move-up 中,只有down能走进去, move 和 up 靠边站,根本进不来。继续往下看,这个时候会遍历容器里面的子View,注意 canViewReceivePointerEvents 和
isTransformedTouchPointInView 方法,

    private static boolean canViewReceivePointerEvents(View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }
一个是说子视图是否可以接收触摸事件,另一个是手指按下的位置是否在子view的范围内,如果不满足这两个条件,就continue找下一个,如果满足,继续往下看

        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }
dispatchTransformedTouchEvent() 方法中,简化后如下

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        return handled;
    }
如果传入的子view不为空,就调用子view的 dispatchTouchEvent 方法,否则就调用自己的 dispatchTouchEvent 方法; addTouchTarget() 方法如下
    
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
这个意思是给 mFirstTouchTarget 赋值,此时 mFirstTouchTarget 不为空,在此也证明了 down 时,如果子view消费了事件,mFirstTouchTarget 不为空,如果mFirstTouchTarget 为null,说明 down 时候,没有消费,这是再看 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) ,就能明白这句话的含义了。alreadyDispatchedToNewTouchTarget = true; break;  赋值一个局部变量,记录子view已经消费了down的时间,然后break跳出for循环,已经被消费了,就没必要继续找了。上面一大段代码,仅仅是down并且 intercepted 没被拦截时的分析,继续看后续代码   重点3

        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            ...
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    ...
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    ...
            }
        }
        return handled;
        
如果子view消费了事件,mFirstTouchTarget 有值,则执行while里面的操作,注意,此时alreadyDispatchedToNewTouchTarget为true,则直接返回return handled,handled 为true,Down事件到此为止;如果 intercepted 为true,被拦截了,或者 intercepted 为false,走进了if (!canceled && !intercepted) 代码中,但没有子view消费触摸事件,mFirstTouchTarget 值为 null,此时,执行 dispatchTransformedTouchEvent 方法,这个方法上面有,此时传进去的子view参数为null,说明里面只能执行 super.dispatchTouchEvent(event),也就是基类的事件分发方法,在基类的 dispatchTouchEvent(event) 中,会调用到ViewGroup 自身的 onTouch(View v, MotionEvent event) 和 onTouchEvent(MotionEventevent)  方法,流程与View一样,最终handled的值就由它两个方法决定了。

down事件就结束了,假设返回值为 handled1。我们假设不管是true或false,都会执行完整的down-move-up事件,实际上非顶层的ViewGrouop外,都是只有为true时才会执行move和up事件。然后分析move, 如果 handled1 为 false, 重点1 中的代码 mFirstTouchTarget 为null,if语句不会执行,onInterceptTouchEvent 方法自然也不会执行;如果 handled1 为 true,分为子View消费和本身消费,如果是本身消费了,mFirstTouchTarget 为null,同样不会执行 onInterceptTouchEvent 方法;如果是子View消费,mFirstTouchTarget 不为null,可以走到if判断语句,默认是可以执行 onInterceptTouchEvent 方法,这一点也与之前的log日志相对应。 重点2 中的代码,不管 handled1 值是多少,都不会执行,因为if的语句判断,把move和up排除在外了,我们还是重点看看  重点3 的代码回归正题,只有down的时候被消费了,踩会执行move和up。分析move和up: 一、如果是子View消费了事件,mFirstTouchTarget 不为null,走到else里面,此时 alreadyDispatchedToNewTouchTarget为false,因为是局部变量不是成员变量,这时候会执行 dispatchTransformedTouchEvent 方法,此时传进去子view不为空,执行子view的 dispatchTouchEvent 方法,进而执行 onTouchEvent 方法;二、如果子View没有消费事件,是被ViewGroup消费掉了,那么 mFirstTouchTarget 为null,直接执行dispatchTransformedTouchEvent 方法,传入 View child 参数为null,此时执行super.dispatchTouchEvent(event) 即父类的 dispatchTouchEvent 方法,进而执行ViewGroup的onTouchEvent 方法。也就是说,如果down的时候子view中没有消费事件,那么接下来move和down就不会传给子view了,而是直接调用基类的 dispatchTouchEvent 方法,在它里面调用
onTouchEvent 方法。

最后看一下 重点1 中的一行代码, final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 这个是是否不允许拦截,如果父类的这个属性为true,则父View肯定不会拦截子view,ViewGroup 中有个方法 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 可以决定这个值

    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            return;
        }
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }
我们发现,一旦子view拿到它的父View,然后调用了这个方法,这个方法是遍历往上层掉用,一直到根节点位置。一点调用这个方法,比如说子View中执行getParent().requestDisallowInterceptTouchEvent(true); 在眼下这个例子中,disallowIntercept 值为true,则 disallowIntercept 为true,intercepted = false; 表示不拦截焦点触发事件;反之,传入 false,则标识允许拦截,会执行 onInterceptTouchEvent 方法,由这个方法决定是否拦截。我们发现, onInterceptTouchEvent 方法返回false表示不拦截,
requestDisallowInterceptTouchEvent 方法传入true表示不拦截,这两个方法是相对的。这个方法常在子View需要处理焦点事件时调用这个方法,告知父View不要拦截。
 

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/89374305