Android Touch事件的传递机制

android中会经常遇到多个View ViewGroup嵌套的问题,如果想要快速的解决这种问题,就需要对View的事件传递有较深入的理解。一次完整的事件传递机制,主要是三个阶段,分别是事件的分发,拦截和消费。

1 触摸事件的主要类型
触摸事件对应的是MotionEvent类,事件的类型主要有如下三种。

  • ACTION_DOWN:用户手指按下的操作,一个按下操作标志着一次触摸事件的开始。
  • ACTION_MOVE:用户手指按压屏幕后,在松开之前,如果移动的距离超过一定的阈值,那么会被判定为ACTION_MOVE的操作,一般情况下,手指的轻微移动都会触发一系列的移动事件。
  • ACTION_UP:用户手指离开 屏幕的操作,一次抬起操作标志者这一次触摸事件的结束。

在一次触摸事件操作中,ACTION_DOWN和ACTION_UP这两个事件是必须的,而ACTION_MOVE视情况而定,如果用户只点击了一下屏幕,那么可能只会监听到按下和抬起的动作。

2 事件传递的三个阶段

  • 分发(Dispatch):事件的分发对应着dispatchTouchEvent方法,在Android系统中,所有的触摸事件都是通过这个方法来分发的,方法原型如下:
// 方法值返回true,表示事件被当前视图消费掉,不再继续分发事件
// 方法值返回super.dispatchTouchEvent 表示继续分发该事件

public boolean dispatchTouchEvent(MotionEvent ev)
  • 拦截(Intercept):事件的拦截对应着onInterceptTouchEvent方法,这个方法只在ViewGroup及其子类中才存在,在View和Activity中是不存在的,方法原型如下:
// 方法值返回true,表示拦截这个事件,不继续分发给子视图,同时交由自身的onTouchEvent方法进行消费
// 方法值返回false或者super.onInterceptTouchEvent表示不对事件进行拦截,需要继续传递给子视图

public boolean onInterceptTouchEvent(MotionEvent ev)
  • 消费(Consume):事件的消费对应着onTouchEvent方法,方法原型如下:
// 方法值返回true,表示当前视图可以处理对应的事件,事件将不会向上传递给父视图
// 方法值返回false,表示当前视图不处理这个事件,事件会被传递给父视图的onTouchEvent方法处理

public boolean onTouchEvent(MotionEvent ev)

在Android 系统中,拥有事件传递能力的类有以下三种:

  • Activity:拥有dispatchTouchEvent和onTouchEvent两个方法
  • ViewGroup:拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法
  • View:拥有dispatchTouchEvent和onTouchEvent两个方法

注意:这里的View是不包括ViewGroup的View控件,比如Button,TextView等本身已经是最小的单位。

3 View的事件传递机制

// 自定义MyTextView
public class MyTextView extends TextView {
    private static final 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 defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        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");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.e(TAG, "onTouchEvent ACTION_CANCEL");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }
}

自定义一个MyTextView 重写dispatchTouchEvent()和onTouchEvent()方法,将每个事件触发都打印Log日志。在定义一个MainActivity,在MainActivity中监听MyTextView对象的触摸和点击事件。

public class MainActivity extends AppCompatActivity implements View.OnTouchListener, View.OnClickListener {
    private static final String TAG = "MainActivity";
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.my_text_view);
        mTextView.setOnClickListener(this);
        mTextView.setOnTouchListener(this);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.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(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            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;
            case MotionEvent.ACTION_CANCEL:
                Log.e(TAG, "onTouchEvent ACTION_CANCEL");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (v.getId()) {
            case R.id.my_text_view :
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN :
                        Log.e(TAG, "MyTextView onTouch ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE :
                        Log.e(TAG, "MyTextView onTouch ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP :
                        Log.e(TAG, "MyTextView onTouch ACTION_UP");
                        break;
                    default:
                        break;
                }
                break;
            default:
                break;
        }
        return false;
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.my_text_view:
                Log.e(TAG, "MyTextView onClick");
                break;
            default:
                break;
        }
    }
}

