学习笔记:触摸事件MotionEvent

1. 铺垫

1.1 MotionEvent : 触屏事件
int ACTION_DOWN=0 : 代表down
Int ACTION_MOVE=2 ; 代表move
Int ACTION_UP=1 : 代表up

1.2 Activity
boolean dispatchTouchEvent(MotionEvent event) : 分发事件
boolean onTouchEvent(MotionEvent event) : 处理事件的回调

1.3 View
boolean dispatchTouchEvent(MotionEvent event) : 分发事件
void setOnTouchListener(OnTouchListener l) : 设置事件监听器
boolean onTouchEvent(MotionEvent event) : 处理事件的回调方法

1.4 ViewGroup
boolean dispatchTouchEvent(MotionEvent ev) : 分发事件
boolean onInterceptTouchEvent(MotionEvent ev) : 拦截事件的回调方法

1.5 若onTouchEvent或onTouchEvent返回true,则表示事件被消费了,就不会进一步分发。

2.

<?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.sagereal.motionevent.MainActivity">

    <com.sagereal.motionevent.MyImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        android:layout_margin="30dp"
        android:id="@+id/iv_test"/>
</LinearLayout>
public class MyImageView extends ImageView {
    private static final String TAG = "MyImageView";
    //布局  用两个参数的构造 : 属性集合 AttributeSet 
    public MyImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.e(TAG, "MyImageView()");
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent: action = " + event.getAction());
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: action = " + event.getAction());
        return super.onTouchEvent(event);
    }
}
public class MainActivity extends Activity{
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.iv_test).setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e("MyImageView", "Listener onTouch: action = " + event.getAction());
                return false;
            }
        });
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "dispatchTouchEvent: action =" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: action =" + event.getAction());
        return super.onTouchEvent(event);
    }
}

对imageView来说,其回调监听setOnTouchListener默认返回false,即处理事件但不消费事件。

2.1 点击图标之外的位置
对事件down来说,Activity的dispatchTouchEvent方法分发事件,但没有子view来处理、消费,只好调用自己的onTouchEvent分发来处理消费。事件move(1)/up(2)类似。

01-03 10:26:19.660 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =0
01-03 10:26:19.662 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =0
01-03 10:26:19.685 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:26:19.686 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:26:19.702 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:26:19.702 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:26:19.722 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:26:19.722 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:26:19.724 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =1
01-03 10:26:19.725 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =1

2.2 点击图标

对事件down来说,Activity的dispatchTouchEvent方法分发事件,分发到子view,会先调用imgView的dispatchTouchEvent分发,
但它是最后一层,无子view,就由imageView来处理、消费。
对imageView来说,回调监听(setOnTouchListener)的优先级大于回调方法(onTouchEvent)的优先级,若回调监听(setOnTouchListener)、回调方法(onTouchEvent)都不处理消费该事件,则由Activity的onTouchEvent方法处理。
因为imageView连down事件都不处理,故move/up事件也不会分发到imageView,直接由Activity的onTouchEvent方法处理了。

01-03 10:31:14.237 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =0
01-03 10:31:14.239 3437-3437/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 0
01-03 10:31:14.239 3437-3437/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 0
01-03 10:31:14.239 3437-3437/com.sagereal.motionevent E/MyImageView: onTouchEvent: action = 0
01-03 10:31:14.240 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =0
01-03 10:31:14.262 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:31:14.262 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:31:14.272 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:31:14.272 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:31:14.288 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:31:14.289 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:31:14.291 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =1
01-03 10:31:14.291 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =1

3. 让imageView的回调监听返回true

findViewById(R.id.iv_test).setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e("MyImageView", "Listener onTouch: action = " + event.getAction());
        //return false;
        return true;
    }
});

因为imgView的回调监听(setOnTouchListener)的优先级大于回调方法(onTouchEvent)的优先级,
当消息从Activity分发到imageView后,就在setOnTouchListener内被消费了(返回了true),故不会走到回调方法onTouchEvent中
down/move/up事件的处理类似。

点击图标,对应的日志:

01-03 12:32:41.807 5681-5681/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =0
01-03 12:32:41.809 5681-5681/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 0
01-03 12:32:41.809 5681-5681/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 0
01-03 12:32:41.839 5681-5681/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 12:32:41.840 5681-5681/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 2
01-03 12:32:41.840 5681-5681/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 2
01-03 12:32:41.849 5681-5681/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 12:32:41.850 5681-5681/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 2
01-03 12:32:41.850 5681-5681/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 2
01-03 12:32:41.852 5681-5681/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =1
01-03 12:32:41.853 5681-5681/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 1
01-03 12:32:41.854 5681-5681/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 1

4. 让imageView的down事件对应的回调监听返回true,move/up事件都返回false

findViewById(R.id.iv_test).setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e("MyImageView", "Listener onTouch: action = " + event.getAction());
        //return false;
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            return true;
        }
        return false;
    }
});

down事件是被分发到imageView,被setOnTouchListener消费,但move/up事件并没有被消费,仍然返回false,故最终会被Activity的onTouchEvent处理消费。

点击图标,对应的日志:

01-01 19:40:02.793 5725-5725/com.sagereal.motionevent E/MyImageView: MyImageView()
01-01 19:40:09.469 5725-5725/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =0
01-01 19:40:09.472 5725-5725/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 0
01-01 19:40:09.472 5725-5725/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 0
01-01 19:40:09.477 5725-5725/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-01 19:40:09.478 5725-5725/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 2
01-01 19:40:09.479 5725-5725/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 2
01-01 19:40:09.479 5725-5725/com.sagereal.motionevent E/MyImageView: onTouchEvent: action = 2
01-01 19:40:09.480 5725-5725/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-01 19:40:09.484 5725-5725/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =1
01-01 19:40:09.485 5725-5725/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 1
01-01 19:40:09.486 5725-5725/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 1
01-01 19:40:09.486 5725-5725/com.sagereal.motionevent E/MyImageView: onTouchEvent: action = 1
01-01 19:40:09.487 5725-5725/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =1

