[Android进阶笔记]Android触摸事件的拦截机制

时间:2022-10-21 22:33:45

第一句话总是最重要的:
Android的拦截机制是一个自顶向下的事件分发与自底向上的事件响应机制
自顶向下的分发,就是我从View树的顶部开始向下分发事件
自底向上的响应,就是当事件传递到View树的底层,那么他就开始往上层层响应

View

View有2个方法 dispatchTouchEvent和onTouchEvent,源码如下:

3363        public boolean dispatchTouchEvent(MotionEvent event) {
3364 if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
3365 mOnTouchListener.onTouch(this, event)) {
3366 return true;
3367 }
3368 return onTouchEvent(event);
3369 }

根据我上边的话,可以这么理解
dispatchTouchEvent 是用来 事件分发的
onTouchEvent 是用来事件响应的

View的onTouchEvent源码

3792        public boolean onTouchEvent(MotionEvent event) {
3793 final int viewFlags = mViewFlags;
3794
3795 if ((viewFlags & ENABLED_MASK) == DISABLED) {
3796 // A disabled view that is clickable still consumes the touch
3797 // events, it just doesn't respond to them.
3798 return (((viewFlags & CLICKABLE) == CLICKABLE ||
3799 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
3800 }
3801
3802 if (mTouchDelegate != null) {
3803 if (mTouchDelegate.onTouchEvent(event)) {
3804 return true;
3805 }
3806 }
3807
3808 if (((viewFlags & CLICKABLE) == CLICKABLE ||
3809 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
3810 switch (event.getAction()) {
3811 case MotionEvent.ACTION_UP:
3812 if ((mPrivateFlags & PRESSED) != 0) {
3813 // take focus if we don't have it already and we should in
3814 // touch mode.
3815 boolean focusTaken = false;
3816 if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
3817 focusTaken = requestFocus();
3818 }
3819
3820 if (!mHasPerformedLongPress) {
3821 // This is a tap, so remove the longpress check
3822 if (mPendingCheckForLongPress != null) {
3823 removeCallbacks(mPendingCheckForLongPress);
3824 }
3825
3826 // Only perform take click actions if we were in the pressed state
3827 if (!focusTaken) {
3828 performClick();
3829 }
3830 }
3831
3832 if (mUnsetPressedState == null) {
3833 mUnsetPressedState = new UnsetPressedState();
3834 }
3835
3836 if (!post(mUnsetPressedState)) {
3837 // If the post failed, unpress right now
3838 mUnsetPressedState.run();
3839 }
3840 }
3841 break;
3842
3843 case MotionEvent.ACTION_DOWN:
3844 mPrivateFlags |= PRESSED;
3845 refreshDrawableState();
3846 if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
3847 postCheckForLongClick();
3848 }
3849 break;
3850
3851 case MotionEvent.ACTION_CANCEL:
3852 mPrivateFlags &= ~PRESSED;
3853 refreshDrawableState();
3854 break;
3855
3856 case MotionEvent.ACTION_MOVE:
3857 final int x = (int) event.getX();
3858 final int y = (int) event.getY();
3859
3860 // Be lenient about moving outside of buttons
3861 int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();
3862 if ((x < 0 - slop) || (x >= getWidth() + slop) ||
3863 (y < 0 - slop) || (y >= getHeight() + slop)) {
3864 // Outside button
3865 if ((mPrivateFlags & PRESSED) != 0) {
3866 // Remove any future long press checks
3867 if (mPendingCheckForLongPress != null) {
3868 removeCallbacks(mPendingCheckForLongPress);
3869 }
3870
3871 // Need to switch from pressed to not pressed
3872 mPrivateFlags &= ~PRESSED;
3873 refreshDrawableState();
3874 }
3875 } else {
3876 // Inside button
3877 if ((mPrivateFlags & PRESSED) == 0) {
3878 // Need to switch from not pressed to pressed
3879 mPrivateFlags |= PRESSED;
3880 refreshDrawableState();
3881 }
3882 }
3883 break;
3884 }
3885 return true;
3886 }
3887
3888 return false;

如果消化了点击事件的Action_Down,则返回true。若不消耗则返回false,返回false则对应的dispatchTouchEvent也返回fasle。 则表示不拦截,继续向下分发事件。至于怎么继续向下分发呢,这个放在ViewGroup的事件分发机制来说。返回True 则表示这个View拦截这个事件,这样这个完整的点击事件(Action_Down到Up)都交给这个View来处理。

ViewGroup:
ViewGroup有三个方法dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent
dispatchTouchEvent 先看看源码里的一小段,就能马上理解ViewGroup的分发机制了

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.

依次调用ViewGroup里所包含的childView的dispatchTouchEvent,倘若该ViewGroup里的childView,有一个选择了拦截,那么这个ViewGroup的dispatchTouchEvent就返回True,而事件的传递也就会在拦截的childView的onTouchEvent 里停止。如果ViewGroup里所有的childView的dispatchTouchEvent都返回false,即子View都不拦截,那么就相当于这个点击事件已经传递到底了,只能逐层向上响应,即调用ViewGroup的onTouchEvent,直到某个ViewGroup的onTouchEvent返回True,否则一直向上响应,直至rootView。

废话一堆不如代码来的实在。请看例子!
先写2个自定义的ViewGroup,ViewGroupB代码同ViewGroupA,只是在三个事件拦截的方法里加了打印信息

package com.dongua.toucheventtest;


import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.RelativeLayout;

public class ViewGroupA extends RelativeLayout
{


public ViewGroupA(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}

public ViewGroupA(Context context)
{
super(context);
}

public ViewGroupA(Context context, AttributeSet attrs)
{
super(context, attrs);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("click", "dispatchTouchEvent: ViewGroupA");
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("click", "onInterceptTouchEvent: ViewGroupA");
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("click", "onTouchEvent: ViewGroupA");
return super.onTouchEvent(event);
}


}

和一个自定义的View,这里先继承自TextView,因为TextView对点击事件是默认不响应,当然如果熟悉的话你继承一个Button,然后手动的在dispatchTouchEvent里返回fasle,使button不拦截也是ok 的~

package com.dongua.toucheventtest;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;

/**
* Created by dongua on 2016/11/21.
*/

public class ViewC extends TextView {

public ViewC(Context context) {
super(context);
}

public ViewC(Context context, AttributeSet attrs) {
super(context, attrs);
}

public ViewC(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}


@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("click", "dispatchTouchEvent: ViewC");
return super.dispatchTouchEvent(ev);
}


@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("click", "onTouchEvent: ViewC");
return super.onTouchEvent(event);
}
}