当我们点击MyTextView的对象时,打印log如下:

com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN   
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onClick

可以看出一下结论:
1. 触摸事件的传递流程从dispatchTouchEvent开始,如果不进行人为干预,则事件会依靠嵌套从外层向里层传递,到达最里层的view时,就由它的onTouchEvent处理,如果它消费不了就依次从里向外传递,由外层view的onTouchEvent方法进行处理。
2. 如果事件在向内层传递过程中人为干预,事件处理函数返回true,则会导致事件提前被消费掉,内层view将不会接收到这个事件。
3. view控件的事件触发顺序是先执行onTouch方法,在最后才执行onClick方法,如果onTouch返回true,则事件不会继续传递,最后也不会调用onClick方法,如果onTouch返回false,则事件继续传递。
4. 另外,dispatchTouchEvent()方法中还有“记忆”的功能,如果第一次事件向下传递到某View,它把事件继续传递交给它的子View,它会记录该事件是否被它下面的View给处理成功了,(怎么能知道呢?如果该事件会再次被向上传递到我这里来由我的onTouchEvent()来处理,那就说明下面的View都没能成功处理该事件);当第二次事件向下传递到该View,该View的dispatchTouchEvent()方法机会判断,若上次的事件由下面的view成功处理了,那么这次的事件就继续交给下面的来处理,若上次的事件没有被下面的处理成功,那么这次的事件就不会向下传递了,该View直接调用自己的onTouchEvent()方法来处理该事件。

4 ViewGroup的事件传递机制
ViewGroup是作为view控件的容器存在的,ViewGroup拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法,可以看出和View的唯一区别是多了一个onInterceptTouchEvent方法。我们自定义一个MyRelativeLayout 继承RelativeLayout。

// 自定义RelativeLayout
public class MyRelativeLayout extends RelativeLayout{


    private static final String TAG = "MyRelativeLayout";

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.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(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            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.onInterceptHoverEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            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;
            case MotionEvent.ACTION_CANCEL:
                Log.e(TAG, "onTouchEvent ACTION_CANCEL");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }
}

修改xml文件,将这个Layout作为MyTextView的容器,如下:

<?xml version="1.0" encoding="utf-8"?>
<com.example.jmf.advancedlevel.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.jmf.advancedlevel.MyTextView
        android:id="@+id/my_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="请点击我"
        android:textSize="20sp" />

</com.example.jmf.advancedlevel.MyRelativeLayout>

运行,点击MyTextView,打印Log日志如下:

com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onClick

从上面的日志情况得到如下结论:
1. 触摸事件的传递顺序是由Activity到ViewGroup,再由ViewGroup递归传递给它的子View
2. ViewGroup通过onInterceptTouchEvent方法对事件进行拦截,如果该方法返回true,则事件不会继续传递给子View,如果返回false或者super.onInterceptTouchEvent,则事件会继续传递给子View。
3. 在子View中对事件进行消费后,ViewGroup将接收不到任何事件

5关于事件的拦截和反拦截
1.5.1 怎么拦截事件?很简单,复写ViewGroup的onInterceptTouchEvent方法:

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN :
                Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
                return true;
            case MotionEvent.ACTION_MOVE :
                Log.e(TAG,"onInterceptTouchEvent ACTION_MOVE");
                return true;
            case MotionEvent.ACTION_UP :
                Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
                return true;
        }
        return true;
    }

默认返回false 或者super.onInterceptTouchEvent,表示是不拦截事件,现在运行,如下:

com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_MOVE
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_MOVE
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_UP

Log中可以看到MyTextView中什么事件也接受不到,如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ;

1.5.2 怎么实现反拦截,就是不让父类拦截子View?
Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
    // getParent().requestDisallowInterceptTouchEvent(true);  这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。
        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);
    }

从源码也可以解释:

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是木有办法的捕获事件的~~~

6 总结
1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;

2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法

3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值