Android事件分发机制完全解析(三) :ViewGroup的事件分发机制

ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是Android中所有布局的父类或间接父类,像LinearLayoutRelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。

本文通过源码(api:10)的形式介绍ViewGroup的事件分发机制。
在这里插入图片描述

一.onInterceptTouchEvent

代码片1
/**
     * 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) {
        return false;
    }

onInterceptTouchEvent()的注释很长,但源码很短,短得只有1行。

onInterceptTouchEvent()的作用是是否拦截事件。
a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
b. 返回false = 不拦截(默认)

二.dispatchTouchEvent

1. ACTION_DOWN事件

 public boolean dispatchTouchEvent(MotionEvent ev) { 

         ... // 仅贴出关键代码

        if (action == MotionEvent.ACTION_DOWN) {
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }

            // 重点分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件

            if (disallowIntercept || !onInterceptTouchEvent(ev)) {

                // 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false)
                // ,可通过调用requestDisallowInterceptTouchEvent()修改
                // 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
                // a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
                // b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
                // c. 关于onInterceptTouchEvent() ->>在一中onInterceptTouchEvent有分析

                ev.setAction(MotionEvent.ACTION_DOWN);
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;

                // 重点分析2
                // 通过for循环,遍历了当前ViewGroup下的所有子View
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        child.getHitRect(frame);

                        // 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
                        // 若是,则进入条件判断内部
                        if (frame.contains(scrolledXInt, scrolledYInt)) {
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;

                            // 条件判断的内部调用了该View的dispatchTouchEvent()
                            // 即 实现了点击事件从ViewGroup到子View的传递(即:View的事件分发机制)
                            if (child.dispatchTouchEvent(ev)) {

                                mMotionTarget = child;
                                return true;
                                // 调用子View的dispatchTouchEvent后是有返回值的
                                // 若该控件可点击,那么点击时dispatchTouchEvent的返回值必定是true(由view的事件分发可知),因此会导致条件判断成立
                                // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                                // 即把ViewGroup的点击事件拦截掉

                            }
                        }
                    }
                }
            }
        }
        
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);
        if (isUpOrCancel) {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        final View target = mMotionTarget;

        // 重点分析3
        // 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)
        if (target == null) {
            ev.setLocation(xf, yf);
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }

            return super.dispatchTouchEvent(ev);
            // 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
            // 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick()
            // ,即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())
            // 此处需与上面区别:子View的dispatchTouchEvent()
        } 

        ...

    }

2. ACTION_MOVE事件

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;
 
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
 
       //...ACTION_DOWN
 
       //...ACTIN_UP or ACTION_CANCEL
 
        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
	final View target = mMotionTarget;
      
 
        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            //....
        }
        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);
 
        return target.dispatchTouchEvent(ev);
    }

18行:把ACTION_DOWN时赋值的mMotionTarget,付给target ;

23行:if (!disallowIntercept && onInterceptTouchEvent(ev)) 当前允许拦截且拦截了,才进入IF体,当然了默认是不会拦截的~这里执行了onInterceptTouchEvent(ev)

28-30行:把坐标系统转化为子View的坐标系统

32行:直接return target.dispatchTouchEvent(ev);

可以看到,正常流程下,ACTION_MOVE在检测完是否拦截以后,直接调用了子View.dispatchTouchEvent,事件分发下去;

3. ACTION_UP事件

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }
 
        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;
 
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
 
        if (action == MotionEvent.ACTION_DOWN) {...}
	
	boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);
 
	if (isUpOrCancel) {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
	final View target = mMotionTarget;
	if(target ==null ){...}
	if (!disallowIntercept && onInterceptTouchEvent(ev)) {...}
 
        if (isUpOrCancel) {
            mMotionTarget = null;
        }
 
        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);
 
        return target.dispatchTouchEvent(ev);
    }

17行:判断当前是否是ACTION_UP

21,28行:分别重置拦截标志位以及将DOWN赋值的mMotionTarget置为null,都UP了,当然置为null,下一次DOWN还会再赋值的~

最后,修改坐标系统,然后调用target.dispatchTouchEvent(ev);

正常情况下,即我们上例整个代码的流程我们已经走完了:

1、ACTION_DOWN中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则找到包含当前x,y坐标的子View,赋值给mMotionTarget,然后调用
mMotionTarget.dispatchTouchEvent(ev)

2、ACTION_MOVE中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)

3、ACTION_UP中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)

当然了,在分发之前都会修改下坐标系统,把当前的x,y分别减去child.leftchild.top ,然后传给child;

三.关于拦截

1.如何拦截

上面的总结都是基于:如果没有拦截;那么如何拦截呢?

复写ViewGrouponInterceptTouchEvent方法:

@Override
	public boolean onInterceptTouchEvent(MotionEvent ev)
	{
		int action = ev.getAction();
		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			//如果你觉得需要拦截
			return true ; 
		case MotionEvent.ACTION_MOVE:
			//如果你觉得需要拦截
			return true ; 
		case MotionEvent.ACTION_UP:
			//如果你觉得需要拦截
			return true ; 
		}
		
		return false;
	}

默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这样该事件就不会往子View传递了,并且如果你在DOWN事件 retrun true ,则DOWNMOVEUPView都不会捕获事件;如果你在MOVE事件 return true , 则子View在MOVEUP都不会捕获事件。

原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ;

2.如何不被拦截

如果ViewGrouponInterceptTouchEvent(ev)ACTION_MOVEreturn true ,即拦截了子ViewMOVE以及UP事件;

此时子View希望依然能够响应MOVEUP时该咋办呢?

Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子ViewdispatchTouchEvent中直接这么写:

@Override
	public boolean dispatchTouchEvent(MotionEvent event)
	{
		getParent().requestDisallowInterceptTouchEvent(true);  
		int action = event.getAction();
 
		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
			break;
		case MotionEvent.ACTION_MOVE:
			Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
			break;
		case MotionEvent.ACTION_UP:
			Log.e(TAG, "dispatchTouchEvent ACTION_UP");
			break;
 
		default:
			break;
		}
		return super.dispatchTouchEvent(event);
	}

getParent().requestDisallowInterceptTouchEvent(true); 这样即使ViewGroupMOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。

从源码也可以解释:

ViewGroup MOVEUP拦截的源码是这样的:

if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            if (!target.dispatchTouchEvent(ev)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

当我们把disallowIntercept设置为true时,!disallowIntercept直接为false,于是拦截的方法体就被跳过了~

注:如果ViewGrouponInterceptTouchEvent(ev) ACTION_DOWN里面直接return true了,那么子View是木有办法的捕获事件的~~~

3、如果没有找到合适的子View

我们的实例,直接点击ViewGroup内的按钮,当然直接很顺利的走完整个流程;

但是有两种特殊情况

1、ACTION_DOWN的时候,子View.dispatchTouchEvent(ev)返回的为false ;

如果你仔细看了,你会注意到ViewGroupdispatchTouchEvent(ev)ACTION_DOWN代码是这样的

 if (child.dispatchTouchEvent(ev))  {
        // Event handled, we have a target now.
        mMotionTarget = child;
        return true;
 }

只有在child.dispatchTouchEvent(ev)返回true了,才会认为找到了能够处理当前事件的View,即mMotionTarget = child;

但是如果返回false,那么mMotionTarget 依然是null

mMotionTargetnull会咋样呢?

其实ViewGroup也是View的子类,如果没有找到能够处理该事件的子View,或者干脆就没有子View

那么,它作为一个View,就相当于View的事件转发了~~直接super.dispatchTouchEvent(ev);

源码是这样的:

  final View target = mMotionTarget;
            if (target == null) {
                // We don't have a target, this means we're handling the
                // event as a regular view.
                ev.setLocation(xf, yf);
                if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                    ev.setAction(MotionEvent.ACTION_CANCEL);
                    mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                }
                return super.dispatchTouchEvent(ev);
            }

我们没有一个能够处理该事件的目标元素,意味着我们需要自己处理~~~就相当于传统的View~

2、那么什么时候子View.dispatchTouchEvent(ev)返回的为true

如果你仔细看了上篇博客,你会发现只要子View支持点击或者长按事件一定返回true~~

源码是这样的:

if(((viewFlags &CLICKABLE)==CLICKABLE ||(viewFlags &LONG_CLICKABLE)==LONG_CLICKABLE)){
            return true;
  }

四、总结

关于代码流程上面已经总结过了~

1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;

2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法;

3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true) 阻止ViewGroup对其MOVE或者UP事件进行拦截;

在这里插入图片描述
现在整个ViewGroup的事件分发流程的分析也就到此结束了,我们最后再来简单梳理一下吧。

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

2.在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false;

3.子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值