View的事件分发及滑动冲突的解决

一、触摸事件的类型

ACTION_DOWN:用户手指按下操作,一个按下操作标志着一次触摸事件的开始
ACTION_UP:用户手指抬起操作,一次抬起标志着一次事件的结束
ACTION_MOVE:手指按下抬起前,如果移动的距离超过一定的阈值,就会触发ACTION_MOVE

一次触摸事件,ACTION_DOWN和ACTION_UP是必须存在的,ACTION_MOVE视情况而定。

二、事件传递的三个阶段

分发(dispatch) dispatchTouchEvent
  • public boolean dispatchTouchEvent(MotionEvent event)
  • 根据当前视图的具体实现逻辑,来决定是直接消费这个事件还是将这个事件继续分发给子视图进行处理
  • true 表示事件被当前视图消费掉,不在继续分发事件
  • super.dispatchEvent表示继续分发改事件,如果当前视图是viewGroup及其子类,则会调用onInterceptTouchEvent方法判断是否拦截该事件
拦截(intercept) onInterceptTouchEvent
  • 事件的拦截对应着onInterceptTouchEvent方法,这个方法只在viewGroup及其子类中存在,不在activity和view中存成
  • public boolean onInterceptTouchEvent(MotionEvent event)
  • true 表示拦截这个事件,不继续分发给子视图,并调用自身的onTouchEvent进行消费
  • false或者super.onInterceptEvent表示不对事件进行拦截,需要继续传递给子视图
消费(consume) onTouchEvent
  • public boolean onTouchEvent(MotionEvent event)
  • true 表示当前视图处理对应的事件,事件将不会向上传递给父视图
  • false 表示当前视图不处理对应的事件,事件将会向上传递给父视图的onTouchEvent进行处理
在Android中拥有事件传递的类有三种 activity view 和viewGroup
  • activity:拥有dispatchTouchEvent和onTouchEvent方法
  • view:拥有dispatchTouchEvent和onTouchEvent方法
  • viewGroup:永远dispatchTouchEvent、onInterceptEvent和onTouchEvent方法

三、view的事件传递

虽然viewGroup是view的子类,这里的view指除去viewGroup的view控件,例如textView,button,imageView等控件
写个简单的demo,分析view的事件传递

3.1、自定义一个view继承textView,并重写onTouchEvent和dispatchTouchEvent方法

**

class MyTextView : androidx.appcompat.widget.AppCompatTextView {

    constructor(context: Context):super(context){

    }
    constructor(context: Context, attributeSet: AttributeSet): super(context, attributeSet){

    }

    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int): super(context, attributeSet, defStyleAttr){

    }



    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                Log.e("MyTextView","dispatchTouchEvent ACTION_DOWN")
            }
            MotionEvent.ACTION_UP -> {
                Log.e("MyTextView","dispatchTouchEvent ACTION_UP")
            }
            MotionEvent.ACTION_MOVE -> {
                Log.e("MyTextView","dispatchTouchEvent ACTION_MOVE")
            }
        }

        return super.dispatchTouchEvent(event)
    }


    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                Log.e("MyTextView","onTouchEvent ACTION_DOWN")
            }
            MotionEvent.ACTION_UP -> {
                Log.e("MyTextView","onTouchEvent ACTION_UP")
            }
            MotionEvent.ACTION_MOVE -> {
                Log.e("MyTextView","onTouchEvent ACTION_MOVE")
            }
        }


        return super.onTouchEvent(event)
    }


}
3.2、在activity的xml中添加MyTextView,给MyTextView设置setOnTouchListener和setOnClickListener监听,并重写activity的onTouchEvent和dispatchTouchEvent方法

