前言:ViewGroupe的事件分发是一个重点也是一个痛点,许多地方都会用到,尤其是滑动冲突时的解决,下面通过一个实例并结合源码来分析ViewGroupe的事件分发。前提是有一定的View事件分发基础,不熟悉的可以看我之前写的View的事件分发。
自定义的Button:
package com.zjt.touch;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
public class MyButton extends Button {
private final static String TAG = "MyButton";
public MyButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//解决父类拦截事件的调用方法
//getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction()) {
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;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
// return false;
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);
}
}
自定义的TextView:
package com.zjt.touch;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;
import com.zjt.view.TestView;
/**
* Created by hc on 2017/1/23.
*/
public class MyTextView extends TextView {
private final static String TAG = "MyTextView";
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
}
return super.onTouchEvent(event);
}
}
自定义的LinearLayout:
package com.zjt.touch;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
/**
* Created by hc on 2017/1/23.
*/
public class MyLinearLayout extends LinearLayout {
private final static String TAG = "MyLinearLayout";
public MyLinearLayout(Context context) {
super(context);
}
public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
// return true;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
break;
//return false;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
// return false;
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
// return true;
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
}
return super.onTouchEvent(event);
}
}
上面的自定义view都很简单,增加了日志打印而已。
MainActivity如下:
package com.zjt.touch;
import com.example.testactivity2.R;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;
public class TestViewTouchActivity extends Activity implements OnClickListener{
private final static String TAG = "TestViewTouchActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.view_touch_layout);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_touch:
Log.e(TAG, "btn click......");
break;
default:
break;
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
}
return super.onTouchEvent(event);
}
}
当我们点击button的时候,log如下:
点击TextView的时候,log如下:
点击空白地方的log如下:
为什么会出现如上的结果呢,我们通过ViewGroupe的源码来分析,ViewGroupe的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;
/**
* disallowIntercept表示是否允许事件拦截,默认是false,即不拦截事件
* 此值可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法进行设置
*/
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
/**
* 因为从ACTION_DOWN开始要开启新一轮的事件分发,所有要将mMotionTarget(目标)置为空
*/
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
/**
* 当disallowIntercept(默认是false)为true,或者onInterceptTouchEvent(ev)(默认返回为false)方法的返回值
* 为false,取反则为true,则判断条件成立
*/
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;
/**
* 遍历当前ViewGroup的所有子View
*/
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
/**
* 如果当前的View是VISIBLE的或者有动画执行
*/
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
/**
* 如果子View包含当前触摸的点
*/
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.
/**
* 如果子View的dispatchTouchEvent方法的返回值为true,则表示子View已经消费了事件
* 此时将子View赋值给mMotionTarget
*/
mMotionTarget = child;
/**
* 直接返回true,表示down事件被消费掉了
*/
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
/**
* 如果是ACTION_UP或者ACTION_CANCEL, 将disallowIntercept设置为默认的false
* 因为ACTION_UP或者ACTION_CANCEL表示事件执行完,要将前面设置的值复位
*/
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
/**
* 当mMotionTarget为空表示没有找到消费事件的View,此时需要调用ViewGroup父类的dispatchTouchEvent方法,
* ViewGroup的父类即为View
*/
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);
}
// if have a target, see if we're allowed to and want to intercept its
// events
/**
* 如果执行到此说明target!=null,然后判断是否允许拦截和是否想要拦截
* 如果允许拦截(!disallowIntercept=true),并且想要拦截(onInterceptTouchEvent(ev)返回值为true)
* 则条件成立
*/
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
/**
* 设置动作为ACTION_CANCEL,此处与ev.getAction相对应
*/
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;
}
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);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
/**
* 将事件分发给target返回target.dispatchTouchEvent(ev)的返回值
*/
return target.dispatchTouchEvent(ev);
}
View事件分发是从Activity的DecorView一直往下传递,直到我们的Linearlayout再到我们手指触摸的view,比如我们代码中的Button、TextView以及Linearlayout的空白部分。
知道上面的原则,我们接着往下分析点击Button的Log,首先从Activity传递到我们的布局LinearLayout中,由于LinearLayout的父布局没有设置事件拦截,所以走到代码的61行:
61行的child就表示我们点击的button,button是View所以走View的dispatchTouchEvent,
由于button是可点击的所以消费了ACTION_DOWN事件,所以61行返回true,往下走,到67行:
mMotionTarget = child; //这里的child就是Button
然后在71行返回true,ACTION_DOWN事件执行完毕。
然后是 ACTION_MOVE:
97行:final View target = mMotionTarget;
102行的 target 不为空,120行没有拦截,直接走到165行–target.dispatchTouchEvent(ev); 即走到MyButton的onTouchEvent
ACTION_UP : 和ACTION_MOVE没有拦截的话基本一样就不分析了。
下面分析点击MyTextView的log,
走到61行 ,此时的 child 就是我们点击的 MyTextView了,由于 MyTextView是继承自 TextView ,所以 MyTextView默认也是不可点击的,所以, 61行的child.dispatchTouchEvent(ev)返回的是false,所以我们需要往上看往上看MyLinearLayout的dispatchTouchEvent()方法的61行, 由于由于TextView.dispatchTouchEvent()为false, 导致mMotionTarget没有被赋值,
那么就得继续往下走,走到97行:
final View target = mMotionTarget; //此时由于MyTextView没有消费down事件,所以mMotionTarget = null,所以导致 target 为 null,
所以会走到110行:return super.dispatchTouchEvent(ev); 这里调用的是MyLinearLayout父类View的dispatchTouchEvent()方法,由于MyLinearLayout没有设置onTouchListener,但是重写了onTouchEvent方法,但是由于MyLinearLayout既不是clickable的也是longClickable的,所以其onTouchEvent()方法false,所以dispatchTouchEvent()也是返回false。这也表示 ACTION_DOWN未被消费,从而不能继续执行下面的ACTION_MOVE, ACTION_UP等事件了,具体为什么,代码中没看出来,如果知道原因的小伙伴可以告诉我啊。
点击MyLinearLayout的空白区域和点击MyTextView情况类型,就不作过多分析啦,有问题就留言大家一起讨论。
现在我们看看事件的拦截,我们修改下MyLinearLayout中的onInterceptTouchEvent的方法,现将 ACTION_MOVE中return true,表示拦截move,以及move的后续事件(up等)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
break;
// return false;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
// return false;
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
return true;
// break;
}
return super.onInterceptTouchEvent(ev);
}
然后我们点击MyButton,我们先分析结果,等下再看看log,由于是点击button而且是在down事件之后的move中拦截事件的,所以MyButton的down事件会完整的走完,我们接着看move事件。
move :走到97行,对 target 进行赋值: final View target = mMotionTarget; mMotionTarget 就是在down事件中进行赋值的 MyButton。
走到120行:if (!disallowIntercept && onInterceptTouchEvent(ev)) ,disallowIntercept 默认是false的,由于在onInterceptTouchEvent的move中 返回true,所以 onInterceptTouchEvent(ev) = true,这个if语句成立,
走到127 行 :ev.setAction(MotionEvent.ACTION_CANCEL); 将action设为 ACTION_CANCEL。
132行:if (!target.dispatchTouchEvent(ev)),就调用MyButton的dispatchTouchEvent方法,由于127行,将将action设为 了ACTION_CANCEL,所以会调用 MyButton的dispatchTouchEvent方法中的ACTION_CANCEL,日志中会提现出来。
140行:mMotionTarget = null; 将 mMotionTarget 置为null。
144行:return true; 表示move事件一被消费掉。
接着看UP事件:
同样是走到97行为target赋值,final View target = mMotionTarget; 但此时的mMotionTarget在move事件中已被置为null,所以target也是null;
102行:if (target == null) ,成立,走到该条件语句的110行:return super.dispatchTouchEvent(ev);
即调用 MyLinearLayout的父类的dispatchTouchEvent方法,即View的dispatchTouchEvent,MyLinearLayout中没有设置onTouchListener,但是重写了 onTouchEvent方法,由于MyLinearLayout不是clickable以及longclickable所以 onTouchEvent 返回false,所以 dispatchTouchEvent也返回false,UP事件未被消费。
现在看看log,log如下:
log和我们的分析是一致的。
现在再看看我们在MyLinearLayout的onInterceptTouchEvent方法的down事件中,进行事件拦截,代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
break;
// return false;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
return true;
// break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
// return true;
break;
}
return super.onInterceptTouchEvent(ev);
}
我们还是先分析源码再提高log做验证。
DOWN事件:
由于在down事件进行了拦截,所以走到31行:if (disallowIntercept || !onInterceptTouchEvent(ev))中的onInterceptTouchEvent为true,所以该条件不成立。
102行:if (target == null)由于down事件未被消费,所以 target 为null,走到 110行:return super.dispatchTouchEvent(ev); 跟之前的分析一样,由于MyLinearLayout重写了onTouchEvent方法,由于MyLinearLayout不是clickable以及longclickable,所以 onTouchEvent 返回false,所以 dispatchTouchEvent也返回false,Down事件未被消费。
由于Down事件未被消费,所以接下来的MOVE和UP事件不执行了,这个问题上面有提到,我也不知原因。
log如下:
如果MyLinearLayout的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截子View的MOVE以及UP事件;
此时子View希望依然能够响应MOVE和UP时我们应该怎么办呢?
我们可以如下做,比如我们的MyButton中的代码:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//解决父类拦截事件的调用方法
getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction()) {
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;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
修改MyLinearLayout的onInterceptTouchEvent(ev)的方法,在move中拦截事件,
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
break;
// return false;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
// return true;
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
return true;
// break;
}
return super.onInterceptTouchEvent(ev);
}
结果代码就可以正常执行下去了,这个源码分析不难,就不做讲解了,log如下:
好了,文章就到这里了,有问题请留言,一起学习,一起进步。