触摸事件传递机制分析

1.触摸事件的类型

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

  • ACTION_DOWN:用户按下的操作,标记这按下操作的开始
  • ACTION_MOVE:用户手指按下后移动一定距离的操作,一般情况下,手指的轻微移动都会触发移动事件。
  • ACTION_UP:用户手指离开屏幕是的操作,标记着触摸事件的结束。
2.事件传递的三个阶段
  • 分发(Dispatch):事件分发对应着dispatchTouchEvent方法,在Android系统中,所有的触摸事件都是通过这方法来分发的:
public boolean dispatchTouchEvent(MotionEvent ev)

在这个方法中,根据当前视图的具体实现逻辑,来决定是直接消费这个事件还是将事件继续分发给子视图处理,方法返回值为true表示被当前视图消费掉,不再继续分发事件;方法返回值为super.dispatchTouchEvent(ev)表示为继续分发事件。如果当前视图是ViewGroup及其子类,则会调用onInterceptTouchEvent(ev)方法判断是否拦截该事件。

  • 拦截(Intercept):事件的拦截对应着onInterceptTouchEvent 方法,这个方法只在ViewGroup及其子类中存在,在View和Activity中是不存在的:
public boolean onInterceptTouchEvent(MotionEvent ev)

这方法也是通过返回的布尔值决定是否拦截对应的事件,根据具体的实现逻辑,返回true拦截这个事件,不继续分发给子视图,同时交由自身的onTouchEvent(ev)方法进行消费,返false或者super.onInterceptTouchEvent(ev)表示不对事件进行拦截,继续传递给子视图。

  • 消费(Consume):事件的消费对应着onTouchEvent方法:
public boolean onTouchEvent(MotionEvent ev)

该方法返回值为true表示当前视图可以处理对应的事件,事件将不会向上传递给父视图;返回值为false表示当前视图不处理这个事件,事件会被传递给父视图的onTouchEvent方法进行处理。

在Android里,拥有事件传递处理能力的类有三种:

  • Activity:拥有dispatchTouchEvent和onTouchEven两个方法
  • ViewGroup:拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法
  • View:拥有dispachTouchEvent和onTouchEvent两个方法。
3.View的事件传递机制

虽然ViewGroup是View子类,但是这里所说的View专指出ViewGroup外的View控件,例如TextView,ImageView,Button等,View控件本本身已经是最小的单位,不能作为其他View的容器。View控件拥有dispatchTouchEvent和onTouchEvent两个方法。如下所示:

class MyTextView : androidx.appcompat.widget.AppCompatTextView {
    companion object {
        private const val TAG = "MyTextView"
    }

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> Log.e(TAG, "dispatchTouchEvent: ACTION_DOWN")
            MotionEvent.ACTION_MOVE -> Log.e(TAG, "dispatchTouchEvent: ACTION_MOVE")
            MotionEvent.ACTION_UP -> Log.e(TAG, "dispatchTouchEvent: ACTION_UP")
            MotionEvent.ACTION_CANCEL -> Log.e(TAG, "dispatchTouchEvent: ACTION_CANCEL")
        }
        return super.dispatchTouchEvent(event)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> Log.e(TAG, "onTouchEvent: ACTION_DOWN")
            MotionEvent.ACTION_MOVE -> Log.e(TAG, "onTouchEvent: ACTION_MOVE")
            MotionEvent.ACTION_UP -> Log.e(TAG, "onTouchEvent: ACTION_UP")
            MotionEvent.ACTION_CANCEL -> Log.e(TAG, "onTouchEvent: ACTION_CANCEL")
        }
        return super.onTouchEvent(event)
    }
}
class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnTouchListener {
    companion object {
        const val TAG = "MainActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        my_textView.setOnClickListener(this)
        my_textView.setOnTouchListener(this)
    }

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> Log.e(TAG, "dispatchTouchEvent: ACTION_DOWN")
            MotionEvent.ACTION_MOVE -> Log.e(TAG, "dispatchTouchEvent: ACTION_MOVE")
            MotionEvent.ACTION_UP -> Log.e(TAG, "dispatchTouchEvent: ACTION_UP")
            MotionEvent.ACTION_CANCEL -> Log.e(TAG, "dispatchTouchEvent: ACTION_CANCEL")
        }
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> Log.e(TAG, "onTouchEvent: ACTION_DOWN")
            MotionEvent.ACTION_MOVE -> Log.e(TAG, "onTouchEvent: ACTION_MOVE")
            MotionEvent.ACTION_UP -> Log.e(TAG, "onTouchEvent: ACTION_UP")
            MotionEvent.ACTION_CANCEL -> Log.e(TAG, "onTouchEvent: ACTION_CANCEL")
        }
        return super.onTouchEvent(event)
    }

    override fun onClick(v: View) {
        when (v.id) {
            R.id.my_textView -> Log.e(TAG, "onClick: MyTextView")
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        when (v.id) {
            R.id.my_textView -> {
                when (event.action) {
                    MotionEvent.ACTION_DOWN -> Log.e(TAG, " my_textView onTouchEvent: ACTION_DOWN")
                    MotionEvent.ACTION_MOVE -> Log.e(TAG, " my_textView onTouchEvent: ACTION_MOVE")
                    MotionEvent.ACTION_UP -> Log.e(TAG, " my_textView onTouchEvent: ACTION_UP")
                    MotionEvent.ACTION_CANCEL -> Log.e(TAG, " my_textView onTouchEvent: ACTION_CANCEL")
                }
            }
        }
        return super.onTouchEvent(event)
    }
}