**

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var mTextView = findViewById<MyTextView>(R.id.mTextView)

        mTextView.setOnClickListener {
            Log.e("ysl","mTextView Click")
        }




        mTextView.setOnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    Log.e("mTextView","OnTouch ACTION_DOWN")
                }
                MotionEvent.ACTION_UP -> {
                    Log.e("mTextView","OnTouch ACTION_UP")
                }
                MotionEvent.ACTION_MOVE -> {
                    Log.e("mTextView","OnTouch ACTION_MOVE")
                }
            }
            return@setOnTouchListener super.onTouchEvent(event)
        }




    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                Log.e("MainActivity","dispatchTouchEvent ACTION_DOWN")
            }
            MotionEvent.ACTION_UP -> {
                Log.e("MainActivity","dispatchTouchEvent ACTION_UP")
            }
            MotionEvent.ACTION_MOVE -> {
                Log.e("MainActivity","dispatchTouchEvent ACTION_MOVE")
            }
        }


        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                Log.e("MainActivity","onTouchEvent ACTION_DOWN")
            }
            MotionEvent.ACTION_UP -> {
                Log.e("MainActivity","onTouchEvent ACTION_UP")
            }
            MotionEvent.ACTION_MOVE -> {
                Log.e("MainActivity","onTouchEvent ACTION_MOVE")
            }
        }

        return super.onTouchEvent(event)
    }
}
3.3、日志打印结果

**

2021-03-30 18:07:14.880 23744-23744/com.ysl.dispatchstudy E/MainActivity: dispatchTouchEvent ACTION_DOWN
2021-03-30 18:07:14.880 23744-23744/com.ysl.dispatchstudy E/MyTextView: dispatchTouchEvent ACTION_DOWN
2021-03-30 18:07:14.880 23744-23744/com.ysl.dispatchstudy E/mTextView: OnTouch ACTION_DOWN
2021-03-30 18:07:14.880 23744-23744/com.ysl.dispatchstudy E/MyTextView: onTouchEvent ACTION_DOWN
2021-03-30 18:07:14.960 23744-23744/com.ysl.dispatchstudy E/MainActivity: dispatchTouchEvent ACTION_UP
2021-03-30 18:07:14.960 23744-23744/com.ysl.dispatchstudy E/MyTextView: dispatchTouchEvent ACTION_UP
2021-03-30 18:07:14.960 23744-23744/com.ysl.dispatchstudy E/mTextView: OnTouch ACTION_UP
2021-03-30 18:07:14.960 23744-23744/com.ysl.dispatchstudy E/MyTextView: onTouchEvent ACTION_UP
2021-03-30 18:07:14.961 23744-23744/com.ysl.dispatchstudy E/ysl: mTextView Click

3.4、view事件分发的分析

view的事件传递 根据结果显示
1、触摸事件的传递流程是从dispatchTouchEvent开始的,如果没有人为干预(也就是默认返回父类的同名函数),则事件将会按照嵌套层次有外向内传递,到达最内层的view时,就由最内层的onTouchEvent进行处理,如果能处理就返回true消费掉,如果不能处理就返回false,这时事件会重新向外层传递,并由外层的onTouchEvent进行处理,依次类推
2、如果事件在向内层传递过程中被人为干预,事件处理函数返回true,事件将会被提前消费掉,内层view将不会收到这个事件
3、view的事件触发是先执行onTouch方法,在最后执行onClick方法,如果onTouch返回true,事件将不会继续传递,最后也不会调用onClick方法,如果返回false,事件继续传递

四、viewGroup的事件分发

viewGroup作为view控件的容器存在,Android系统默认提供了一系列viewGroup,例如LinearLayout,FrameLayout,RelativeLayout,ListView等

4.1、自定义一个简单的MyRelativeLayout继承RelativeLayout,重写dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent方法

**

class MyRelativeLayout :RelativeLayout{

    constructor(context: Context):super(context){

    }
    constructor(context: Context, attributeSet: AttributeSet): super(context, attributeSet){

    }

    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int): super(context, attributeSet, defStyleAttr){

    }

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

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

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

}

4.2、在activity的xml中,MyTextView外面嵌套一层MyRelativeLayout
4.3、日志打印结果

**

