关于android:View的事件分发及滑动冲突的解决

44次阅读

共计 9339 个字符,预计需要花费 24 分钟才能阅读完成。

一、触摸事件的类型
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) 办法,让父容器去拦挡事件.

正文完
 0