在自定义ViewGroup中,有时候需要实现触摸事件拦截,比如ListView下拉刷新就是典型的触摸事件拦截的例子。触摸事件拦截就是在触摸事件被parent view拦截,而不会分发给其child,即使触摸发生在该child身上。被拦截的事件会转到parent view的onTouchEvent方法中进行处理。但是这个交互过程还是挺复杂的,有多种情况,今天我们就来分析一下吧。这篇分析文章已经放了一段时间了,如果有任何问题请高人指出。
ViewGroup对于事件的拦截是一个复杂的流程,如果你想对触摸事件进行拦截,那么你需要覆写onInterceptTouchEvent方法,并且返回true。然后后续的事件就会被转移到该ViewGroup的onTouchEvent方法进行处理,而在后续的事件处理过程中onInterceptTouchEvent中也不会收到后续事件,因此你也需要覆写onTouchEvent方法。我们首先看看onInterceptTouchEvent方法的官方说明 :
public boolean onInterceptTouchEvent (MotionEvent ev) 实现这个方法来拦截所有触摸事件。这会使得您可以监控到所有分发到你的子视图的事件,然后您可以随时控制当前的手势。
使用这个方法您需要花些精力,因为它与View.onTouchEvent(MotionEvent)的交互非常复杂,并且要想使用这个功能还需要把当前ViewGroup的onTouchEvent方法和子控件的onTouchEvent方法正确地结合在一起使用。事件获取顺序如下:
你将从这里开始接收ACTION_DOWN触摸事件。
ACTION_DOWN触摸事件可以由该ViewGroup自己处理,也可以由它的子控件的onTouchEvent进行处理;这就意味着你需要实现onTouchEvent(MotionEvent)方法并且返回true,这样你才可以接收到后续的事件(以免会继续寻找父控件进行处理)。如果你在onTouchEvent(MotionEvent)返回了true,那么在onInterceptTouchEvent()方法中您将不会再收到后续的事件,所有这些后续的事件(例如您在ACTION_DOWN中返回了true,那么ACTION_MOVE, ACTION_UP这些成为后续事件)将会被本类的onTouchEvent(MotionEvent)方法中被处理。
************
只要您在onInterceptTouchEvent方法中返回false,每个后续的事件(从当前事件到最后ACTION_UP事件)将会先分发到onInterceptTouchEvent中,然后再交给目标子控件的onTouchEvent处理 (前提是子控件的onTouchEvent返回是true )。
如果在onInterceptTouchEvent返回true,onInterceptTouchEvent方法中将不会收到后续的任何事件,目标子控件中除了ACTION_CANCEL外也不会接收所有这些后续事件,所有的后续事件将会被交付到你自己的onTouchEvent()方法中。
************
import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import android.widget.Scroller;
public class TouchLayout extends FrameLayout {
private String TAG = TouchLayout.class.getSimpleName();
public TouchLayout(Context context) {
super(context);
}
public TouchLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TouchLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// setClickable(true);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = super.dispatchTouchEvent(ev) ;
return result;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//
final int action = MotionEventCompat.getActionMasked(ev);
// Always handle the case of the touch gesture being complete.
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Do not intercept touch event, let the child handle it
return false;
}
TouchUtils.showEventInfo(TAG + "# onInterceptTouchEvent", action);
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
TouchUtils.showEventInfo(TAG + "# *** onTouchEvent", ev.getAction());
Log.d(TAG, "### is Clickable = " + isClickable());
return super.onTouchEvent(ev);
// return true;
}
}
TouchTv ( View 类型)
public class TouchTv extends TextView {
private String TAG = TouchTv.class.getSimpleName();
public TouchTv(Context context) {
this(context, null);
}
public TouchTv(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TouchTv(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// setClickable(true);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
TouchUtils.showEventInfo(TAG + "#dispatchTouchEvent", ev.getAction());
boolean result = super.dispatchTouchEvent(ev);
Log.d(TAG, "### dispatchTouchEvent result = " + result);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
TouchUtils.showEventInfo(TAG + "#onTouchEvent", ev.getAction());
boolean result = super.onTouchEvent(ev);
Log.d(TAG, "### onTouchEvent result = " + result);
return result;
}
}
Activity :
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.touch_event_intercept);
View myView = findViewById(R.id.my_button);
ValueAnimator colorAnim = ObjectAnimator.ofInt(myView,
"backgroundColor", /* Red */
0xFFFF8080, /* Blue */0xFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(myView, "scaleX",
0.5f);
objectAnimator.setDuration(3000);
objectAnimator.setRepeatMode(ObjectAnimator.REVERSE);
objectAnimator.start();
Log.d("", "### Activiti中getWindow()获取的类型是 : " + this.getWindow());
// state list
StateListDrawable stateListDrawable = new StateListDrawable();
stateListDrawable.addState(new int[] {
android.R.attr.state_enabled
}, getResources().getDrawable(R.drawable.ic_launcher));
stateListDrawable.addState(new int[] {
android.R.attr.state_pressed
}, getResources().getDrawable(R.drawable.ic_launcher));
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// Log.d("", "### activity dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
TouchUtils.showEventInfo("activity onTouchEvent", event.getAction());
return super.onTouchEvent(event);
}
}
touch_event_intercept.xml :
// 事件拦截
10-01 20:22:52.892: D/TouchLayout# onInterceptTouchEvent(407): ### action --> ACTION_DOWN
// 处理
10-01 20:22:52.892: D/TouchLayout# *** onTouchEvent(407): ### action --> ACTION_DOWN
// DOWN的后续事件不经过onInterceptTouchEvent,直接交给的onTouchEvent处理
10-01 20:22:52.917: D/TouchLayout# *** onTouchEvent(407): ### action --> ACTION_MOVE
10-01 20:22:52.937: D/TouchLayout# *** onTouchEvent(407): ### action --> ACTION_MOVE
10-01 20:22:52.957: D/TouchLayout# *** onTouchEvent(407): ### action --> ACTION_MOVE
10-01 20:22:52.977: D/TouchLayout# *** onTouchEvent(407): ### action --> ACTION_MOVE
10-01 20:22:52.977: D/TouchLayout# *** onTouchEvent(407): ### action --> ACTION_MOVE
10-01 20:22:52.997: D/TouchLayout# *** onTouchEvent(407): ### action --> ACTION_MOVE
10-01 20:22:53.017: D/TouchLayout# *** onTouchEvent(407): ### action --> ACTION_MOVE
10-01 20:22:53.017: D/TouchLayout# *** onTouchEvent(407): ### action --> ACTION_UP
// DOWN中没有对事件进行拦截,因此可以被TouchTv进行处理
10-01 20:32:05.017: D/TouchLayout# onInterceptTouchEvent(573): ### action --> ACTION_DOWN
// TouchTv事件分发
10-01 20:32:05.017: D/TouchTv#dispatchTouchEvent(573): ### action --> ACTION_DOWN
// TouchTv对事件进行处理,TouchTv的onTouchEvent返回false,导致事件交给TouchLayout的onTouchEvent处理
10-01 20:32:05.017: D/TouchTv#onTouchEvent(573): ### action --> ACTION_DOWN
// TouchLayout的onTouchEvent处理DOWN事件
10-01 20:32:05.017: D/TouchLayout# *** onTouchEvent(573): ### action --> ACTION_DOWN
// TouchLayout的onTouchEvent处理后续事件
10-01 20:32:05.062: D/TouchLayout# *** onTouchEvent(573): ### action --> ACTION_MOVE
10-01 20:32:05.082: D/TouchLayout# *** onTouchEvent(573): ### action --> ACTION_MOVE
10-01 20:32:05.267: D/TouchLayout# *** onTouchEvent(573): ### action --> ACTION_MOVE
10-01 20:32:05.287: D/TouchLayout# *** onTouchEvent(573): ### action --> ACTION_MOVE
10-01 20:32:05.307: D/TouchLayout# *** onTouchEvent(573): ### action --> ACTION_MOVE
10-01 20:32:05.307: D/TouchLayout# *** onTouchEvent(573): ### action --> ACTION_MOVE
10-01 20:32:05.312: D/TouchLayout# *** onTouchEvent(573): ### action --> ACTION_UP
// 事件拦截onInterceptTouchEvent
10-01 20:16:03.617: D/TouchLayout# onInterceptTouchEvent(32675): ### action --> ACTION_DOWN
// 事件处理onTouchEvent
10-01 20:16:03.617: D/TouchLayout# *** onTouchEvent(32675): ### action --> ACTION_DOWN
// TouchLayout的dispatchTouchEvent最终返回了false,
10-01 20:16:03.617: D/TouchLayout(32675): ### dispatchTouchEvent, return false
// 事件没有被处理,最终交给了Activity的onTouchEvent处理
10-01 20:16:03.617: D/activity onTouchEvent(32675): ### action --> ACTION_DOWN
10-01 20:16:03.697: D/activity onTouchEvent(32675): ### action --> ACTION_MOVE
10-01 20:16:03.712: D/activity onTouchEvent(32675): ### action --> ACTION_MOVE
10-01 20:16:03.732: D/activity onTouchEvent(32675): ### action --> ACTION_MOVE
10-01 20:16:03.882: D/activity onTouchEvent(32675): ### action --> ACTION_MOVE
10-01 20:16:03.897: D/activity onTouchEvent(32675): ### action --> ACTION_MOVE
10-01 20:16:03.917: D/activity onTouchEvent(32675): ### action --> ACTION_UP
// TouchLayout不对事件进行拦截
10-01 20:43:04.682: D/TouchLayout# onInterceptTouchEvent(814): ### action --> ACTION_DOWN
// 事件被TouchTv分发
10-01 20:43:04.682: D/TouchTv#dispatchTouchEvent(814): ### action --> ACTION_DOWN
// 事件被TouchTv处理
10-01 20:43:04.682: D/TouchTv#onTouchEvent(814): ### action --> ACTION_DOWN
// 事件被TouchTv的处理结果为false,因此该事件需要找parent来处理
10-01 20:43:04.682: D/TouchTv(814): ### dispatchTouchEvent result = false
// 事件被交给TouchTv的parent的onTouchEvent处理,即TouchLayout的onTouchEvent,该方法返回true
// 因此后续事件继续交给TouchLayout处理
10-01 20:43:04.682: D/TouchLayout# *** onTouchEvent(814): ### action --> ACTION_DOWN
10-01 20:43:04.727: D/TouchLayout# *** onTouchEvent(814): ### action --> ACTION_MOVE
10-01 20:43:04.747: D/TouchLayout# *** onTouchEvent(814): ### action --> ACTION_MOVE
10-01 20:43:04.872: D/TouchLayout# *** onTouchEvent(814): ### action --> ACTION_MOVE
10-01 20:43:04.892: D/TouchLayout# *** onTouchEvent(814): ### action --> ACTION_MOVE
10-01 20:43:04.892: D/TouchLayout# *** onTouchEvent(814): ### action --> ACTION_UP
// TouchLayout不拦截事件,因此事件分发给TouchTv进行处理,而TouchTv的处理结果为true,因此后续的事件将会先从
// TouchLayout的onInterceptTouchEvent再到TouchTv的onTouchEvent
10-01 20:48:49.612: D/TouchLayout# onInterceptTouchEvent(1030): ### action --> ACTION_DOWN
// TouchTv处理事件
10-01 20:48:49.612: D/TouchTv#dispatchTouchEvent(1030): ### action --> ACTION_DOWN
10-01 20:48:49.612: D/TouchTv#onTouchEvent(1030): ### action --> ACTION_DOWN
10-01 20:48:49.612: D/TouchTv(1030): ### dispatchTouchEvent result = true
// 后续事件从TouchLayout的onInterceptTouchEvent再到TouchTv的onTouchEvent
10-01 20:48:49.697: D/TouchLayout# onInterceptTouchEvent(1030): ### action --> ACTION_MOVE
10-01 20:48:49.697: D/TouchTv#dispatchTouchEvent(1030): ### action --> ACTION_MOVE
10-01 20:48:49.697: D/TouchTv#onTouchEvent(1030): ### action --> ACTION_MOVE
10-01 20:48:49.697: D/TouchTv(1030): ### dispatchTouchEvent result = true
10-01 20:48:49.717: D/TouchLayout# onInterceptTouchEvent(1030): ### action --> ACTION_MOVE
10-01 20:48:49.717: D/TouchTv#dispatchTouchEvent(1030): ### action --> ACTION_MOVE
10-01 20:48:49.717: D/TouchTv#onTouchEvent(1030): ### action --> ACTION_MOVE
10-01 20:48:49.717: D/TouchTv(1030): ### dispatchTouchEvent result = true
// UP事件直接在TouchTv中进行分发
10-01 20:48:49.782: D/TouchTv#dispatchTouchEvent(1030): ### action --> ACTION_UP
10-01 20:48:49.782: D/TouchTv#onTouchEvent(1030): ### action --> ACTION_UP
10-01 20:48:49.782: D/TouchTv(1030): ### dispatchTouchEvent result = true
// TouchLayout不对DOWN进行拦截
10-01 20:56:37.642: D/TouchLayout# onInterceptTouchEvent(1205): ### action --> ACTION_DOWN
// TouchTv分发与处理DOWN事件
10-01 20:56:37.642: D/TouchTv#dispatchTouchEvent(1205): ### action --> ACTION_DOWN
10-01 20:56:37.642: D/TouchTv#onTouchEvent(1205): ### action --> ACTION_DOWN
10-01 20:56:37.642: D/TouchTv(1205): ### dispatchTouchEvent result = true
// TouchLayout对MOVE事件进行拦截
10-01 20:56:37.712: D/TouchLayout# onInterceptTouchEvent(1205): ### action --> ACTION_MOVE
// TouchTv收到一个CANCEL事件,然后不会不到MOVE以及后续的事件
10-01 20:56:37.712: D/TouchTv#dispatchTouchEvent(1205): ### action --> ACTION_CANCEL
10-01 20:56:37.712: D/TouchTv#onTouchEvent(1205): ### action --> ACTION_CANCEL
10-01 20:56:37.712: D/TouchTv(1205): ### dispatchTouchEvent result = true
// MOVE以及后续事件被TouchLayout处理
10-01 20:56:37.727: D/TouchLayout# *** onTouchEvent(1205): ### action --> ACTION_MOVE
10-01 20:56:37.747: D/TouchLayout# *** onTouchEvent(1205): ### action --> ACTION_MOVE
10-01 20:56:37.762: D/TouchLayout# *** onTouchEvent(1205): ### action --> ACTION_MOVE
10-01 20:56:37.777: D/TouchLayout# *** onTouchEvent(1205): ### action --> ACTION_MOVE
10-01 20:56:37.797: D/TouchLayout# *** onTouchEvent(1205): ### action --> ACTION_MOVE
10-01 20:56:37.997: D/TouchLayout# *** onTouchEvent(1205): ### action --> ACTION_MOVE
10-01 20:56:38.012: D/TouchLayout# *** onTouchEvent(1205): ### action --> ACTION_MOVE
10-01 20:56:38.017: D/TouchLayout# *** onTouchEvent(1205): ### action --> ACTION_UP
/**
* {@inheritDoc}
*/
@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;
// 是否禁用拦截,如果为true表示不能拦截事件;反之,则为可以拦截事件
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// ACTION_DOWN事件,即按下事件
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept。如果不允许事件拦截或者不拦截该事件,那么执行下面的操作
if (disallowIntercept || !onInterceptTouchEvent(ev)) // 1、是否禁用拦截、是否拦截事件的判断
// 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--) // 2、迭代所有子view,查找触摸事件在哪个子view的坐标范围内
final View child = children[i];
// 该child是可见的
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
// 3、获取child的坐标范围
child.getHitRect(frame);
// 4、判断发生该事件坐标是否在该child坐标范围内
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;
// 5、child处理该事件,如果返回true,那么mMotionTarget为该child。正常情况下,
// dispatchTouchEvent(ev)的返回值即onTouchEcent的返回值。因此onTouchEcent如果返回为true,
// 那么mMotionTarget为触摸事件所在位置的child。
if (child.dispatchTouchEvent(ev))
// 6、 mMotionTarget为该child
mMotionTarget = child;
return true;
}
}
}
}
}
}// end if
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
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// 触摸事件的目标view, 即触摸所在的view
final View target = mMotionTarget;
// 7、如果mMotionTarget为空,那么执行super.super.dispatchTouchEvent(ev),
// 即View.dispatchTouchEvent(ev),就是该View Group自己处理该touch事件,只是又走了一遍View的分发过程而已.
// 拦截事件或者在不拦截事件且target view的onTouchEvent返回false的情况都会执行到这一步.
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);
}
// 8、如果没有禁用事件拦截,并且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;
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;
}
// 9、事件不拦截,且target view在ACTION_DOWN时返回true,那么后续事件由target来处理事件
return target.dispatchTouchEvent(ev);
}
如果不对事件进来拦截,且TouchTv对事件的处理返回true,那么在DOWN事件时,mMotionTarget就是TouchTv,后续的事件就会通过注释9来处理,即直接交给TouvhTv来处理。如果在DOWN时就拦截事件,那么mMotionTarget为空,则会执行注释7出的代码,一直调用super.dispatchTouchEvent处理事件,即调用本类的事件处理,最终会调用onTouchEvent方法。如果在DOWN时不拦截,MOVE时拦截,那么会引发注释8的代码,target view收到一个cancel事件,且mMotionTarget被置空,后续事件在注释7出的代理进行处理,即在自己的onTouchEvent中进行处理。