上一篇咱们写过View的事件分发机制,若是你对这还不了解的能够看这一篇文章:android
https://my.oschina.net/quguangle/blog/793903ide
那么今天咱们将继续上次未完成的话题,从源码的角度分析ViewGroup的事件分发。首先咱们来探讨一下,什么是ViewGroup?它和普通的View有什么区别?源码分析
顾名思义,ViewGroup就是一组View的集合,它包含不少的子View和子VewGroup,是Android中全部布局的父类或间接父类,像LinearLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了能够包含子View和定义布局参数的功能。ViewGroup继承结构示意图以下所示:布局
能够看到,咱们平时项目里常常用到的各类布局,全都属于ViewGroup的子类。this
下面直接上案例:spa
package qu.com.handlerthread; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; /** * Created by quguangle on 2016/11/25. */ public class MyLinearLayout extends LinearLayout{ private static final String TAG = MyLinearLayout.class.getSimpleName(); public MyLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.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(ev); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "onTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onTouchEvent ACTION_UP"); break; default: break; } return super.onTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onInterceptTouchEvent ACTION_UP"); break; default: break; } return super.onInterceptTouchEvent(ev); } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { Log.e(TAG, "requestDisallowInterceptTouchEvent "); super.requestDisallowInterceptTouchEvent(disallowIntercept); } }
代码依然的仍是那么的简单,重写一些相关的方法。.net
而后看咱们的布局文件:日志
<?xml version="1.0" encoding="utf-8"?> <qu.com.handlerthread.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <qu.com.handlerthread.MyButton android:id="@+id/btnTest" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button" android:onClick="btnTest"/> </qu.com.handlerthread.MyLinearLayout>
Activitycode
public class MainActivity extends AppCompatActivity { private static final String TAG = "MyButton"; private Button btnTest; private LinearLayout MyLinearLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnTest = (Button) findViewById(R.id.btnTest); btnTest.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { int action = motionEvent.getAction(); switch (action){ case MotionEvent.ACTION_DOWN: Log.e(TAG,"onTouch----ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG,"onTouch----ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG,"onTouch----ACTION_UP"); break; default: break; } return true; } }); btnTest.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Log.e(TAG,"onClick----"); } }); } }
布局文件也很简单,自定义MyLinearLayout 中放了一个以前用过的自定义MyButton,而后运行项目,我在点击Button时任然Move下,否则不会出现ACTION_MOVE,看打印Log日志:orm
从打印的日志来看,大致上事件的流程为:MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent
咱们如今换一种方式:Activity
public class MainActivity extends AppCompatActivity { private static final String TAG = "MyButton"; private Button btnTest; private LinearLayout MyLinearLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnTest = (Button) findViewById(R.id.btnTest); MyLinearLayout.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { int action = motionEvent.getAction(); switch (action){ case MotionEvent.ACTION_DOWN: Log.e(TAG,"onTouch----ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG,"onTouch----ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG,"onTouch----ACTION_UP"); break; default: break; } return false; } }); btnTest.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Log.e(TAG,"onClick----"); } }); } }
当咱们点击Button时,打印状况:
咱们的MyLinearLayout的onTouch方法并无执行。
而当咱们点击空白区域时又执行了此方法,打印状况:
Oh My Good!你能够先理解成Button的onClick方法将事件消费掉了,所以事件不会再继续向下传递。那就说明Android中的touch事件是先传递到View,再传递到ViewGroup的,这不跟咱们上面所说的相矛盾,难道真的是这样吗?
咱们从源码中找真相:
ViewGroup - dispatchTouchEvent
2.1首先是ViewGroup的dispatchTouchEvent----ACTION_DOWN
@Override 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) { 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; } // If we're disallowing intercept or if we're allowing and we didn't // intercept if (disallowIntercept || !onInterceptTouchEvent(ev)) { // reset this event's action (just to protect ourselves) ev.setAction(MotionEvent.ACTION_DOWN); // We know we want to dispatch the event down, find a child // who can handle it, start with the front-most child. 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)) { // offset the event to the view's coordinate system 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)) { // Event handled, we have a target now. mMotionTarget = child; return true; } // The event didn't get handled, try the next view. // Don't reset the event's location, it's not // necessary here. } } } } } ....//other code omitted
因为dispatchTouchEvent方法中代码比较多,所以咱们首先分析ACTION_DOWN这部分。
1.进入ACTION_DOWN的处理。
2.将mMotionTarget置为null。
3.进行判断:if(disallowIntercept || !onInterceptTouchEvent(ev))根据判断条件,咱们能够将他分为2中可能
特别提醒的是:disallowIntercept 能够经过viewGroup.requestDisallowInterceptTouchEvent(boolean);进行设置,后面会详细说;而onInterceptTouchEvent(ev)能够进行复写。
注意:若是说咱们在这里使onInterceptTouchEvent返回值为false,那么它就不会进入IF,那么咱们的button事件就会被屏蔽掉。
4.开始遍历全部的子View
5.获取当前触摸点X,Y的坐标,判断是否落入在子View上,若是是就直接执行child.dispatchTouchEvent(ev)方法,意味这就进入到咱们以前讲的View.dispatchTouchEvent(ev),不懂的能够看我前面所讲的,当child.dispatchTouchEvent(ev)返回值为true,就将mMotionTarget=child,而后返回true.
到此ACTION_DOWN源码结束了,可是并没玩,还记得前面咱们的疑问吗?
咱们已经知道,若是一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值一定是true。由5可知当child.dispatchTouchEvent(ev)返回true,那么就会直接进入到IF语句,而后返回true。后面的代码就不会在执行了。
总结:
也就是说ViewGroup捕捉了DOWN事件,若是代码中不作TOUCH事件拦截,则开始查找当前x,y是否在某个子View的区域内,若是在,则把事件分发下去。
2.2首先是ViewGroup的dispatchTouchEvent----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); }
一样咱们只看ACTION_MOVE代码:
1.把ACTION_DOWN时赋值的mMotionTarget,付给target 。
2.if (!disallowIntercept && onInterceptTouchEvent(ev)) 当前容许拦截且拦截了,才进入IF体,固然了默认是不会拦截的~这里执行了onInterceptTouchEvent(ev)。
3.把坐标系统转化为子View的坐标系统。
4.直接return target.dispatchTouchEvent(ev); 能够看到,正常流程下,ACTION_MOVE在检测完是否拦截之后,直接调用了子View.dispatchTouchEvent,事件分发下去;最后就是ACTION_UP了。
2.3首先是ViewGroup的dispatchTouchEvent----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); }
1.判断当前是不是ACTION_UP
2.分别重置拦截标志位以及将DOWN赋值的mMotionTarget置为null,都UP了,固然置为null,下一次DOWN还会再赋值的~最后,修改坐标系统,而后调用target.dispatchTouchEvent(ev);
如今整个ViewGroup的事件分发流程的分析也就到此结束了,咱们最后再来简单梳理一下吧:
ACTION_UP中,ViewGroup捕获到事件,而后判断是否拦截,若是没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)固然了在分发以前都会修改下坐标系统,把当前的x,y分别减去child.left 和 child.top ,而后传给child;
3.1如何拦截
上面的总结都是基于:若是没有拦截;那么如何拦截呢?
复写ViewGroup的onInterceptTouchEvent方法:
@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 ,则DOWN,MOVE,UP子View都不会捕获事件;若是你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。
缘由很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ;
3.2如何不被拦截
若是ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;
此时子View但愿依然可以响应MOVE和UP时该咋办呢?
android给咱们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否容许拦截,咱们在子View的dispatchTouchEvent中直接这么写:
@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); 这样即便ViewGroup在MOVE的时候return true,子View依然能够捕获到MOVE以及UP事件。
ViewGroup MOVE和UP拦截的源码是这样的:
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,因而拦截的方法体就被跳过了
注:若是ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN里面直接return true了,那么子View是木有办法的捕获事件的
咱们的实例,直接点击ViewGroup内的按钮,固然直接很顺利的走完整个流程;
可是有两种特殊状况
一、ACTION_DOWN的时候,子View.dispatchTouchEvent(ev)返回的为false ;
若是你仔细看了,你会注意到ViewGroup的dispatchTouchEvent(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
mMotionTarget 为null会咋样呢?
其实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~
二、那么何时子View.dispatchTouchEvent(ev)返回的为true
若是你仔细看了上篇博客,你会发现只要子View支持点击或者长按事件必定返回true~~
源码是这样的:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { return true ; }
一、若是ViewGroup找到了可以处理该事件的View,则直接交给子View处理,本身的onTouchEvent不会被触发;
二、能够经过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给本身处理,则会执行本身对应的onTouchEvent方法
三、子View能够经过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;
好了,那么实际应用中能解决哪些问题呢?好比你须要写一个相似slidingmenu的左侧隐藏menu,主Activity上有个Button、ListView或者任何能够响应点击的View,你在当前View上死命的滑动,菜单栏也出不来;由于MOVE事件被子View处理了~ 你须要这么作:在ViewGroup的dispatchTouchEvent中判断用户是否是想显示菜单,若是是,则在onInterceptTouchEvent(ev)拦截子View的事件;本身进行处理,这样本身的onTouchEvent就能够顺利展示出菜单栏了~~
参考文章:http://blog.csdn.net/lmj623565791/article/details/39102591