上篇Android事件分发机制(一)我们已经讲述了View的dispatchTouchEvent方法,今天我们来看一下ViewGroup的事件传递。先来看下什么是ViewGroup,ViewGroup是一组View的集合,它可以包含子View或者子ViewGroup,我们常用的一些布局都是继承自ViewGroup,当然他也是View的子类,只不过是可以包含子View和子ViewGroup。下面我们自定一个布局来看一下ViewGroup的传递机制。
public class MyLayout extends LinearLayout { public MyLayout(Context context, AttributeSet attrs) { super(context, attrs); } }
然后引入我们的布局:
<com.example.viewgrouptouchevent.MyLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/my_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.viewgrouptouchevent.MyLayout>
在activity中我们给两个button和布局添加监听:
myLayout.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("TAG", "myLayout on touch"); return false; } }); button1.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "You clicked button1"); } }); button2.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "You clicked button2"); } });
我们来分别点击btn1,btn2,空白区域打印结果如下:
通过结果看到,点击按钮的时候不会触发父布局的touch方法,难道是从btn分发到父布局的?查阅文档,看到ViewGroup除了dispatchTouchEvent方法还有一个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) { return false; }
注释比较多,方法却很简单,直接返回了一个false。那我们在自定义布局里面重写一下这个方法,让他返回true试一下
public class MyLayout extends LinearLayout { public MyLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true; } }
打印结果:
点击btn不会触发点击动作了,只会触发父布局的touch方法,那如果是从View到ViewGroup,那么父布局是怎么拦截的呢?我们来看一下ViewGroup的dispatchTouchEvent方法
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; if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { mMotionTarget = null; } if (disallowIntercept || !onInterceptTouchEvent(ev)) { ev.setAction(MotionEvent.ACTION_DOWN); final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; 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); 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; if (child.dispatchTouchEvent(ev)) { mMotionTarget = child; return true; } } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } final View target = mMotionTarget; 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); } 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)) { } mMotionTarget = null; return true; } if (isUpOrCancel) { mMotionTarget = null; } final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; mMotionTarget = null; } return target.dispatchTouchEvent(ev); }
ViewGroup的dispatchTouchEvent方法要比View的dispatchTouchEvent方法复杂一些,我们还是来看重点,13行的判断条件,第一个是disallowIntercept,这个意思是禁止事件拦截,默认是false,可以通过requestDisallowInterceptTouchEvent方法对这个值进行修改,那么只要是onInterceptTouchEvent返回true就会进入下面的方法,我们上面说道onInterceptTouchEvent默认是返回false的呀,仔细看下你会发现,判断条件里是取反的!也就是返回false正好能进入。也就是可以理解为返回true代表拦截事件,false返回不拦截。
再来看进入以后的方法,进入以后呢就是对ViewGroup内的View进行遍历,直到找到被点击的对象,然后调用他的dispatchTouchEvent方法。接着就是我们上一篇的内容了,返回true就不会执行后面的代码了,可见我们重写onInterceptTouchEvent返回true就不会进入13行的判断了。如果点击空白区域,就不会在31行返回true,看44行判断target,一般都会返回null,那么继续执行super.dispatchTouchEvent(ev); 也就是调用父类View的dispatchTouchEvent(ev)了,因此我们重写的父布局的touch方法得到执行。
我们借用一张流程图来看下整个事件的传递过程:
总结一下:
Android 的事件传递机制是从ViewGroup往下传到View的,如果ViewGroup对事件进行了拦截,也就是onInterceptTouchEvent方法返回了true,事件就不会往下传递,由ViewGroup本身消费。
子View如果消费了事件,那么ViewGroup不会接收到任何事件。