点击MyTextView,在logcat中将打印如下:
com.xingguang.toucheventdemo E/MainActivity: dispatchTouchEvent: ACTION_DOWN
com.xingguang.toucheventdemo E/MyTextView: dispatchTouchEvent: ACTION_DOWN
com.xingguang.toucheventdemo E/MainActivity: my_textView onTouchEvent: ACTION_DOWN
com.xingguang.toucheventdemo E/MyTextView: onTouchEvent: ACTION_DOWN
com.xingguang.toucheventdemo E/MainActivity: dispatchTouchEvent: ACTION_UP
com.xingguang.toucheventdemo E/MyTextView: dispatchTouchEvent: ACTION_UP
com.xingguang.toucheventdemo E/MainActivity: my_textView onTouchEvent: ACTION_UP
com.xingguang.toucheventdemo E/MyTextView: onTouchEvent: ACTION_UP
com.xingguang.toucheventdemo E/MainActivity: onClick: MyTextView

从上面的代码运行日志来看,disoatchTouchEvent、onTouchEvent两个方法的返回值存在三种情况:

  • 直接返回false。
  • 直接返回true。
  • 返回父类的同名方法。
    不同返回值会导致事件传递的流程相差甚远。通过不断的修改调试返回值查到日志记录,得到以下流程图
    View事件流程图
    从上面流程图得到以下结论:
  • 触摸事件的传递流程是从dispachTouchEvent开始的,如果人为干预(也就是默认返回父类的同名函数),则事件将会依照嵌套层次从外层向内层传递,到达最内层的View时,就由它的onTouchEvent方法处理,该方法如果能够消费该事件,则返回true,如果处理不了,则返回false,这时事件会重新向外层传递,并由外层View的onTouchEvent方法进行处理,以此类推。
  • 如果事件在向内层传递过程中由人为干预,事件处理函数返回true,则会导致事件提前被消费掉,内层View将不会收到这个事件。
  • View控件的事件触发顺序是先执行onTouch方法,在最后执行onClick方法。如果onTuch返回true,则事件不会继续传递,最后也不会调用onClick方法。如果onTouch返回false,则事件继续传递。
4.ViewGroup的事件传递机制

ViewGroup是作为View控件的容器存在的,Android系统默认提供一系列ViewGroup的子类,常见的由LinearLayout、RelativeLayout、FrameLayout、ListView、ScrollView等。ViewGroup拥有dispachTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法,可以看出和View的唯一区别是多一个onInterceptTouchEvent方法。示例如下:

class MyRelativeLayout : RelativeLayout {
   companion object {
       private const val TAG = "MyRelativeLayout"
   }

