ViewGroup事件分发机制
1.概述
上一篇我们写过View的事件分发机制,如果你对这还不了解的可以看这一篇文章:
https://my.oschina.net/quguangle/blog/793903
那么今天我们将继续上次未完成的话题,从源码的角度分析ViewGroup的事件分发。首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别?
顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是Android中所有布局的父类或间接父类,像LinearLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。ViewGroup继承结构示意图如下所示:
可以看到,我们平时项目里经常用到的各种布局,全都属于ViewGroup的子类。
下面直接上案例:
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);
}
}
代码依然的还是那么的简单,重写一些相关的方法。
然后看我们的布局文件:
<?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>
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);
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日志:
从打印的日志来看,大体上事件的流程为: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的,这不跟我们上面所说的相矛盾,难道真的是这样吗?
我们从源码中找真相:
2.源码分析
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 =true (默认为false)。
- 当前允许拦截但是不拦截,即disallowIntercept =false,但是onInterceptTouchEvent(ev)返回false
特别提醒的是: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_DOWN中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则找到包含当前x,y坐标的子View,赋值给mMotionTarget,然后调用 mMotionTarget.dispatchTouchEvent
- ACTION_MOVE中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)
-
ACTION_UP中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)当然了在分发之前都会修改下坐标系统,把当前的x,y分别减去child.left 和 child.top ,然后传给child;
3.关于拦截
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是木有办法的捕获事件的
3.3如果没有找到合适的子View
我们的实例,直接点击ViewGroup内的按钮,当然直接很顺利的走完整个流程;
但是有两种特殊情况
1、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~
2、那么什么时候子View.dispatchTouchEvent(ev)返回的为true
如果你仔细看了上篇博客,你会发现只要子View支持点击或者长按事件一定返回true~~
源码是这样的:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
return true ; }
4.总结
1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;
2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
3、子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
来源:oschina
链接:https://my.oschina.net/u/2832222/blog/794346