然后写一下布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.dongua.toucheventtest.MainActivity">

<com.dongua.toucheventtest.ViewGroupA
android:id="@+id/A"
android:background="#0000ff"
android:layout_width="200dp"
android:layout_height="200dp">
<com.dongua.toucheventtest.ViewGroupB
android:id="@+id/B"
android:background="#00ff00"
android:layout_width="100dp"
android:layout_height="100dp">
<com.dongua.toucheventtest.ViewC
android:id="@+id/C"
android:background="#ff0000"
android:layout_width="60dp"
android:layout_height="60dp"
android:text="View C 文本"/>

</com.dongua.toucheventtest.ViewGroupB>
</com.dongua.toucheventtest.ViewGroupA>
</LinearLayout>

布局如图:
[Android进阶笔记]Android触摸事件的拦截机制

然后试着点击一下TextView,看看打印消息

11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewGroupA
11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: onInterceptTouchEvent: ViewGroupA
11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewGroupB
11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: onInterceptTouchEvent: ViewGroupB
11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewC
11-22 18:11:36.771 7412-7412/com.dongua.toucheventtest I/click: onTouchEvent: ViewC
11-22 18:11:36.771 7412-7412/com.dongua.toucheventtest I/click: onTouchEvent: ViewGroupB
11-22 18:11:36.771 7412-7412/com.dongua.toucheventtest I/click: onTouchEvent: ViewGroupA

这就很好理解了吧 这次我们让TextView强行拦截,即ViewC的dispatchTouchEvent返回一个true,我们在看看打印的消息

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("click", "dispatchTouchEvent: ViewC");
return true;
}
11-22 18:17:55.295 15435-15435/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewGroupA
11-22 18:17:55.295 15435-15435/com.dongua.toucheventtest I/click: onInterceptTouchEvent: ViewGroupA
11-22 18:17:55.296 15435-15435/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewGroupB
11-22 18:17:55.296 15435-15435/com.dongua.toucheventtest I/click: onInterceptTouchEvent: ViewGroupB
11-22 18:17:55.296 15435-15435/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewC

可以看到它只到ViewC的dispatchTouchEvent就结束了整个事件的拦截。因为ViewC我们手动设置了拦截,但ViewC继承自TextView,如果我们对onTouchEvent也强行返回true,那么就会执行到ViewC的onTouchEvent方法。
同理的大家可以试试在ViewGroupB的dispatchTouchEvent直接返回true,这就使得事件向下的传递到此为止。对onTouchEvent直接返回true,这就使得事件向上的响应到此为止。

例子就这些,大家想要熟悉的话肯定要自己去手动实现一遍,我觉得一定要在反复的看几遍文字的描述之后,再去写代码才能理解其中深意,盲目的堆代码只会让你的了解停留在表面。之前看完了老是不自己写一遍,结果一阵子之后又忘了,如果多做笔记,在做笔记的过程中无疑又是一次学习的过程。
以上为个人学习笔记,如有错误,请不吝赐教~