Android事件冲突处理

383 阅读4分钟

1. 滑动冲突的常见场景

image.png

  • 场景1:内外View的滑动方向不同,例如viewPager嵌套ListView(当然系统的ViewPager本身已处理)
  • 场景2:内外View的滑动方向相同,例如ViewPager嵌套横向的RecyclerView
  • 场景3:场景1和场景2的叠加复合场景

2. 滑动冲突的两种解决方式

  • 滑动冲突各式各样,需要结合具体的场景选择合适的解决方案,常见的解决滑动冲突的方式有外部拦截法内部拦截法
  • 外部拦截法:所谓外部就是指外层父容器ViewGroup,正如前面所讲的事件分发机制那样,直接重写父容器的onInterceptTouchEvent方法,并在方法内部根据具体的场景进行拦截即可
  • 内部拦截法:所谓内部是指内层元素(View/ViewGroup),内部拦截法的思路是让父元素不拦截任何事件,全都分发给子元素,如果子元素需要就直接消耗掉,不需要再交由父元素处理,通常需要配合requestDisallowInterceptTouchEvent方法进行处理,一般做法是重写子元素的dispatchTouchEvent方法并在合适的位置调用parent.requestDisallowInterceptTouchEventt(boolean disallowIntercept)即可

3. 具体场景代码实例

3.1 不同方向滑动冲突

  • 内外滑动方向不一致时,这个时候就需要判断滑动方向与水平线的角度来判断到底是左右滑动还是垂直滑动,当然实际比较可以比较垂直和水平两个方向的滑动距离绝对值
  • 例如水平方向的HorizontalScrollView嵌套垂直方向的ListView,不做处理时会出现最开始在ListView上斜着滑动时会出现一丝滑动冲突,解决的代码:
/**
 * 外部拦截法解决HorizontalScrollView嵌套ListView的滑动冲突
 * 冲突很小,只会在最最开始斜着滑动时有一些冲突,可以测试在ACTION_MOVE中取消/加上 return false看出来
 *
 * @author LTP  2022/2/28
 */
class MyHorizontalScrollView(context: Context?, attrs: AttributeSet?) :
    HorizontalScrollView(context, attrs) {

    // 记录上一次的横纵坐标
    private var lastX = 0f
    private var lastY = 0f

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = ev.x
                lastY = ev.y
            }
            MotionEvent.ACTION_MOVE -> {
                if (abs(ev.x - lastX) < abs(ev.y - lastY)) {
                    // 当x方向的滑动距离小于y方向的距离时不拦截
                    return false
                }
            }
        }

        // 这里返回super而不是true是由于HorizontalScrollView本身的onInterceptTouchEvent中包含了大量的滑动逻辑
        return super.onInterceptTouchEvent(ev)
    }
}
  • 下面是布局
<com.btpj.eventdispatch.eventconflict.MyHorizontalScrollView
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    tools:context=".eventconflict.EventConflictActivity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent">

        <View
            android:layout_width="300dp"
            android:layout_height="match_parent"
            android:background="@color/_4cd2f5" />

        <ListView
            android:layout_width="300dp"
            android:layout_height="wrap_content"
            android:entries="@array/list_item" />

        <View
            android:layout_width="300dp"
            android:layout_height="match_parent"
            android:background="@color/_6200EE" />
    </LinearLayout>

</com.btpj.eventdispatch.eventconflict.MyHorizontalScrollView>

3.2 相同方向的滑动冲突

  • 当内层与外层View的滑动方向都一致时,就会存在同方向的滑动冲突,至于具体滑动逻辑是怎样的就要根据具体的场景了
  • 以外层ScrollView嵌套内层ListView为例,他们就会出现滑动冲突(当然内层的ListView还会出现获取不到高度的问题,这个不在主题的范围内,直接设置为固定高度即可简单解决)他们的滑动逻辑一般为
    • 当ScrollView没有滑动到底部时,一直由ScrollView本身处理
    • 当ScrollView滑动到底部时:
      • 如果ListView没有滑动到顶部,则交给ListView处理
      • 如果ListView滑动到顶部,分两种情况
        • 向上滑动,则交给listView处理
        • 向下滑动,则交给ScrollView处理
  • 根据具体的滑动逻辑来编写代码
/**
 * 自定义ListView解决与ScrollView的滑动冲突
 *
 * @author LTP  2022/3/4
 */
class MyScrollView(context: Context?, attrs: AttributeSet?) : ScrollView(context, attrs) {

    /** scrollView是否滑动到底部 */
    private var mIsScrollViewToBottom = false
    private var mLastY = 0f

    init {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // 监听ScrollChangeListener判断ScrollView是否滑动到最底部
            setOnScrollChangeListener { v, _, scrollY, _, _ ->
                mIsScrollViewToBottom = v.height + scrollY >= getChildAt(0).height
            }
        }
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        var intercepted = false
        val listView: ListView = (getChildAt(0) as LinearLayout).getChildAt(1) as ListView
        when (ev?.action) {
            MotionEvent.ACTION_MOVE -> {
                intercepted = if (!mIsScrollViewToBottom) {
                    // 如果外层ScrollView没有滑动到最底部,直接由外层ScrollView拦截处理
                    true
                } else {
                    // 外层ScrollView已经滑动到最底部
                    if (!isListViewToTop(listView)) {
                        // 外层ScrollView已经滑动到最底部,且ListView没有处于最顶部,则不拦截交给ListView处理
                        false
                    } else {
                        // 外层ScrollView已经滑动到最底部,且ListView已经处于最顶部
                        // 通过判断当前滑动是否是向上还是向下,向上(ev.y > mLastY)则交给scrollView(intercepted=true),
                        // 向下(ev.y < mLastY)则交给ListView(intercepted=false)
                        ev.y > mLastY
                    }
                }
            }
        }
        mLastY = ev?.y ?: 0f
        // 必须调用父类的onInterceptTouchEvent,ScrollView内部会有一些滑动处理,不调用会导致无法滑动
        super.onInterceptTouchEvent(ev)
        return intercepted
    }

    /**
     * 判断ListView是否滑动到最顶部
     */
    private fun isListViewToTop(listView: ListView): Boolean {
        if (listView.firstVisiblePosition == 0) {
            val firstChildView = listView.getChildAt(0)
            return firstChildView != null && firstChildView.top >= 0
        }
        return false
    }
}
<com.btpj.views.eventconflict.same.MyScrollView 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"
    android:orientation="vertical"
    tools:context="com.btpj.views.eventconflict.same.SameEventConflictActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <View
            android:layout_width="match_parent"
            android:layout_height="400dp"
            android:background="@color/_4cd2f5" />

        <ListView
            android:layout_width="match_parent"
            android:layout_height="800dp"
            android:entries="@array/list_item" />

    </LinearLayout>
</com.btpj.views.eventconflict.same.MyScrollView>

4. 总结

  • 了解事件冲突能更好的了解事件冲突解决的原理,常见解决时间冲突的方法包括外部拦截法以及内部拦截法,通常来说外部拦截法更加简洁容易
  • 一般常见的官方提供的控件内部也有帮我们做事件冲突的处理,但完全自定义ViewGroup则经常涉及到事件冲突的处理,这时就得自己动手去解决了,后面会结合自定义View来讲解一些自定义View涉及到的事件冲突的处理