在自定义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 :
<myview.TouchLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" tools:context="com.example.touch_event.MainActivity" tools:ignore="MergeRootFrame" > <myview.TouchTv android:id="@+id/my_button" android:layout_width="match_parent" android:layout_height="160dp" android:layout_gravity="center" android:layout_margin="20dp" android:background="#00aa00" android:gravity="center" android:text="@string/hello_world" android:textSize="30sp" /> </myview.TouchLayout>
// 事件拦截 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中进行处理。