二.补充

事件序列,一般是指从手指触摸到屏幕在到离开屏幕这么一个过程。在这个过程中其实会产生多个事件,一般是以ACTION_DOWN作为开始,中间存在多个ACTION_MOVE,最后以ACTION_UP结束。我们称一次ACTION_DOWN-->ACTION_MOVE-->ACTION_UP过程称为一个事件序列。

事件分发主要3个方法:

dispatchTouchEvent:负责事件的传递分发,事件到达view时一定回调此方法。返回值表示是否消费此事件,受onTouchEvent和子view的dispatchTouchEvent返回值影响。

onInterceptTouchEvent:用于判断是否拦截事件,也是返回值的含义。在dispatchTouchEvent内部调用。view拦截了某个事件,那后续这一事件序列都会默认拦截,不再调用此方法。只有ViewGroup才有这个方法,View无此方法,即View不可以拦截事件。

onTouchEvent:用于处理事件,返回值表示是否消费此事件。在dispatchTouchEvent内部调用。如果不消耗某一事件,那当前view不再接受同一事件序列的事件。 

三者关系:事件到达view时,会调用dispatchTouchEvent,然后内部调用onInterceptTouchEvent判断是否拦截,如果不拦截就调用子view的dispatchTouchEvent;若拦截,则执行onTouchEvent方法处理这个事件。

/**
  * 点击事件产生后
  */ 
  // 步骤1:调用dispatchTouchEvent()
  public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean consume = false; //代表 是否会消费事件
    
    // 步骤2:判断是否拦截事件
    if (onInterceptTouchEvent(ev)) {
      // a. 若拦截,则将该事件交给当前View进行处理
      // 即调用onTouchEvent ()方法去处理点击事件
        consume = onTouchEvent (ev) ;
    } else {
      // b. 若不拦截,则将该事件传递到下层
      // 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
      // 直到点击事件被最终处理为止
      consume = child.dispatchTouchEvent (ev) ;
    }
    // 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
    return consume;
   }

一般有三种返回:truefalsesuper引用父类对应方法。

dispatchTouchEvent 返回 true:表示改事件在本层不再进行分发且已经在事件分发自身中被消费了。
dispatchTouchEvent 返回 false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费。dispatchTouchEvent 返回 super : 默认会调用自己的 onInterceptTouchEvent 方法    

onInterceptTouchEvent 返回true:表示将事件进行拦截,并将拦截到的事件交由本层控件 的onTouchEvent 进行处理。
onInterceptTouchEvent 返回false:表示不对事件进行拦截,事件得以成功分发到子View。并由子ViewdispatchTouchEvent进行处理。

onTouchEvent 返回 true:表示onTouchEvent处理完事件后消费了此次事件。此时事件终结,将不会进行后续的传递。
onTouchEvent 返回 false:事件在onTouchEvent中处理后继续向上层View传递,且有上层ViewonTouchEvent进行处理。

除此之外还有一个方法也是经常用到的:requestDisallowInterceptTouchEvent(),这个方法能够影响父ViewGroup是否拦截事件,true表示 不拦截事件,false表示拦截事件。

Android开发艺术探索》里总结了11条关于事件传递的结论:

  1. 同一个事件序列是指手机接触屏幕那一刻起,到离开屏幕那一刻结束,有一个down事件,若干个move事件,一个up事件构成。

  2. 某个View一旦决定拦截事件,那么这个事件序列之后的事件都会由它来处理,并且不会再调用onInterceptTouchEvent。

  3. 正常情况下,一个事件序列只能被一个View拦截并消耗。这个原因可以参考第2条,因为一旦拦截了某个事件,那么这个事件序列里的其他事件都会交给这个View来处理,所以同一事件序列中的事件不能分别由两个View同时处理,但是我们可以通过特殊手段做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。

  4. 一个View如果开始处理事件,如果它不处理down事件(onTouchEvent里面返回了false),那么这个事件序列的其他事件就不会交给它来继续处理了,而是会交给它的父元素去处理。

  5. 如果一个View处理了down事件,却没有处理其他事件,那么这些事件不会交给父元素处理,并且这个View还能继续受到后续的事件。而这些未处理的事件,最终会交给Activity来处理。

  6. ViewGroup的onInterceptToucheEvent默认返回false,也就是默认不拦截事件。

  7. View没有InterceptTouchEvent方法,如果有事件传过来,就会直接调用onTouchEvent方法。

  8. View的onTouchEvent方法默认都会消耗事件,也就是默认返回true,除非它是不可点击的(longClickable和clickable同时为false)。注意:View的longClickable默认都为false,clickable要根据控件属性判断。

  9. View的enable属性不会影响onTouchEvent的默认返回值。就算一个View是不可见的,只要它是可点击的(clickable或者longClickable有一个为true),它的onTouchEvent默认返回值也是true。

  10. onClick方法会执行的前提是当前View是可点击的,并且它收到了down和up事件。

  11. 事件传递过程是由外向内的,也就是事件会先传给父元素在向下传递给子元素。但是子元素可以通过requestDisallowInterceptTouchEvent来干预父元素的分发过程,但是down事件除外(因为down事件方法里,会清除所有的标志位)。

参考:

Android事件分发机制详解:史上最全面、最易懂

Android事件传递、多点触控及滑动冲突的处理

Android View的事件分发机制和滑动冲突解决方案

View事件分发、滑动冲突--《Android开发艺术探索》阅读笔记——第三章part2

Android进阶知识:事件分发与滑动冲突

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值