2021-03-30 18:17:56.680 24022-24022/com.ysl.dispatchstudy E/MainActivity: dispatchTouchEvent ACTION_DOWN
2021-03-30 18:17:56.680 24022-24022/com.ysl.dispatchstudy E/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
2021-03-30 18:17:56.680 24022-24022/com.ysl.dispatchstudy E/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
2021-03-30 18:17:56.680 24022-24022/com.ysl.dispatchstudy E/MyTextView: dispatchTouchEvent ACTION_DOWN
2021-03-30 18:17:56.680 24022-24022/com.ysl.dispatchstudy E/mTextView: OnTouch ACTION_DOWN
2021-03-30 18:17:56.680 24022-24022/com.ysl.dispatchstudy E/MyTextView: onTouchEvent ACTION_DOWN
2021-03-30 18:17:56.760 24022-24022/com.ysl.dispatchstudy E/MainActivity: dispatchTouchEvent ACTION_UP
2021-03-30 18:17:56.760 24022-24022/com.ysl.dispatchstudy E/MyRelativeLayout: dispatchTouchEvent ACTION_UP
2021-03-30 18:17:56.760 24022-24022/com.ysl.dispatchstudy E/MyRelativeLayout: onInterceptTouchEvent ACTION_UP
2021-03-30 18:17:56.760 24022-24022/com.ysl.dispatchstudy E/MyTextView: dispatchTouchEvent ACTION_UP
2021-03-30 18:17:56.760 24022-24022/com.ysl.dispatchstudy E/mTextView: OnTouch ACTION_UP
2021-03-30 18:17:56.760 24022-24022/com.ysl.dispatchstudy E/MyTextView: onTouchEvent ACTION_UP
2021-03-30 18:17:56.761 24022-24022/com.ysl.dispatchstudy E/ysl: mTextView Click

4.4、 *viewGroup的事件流程

根据日志打印结果可以看出
1、触摸事件的传递顺序是activity->viewGroup->view
2、viewGroup通过onInterceptTouchEvent方法对事件进行拦截
true 则事件不会传递给子view
false货super.onInterceptTouchEvent,事件会继续传递给子view
3、在子view中对事件进行了消费,viewGroup将接受不到任何事件\

五、滑动冲突

5.1、滑动冲突产生的原因

当我们内外两层View都可以滑动时候,就会产生滑动冲突。

5.2、滑动冲突的结局方法
1、外部拦截法
重写父viewGroup的onInterceptTouchEvent,根据逻辑在MotionEvent.ACTION_MOVE中进行拦截

**

//伪代码
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        var intercepted = false
        when (ev?.getAction()) {
            MotionEvent.ACTION_DOWN -> {
                intercepted = false
            }
            MotionEvent.ACTION_MOVE -> {
                intercepted = 满足父容器的拦截要求
            }
            MotionEvent.ACTION_UP -> {
                intercepted = false
            }
            else -> {
            }
        }
        return intercepted
    }

注意

a、根据业务逻辑需要,在ACTION_MOVE方法中进行判断,如果需要父View处理则返回true,否则返回false,事件分发给子View去处理
b、ACTION_DOWN 一定返回false,不要拦截它,否则根据View事件分发机制,后续ACTION_MOVE 与 ACTION_UP事件都将默认交给父View去处理
c、原则上ACTION_UP也需要返回false,如果返回true,并且滑动事件交给子View处理,那么子View将接收不到ACTION_UP事件,子View的onClick事件也无法触发。而父View不一样,如果父View在ACTION_MOVE中开始拦截事件,那么后续ACTION_UP也将默认交给父View处理

2、内部拦截法
子view重写dispatchTouchEvent,根据逻辑在MotionEvent.ACTION_MOVE中进行拦截,父view需要重写onInterceptTouchEvent

**

//伪代码
//子view重写dispatchTouchEvent
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {

        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                if (父容器需要此类点击事件) {
                    parent.requestDisallowInterceptTouchEvent(false)
                }
            }
            MotionEvent.ACTION_UP -> {
            }
            else -> {
            }
        }

        return super.dispatchTouchEvent(ev)
    }
//父view重写onInterceptTouchEvent
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        val action: Int = ev!!.action
        return action != MotionEvent.ACTION_DOWN
    }

注意

a、内部拦截法要求父View不能拦截ACTION_DOWN事件,由于ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT标志位控制,一旦父容器拦截ACTION_DOWN那么所有的事件都不会传递给子View
b、滑动策略的逻辑放在子View的dispatchTouchEvent方法的ACTION_MOVE中,如果父容器需要获取点击事件则调用 parent.requestDisallowInterceptTouchEvent(false)方法,让父容器去拦截事件。



上一篇:Android 键盘操作常见问题,2021年Android知识体系总结


下一篇:【View系列】View事件分发源码探索,安卓常见面试题知乎