   constructor(context: Context?) : super(context)
   constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)

   override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
       when (ev.action) {
           MotionEvent.ACTION_DOWN -> Log.e(TAG, "dispatchTouchEvent: ACTION_DOWN")
           MotionEvent.ACTION_MOVE -> Log.e(TAG, "dispatchTouchEvent: ACTION_MOVE")
           MotionEvent.ACTION_UP -> Log.e(TAG, "dispatchTouchEvent: ACTION_UP")
           MotionEvent.ACTION_CANCEL -> Log.e(TAG, "dispatchTouchEvent: ACTION_CANCEL")
       }
       return super.dispatchTouchEvent(ev)
   }

   override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
       when (ev.action) {
           MotionEvent.ACTION_DOWN -> Log.e(TAG, "onInterceptTouchEvent: ACTION_DOWN")
           MotionEvent.ACTION_MOVE -> Log.e(TAG, "onInterceptTouchEvent: ACTION_MOVE")
           MotionEvent.ACTION_UP -> Log.e(TAG, "onInterceptTouchEvent: ACTION_UP")
           MotionEvent.ACTION_CANCEL -> Log.e(TAG, "onInterceptTouchEvent: ACTION_CANCEL")
       }
       return super.onInterceptTouchEvent(ev)
   }

   @SuppressLint("ClickableViewAccessibility")
   override fun onTouchEvent(event: MotionEvent): Boolean {
       when (event.action) {
           MotionEvent.ACTION_DOWN -> Log.e(TAG, "onTouchEvent: ACTION_DOWN")
           MotionEvent.ACTION_MOVE -> Log.e(TAG, "onTouchEvent: ACTION_MOVE")
           MotionEvent.ACTION_UP -> Log.e(TAG, "onTouchEvent: ACTION_UP")
           MotionEvent.ACTION_CANCEL -> Log.e(TAG, "onTouchEvent: ACTION_CANCEL")
       }
       return super.onTouchEvent(event)
   }
}
 <?xml version="1.0" encoding="utf-8"?>
<com.xingguang.toucheventdemo.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.xingguang.toucheventdemo.MyTextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:text="Hello World!"
        android:gravity="center"
        android:layout_centerInParent="true"
        android:id="@+id/my_textView"
        android:background="@color/colorAccent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</com.xingguang.toucheventdemo.MyRelativeLayout>

点击MyTextView,在logcat将打印如下:
com.xingguang.toucheventdemo E/MainActivity: dispatchTouchEvent: ACTION_DOWN
com.xingguang.toucheventdemo E/MyRelativeLayout: dispatchTouchEvent: ACTION_DOWN
com.xingguang.toucheventdemo E/MyRelativeLayout: onInterceptTouchEvent: ACTION_DOWN
com.xingguang.toucheventdemo E/MyTextView: dispatchTouchEvent: ACTION_DOWN
com.xingguang.toucheventdemo E/MainActivity: my_textView onTouch: ACTION_DOWN
com.xingguang.toucheventdemo E/MyTextView: onTouchEvent: ACTION_DOWN
com.xingguang.toucheventdemo E/MainActivity: dispatchTouchEvent: ACTION_UP
com.xingguang.toucheventdemo E/MyRelativeLayout: dispatchTouchEvent: ACTION_UP
com.xingguang.toucheventdemo E/MyRelativeLayout: onInterceptTouchEvent: ACTION_UP
com.xingguang.toucheventdemo E/MyTextView: dispatchTouchEvent: ACTION_UP
com.xingguang.toucheventdemo E/MainActivity: my_textView onTouch: ACTION_UP
com.xingguang.toucheventdemo E/MyTextView: onTouchEvent: ACTION_UP
com.xingguang.toucheventdemo E/MainActivity: onClick: MyTextView

可以看到,与View的事件流程唯一不一样的地方是在MainActivity和MyTextView之间增加了一层MyRelativeLayout。通过反复调试得到下图:
ViewGroup事件流程图
从上面的流程图可以得到以下结论:

  • 触摸事件的传递循序是由Activity到ViewGroup,再有ViewGroup递归分发给它的子View。
  • ViewGroup通过onInterceptTouchEvent方法对事件进行拦截,如果该方法返回true,则事件不会继续传递给子View,如果返回false或者super.onInterceptTouchEven则事件会继续传递给子View。
  • 在子View中对事件进行消费后,ViewGroup将接收不到任何事件。
    在掌握上面的事件流程之后,应该可以应对实际项目开发中90%以上的工作,如果想进一步了解,可以阅读framework层的源码,基本上就是遵循上述流程的。

内容参考:Android高级进阶 顾大神

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值