Android中View的滑动冲突

ps:本文系转载文章,阅读原文可获取源码,文章末尾有原文链接

ps:本文的 demo 是基于 kotlin 语言来写的

在 Android 开发中,如果界面内外两层同时可以滑动,那么就会产生滑动冲突;那 View 产生滑动冲突的都有那几种情况呢?产生滑动冲突的无非是以下3种情况:

1)外部滑动方向和内部滑动方向不一致,比如最外层 View 可以左右滑动,内层 View 可以上下滑动

2)外部滑动方向和内部滑动方向一致

3)以上两种情况的嵌套

1)和 2)这种情况我们很常见,先说 1)吧,假设我们自定义了一个 ViewPager 并允许它的子元素可以滑动,当它和 ListView 一起使用的时候就会产生 1)这种情况就会出现滑动卡顿甚至滑动不了;2)呢,当我们用 ScrollView 和 RecyclerView 搭配使用并忘记解决滑动冲突时,2)这种情况就会出现滑动卡顿甚至滑动不了;其实产生滑动冲突的,无非是系统非法分辨用户想要滑动的是外部 View 还是内部 View。

在情况 1)中,我们的解决方案是这样的,移动的过程中,获取到 X 轴和 Y 轴上位移的绝对值,通过对比 X 轴和 Y 轴上的位移,当 X 轴的位移绝对值大于等于 Y轴的位移绝对值时,就拦截内部 View 的触摸事件,外部 View 就会消费事件;当 X 轴的位移绝对值小于 Y轴的位移绝对值时,就允许内部 View 的进行触摸事件,那么此时外部 View 就不会消费触摸事情。

在情况 2)中,我们无法根据滑动的角度、距离差以及速度差来做判断,但是我们可以在业务的需求上做出判断,比如需求规定:当内部 View 先开始滑动并消费事件,滑动到一半后就拦截内部 View 触摸事件并由外部 View 消费,有了处理规则同样可以进行下一步处理。

在情况 3)中,它的滑动规则和情况 2)一样复杂,它也无法直接根据滑动的角度、距离差以及速度差来做判断,但是也是可以从业务的需求上找到解决方案的,和 2)一样类似的处理规则。

为了更好的理解,我们以情况 1)进行举例,情况 2)和情况 3)就不再举例了,感兴趣的读者可以对情况 2)和情况 3)进行实现。

首先我们对 1)制造一个滑动冲突;

1、制造滑动冲突

(1)新建一个 kotlin 语言类型的类 MyListView 并继承于 ListView:

class MyListView: ListView {

var lastX: Int = 0
var lastY: Int = 0

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

}
constructor(context: Context, @Nullable attrs: AttributeSet): super(context,attrs) {

}
constructor(context: Context, @Nullable attrs: AttributeSet, defStyleAttr: Int): super(context,attrs,defStyleAttr) {

}

}

(2)新建一个 kotlin 语言类型的类 MyViewPager 并继承于 ViewPager :

class MyViewPager: ViewPager {

companion object {

    /**
     * 1、表示制造一个滑动冲突
     * 2、表示用外部拦截法解决滑动冲突
     * 3、表示用内部拦截法解决滑动冲突
     */
    var flag: Int = 0;
}
var lastXIntercept: Int = 0
var lastYIntercept: Int = 0
constructor(context: Context): super(context) {
}
constructor(context: Context,@Nullable attrs: AttributeSet): super(context,attrs) {
}

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    if (flag == 1) {
        return forbidInterceptTouchEvent(ev);
    }
    return super.onInterceptTouchEvent(ev)
}
fun forbidInterceptTouchEvent(ev: MotionEvent?): Boolean {
    Log.d(MainActivity.TAG,"--forbidInterceptTouchEvent--")
    return false
}

}

(3)新建一个 kotlin 语言类型的类 ViewPagerAdapter 并继承于 PagerAdapter :

class ViewPagerAdapter: PagerAdapter {

val views: List?
constructor(list: List){
    this.views = list
}

override fun getCount(): Int {
    return views!!.size
}

override fun instantiateItem(container: ViewGroup, position: Int): Any {
    val view = views!!.get(position)
    container.addView(view)
    return view
}

override fun isViewFromObject(view: View, obj: Any): Boolean {
    return view === obj
}

override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
    container.removeView(obj as View)
}

}

(4)新建一个 kotlin 语言类型的 Activity,名叫 SlideCollideActivity :

class SlideCollideActivity : AppCompatActivity() {

var viewPager: ViewPager? = null
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_slide_collide)
    viewPager = findViewById(R.id.viewPager)
    var viewList = java.util.ArrayList()
    for (i in 0..3) {
        val listView = MyListView(this)
        val dataList = java.util.ArrayList()
        for (i in 0..29) {
            dataList.add("数据 $i")
        }
        val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, dataList)
        listView.setAdapter(adapter)
        viewList!!.add(listView)
    }
    viewPager!!.setAdapter(ViewPagerAdapter(viewList))
}

}

(5)SlideCollideActivity 对应的布局文件 activity_slide_collide.xml 如下所示:


android:id="@+id/viewPager"
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.xe.slidecollidedemo.SlideCollideActivity">

首先我们将 MyViewPager 类中的 flag 属性置为 1,再运行程序,界面展示如下所示:

图片

当我向左滑动的时候,发现已经滑动不了了,但打印如下日志:

09-13 18:23:46.023 15084-15084/com.xe.slidecollidedemo D/MainActivity: --forbidInterceptTouchEvent--
09-13 18:23:46.033 15084-15084/com.xe.slidecollidedemo D/MainActivity: --forbidInterceptTouchEvent--
09-13 18:23:46.056 15084-15084/com.xe.slidecollidedemo D/MainActivity: --forbidInterceptTouchEvent--
09-13 18:23:46.080 15084-15084/com.xe.slidecollidedemo D/MainActivity: --forbidInterceptTouchEvent--

我们知道,ViewPager 内部已经做好了滑动冲突的处理,当我们自定义一个 ViewPager 并重写 它的 onInterceptTouchEvent 方法让该方法的返回值为 false 时,它就理所当然的产生滑动冲突了,因为 MyListView 和 MyViewPager 都可以滑动,所以系统无法识别该滑动谁。

下面我们来解决滑动冲突,在日常的开发中,我一般用以下2种方法解决滑动冲突,那就是外部拦截法和内部拦截法。

2、外部拦截法

外部拦截法是指点击事情都先经过父容器的拦截处理,如果父容器需要此事件就拦截,那么父容器就会消费事件;如果不需要此事件就不拦截,就交给子元素去消费事件,这样就可以解决滑动冲突的问题;外部拦截法需要重写父容器的 onInterceptTouchEvent 方法,这种方法比较符合点击事件的分发机制。

我们在 1)滑动冲突的 demo 上稍微改一下代码;

(1)在 MyListView 类中重写一下 onTouchEvent 方法:

override fun onTouchEvent(ev: MotionEvent): Boolean {

    val b = super.onTouchEvent(ev)
    var s = "s"
    when (ev.action) {
        MotionEvent.ACTION_DOWN -> s = "--MyListView--onTouchEvent--MotionEvent.ACTION_DOWN--$b"
        MotionEvent.ACTION_MOVE -> s = "--MyListView--onTouchEvent--MotionEvent.ACTION_MOVE--$b"
        MotionEvent.ACTION_UP -> s = "--MyListView--onTouchEvent--MotionEvent.ACTION_UP--$b"
    }
    Log.d(MainActivity.TAG, s)
    return b

}

(2)将 MyViewPager 类的 flag 置为2,并添加 externalIntercept 方法和改一下 onInterceptTouchEvent 方法:

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {

    if (MyViewPager.flag == 1) {
        return forbidInterceptTouchEvent(ev);
    } else if (MyViewPager.flag == 2) {
        return externalIntercept(ev)
    } 
    return super.onInterceptTouchEvent(ev)

}
fun externalIntercept(ev: MotionEvent?): Boolean {

    var intercepted = false
    val x = ev!!.getX().toInt()
    val y = ev!!.getY().toInt()
    val action = ev.getAction() and MotionEvent.ACTION_MASK
    when (action) {
        MotionEvent.ACTION_DOWN -> {
            intercepted = false

            //调用 ViewPager的 onInterceptTouchEvent 方法用于初始化 mActivePointerId
            super.onInterceptTouchEvent(ev)
        }
        MotionEvent.ACTION_MOVE -> {
            val deltaX = x - lastXIntercept
            val deltaY = y - lastYIntercept
            intercepted = Math.abs(deltaX) > Math.abs(deltaY)
        }
        MotionEvent.ACTION_UP -> {
            intercepted = false
        }
    }
    lastXIntercept = x
    lastYIntercept = y
    return intercepted

}

程序再次运行,当我们向左滑动时,发现可以滑动了,并打印如下日志:

09-13 21:05:04.172 27291-27291/com.xe.slidecollidedemo D/MainActivity: --MyViewPager--onTouchEvent--MotionEvent.ACTION_MOVE--true
09-13 21:05:04.272 27291-27291/com.xe.slidecollidedemo D/MainActivity: --MyViewPager--onTouchEvent--MotionEvent.ACTION_MOVE--true
09-13 21:05:04.279 27291-27291/com.xe.slidecollidedemo D/MainActivity: --MyViewPager--onTouchEvent--MotionEvent.ACTION_UP--true

当我们向下滑动时,也能滑动,也并打印如下日志:

09-13 21:06:12.948 27291-27291/com.xe.slidecollidedemo D/MainActivity: --MyListView--onTouchEvent--MotionEvent.ACTION_DOWN--true
09-13 21:06:12.976 27291-27291/com.xe.slidecollidedemo D/MainActivity: --MyListView--onTouchEvent--MotionEvent.ACTION_MOVE--true
09-13 21:06:13.046 27291-27291/com.xe.slidecollidedemo D/MainActivity: --MyListView--onTouchEvent--MotionEvent.ACTION_MOVE--true

我们重写了 MyViewPager 的 onInterceptTouchEvent 方法,并在该方法进行了滑动冲突的处理,在 MyViewPager 的 down 事件和 up 事件中并没有做滑动处理,当左右滑动距离的绝对值大于上下距离滑动的绝对值时,MyViewPager 就进行事件拦截,并让自己消费;否则就不拦截事件,并交给子元素 MyListView 消费。

3、内部拦截法

内部拦截法是指父容器不直接拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就调用允许父元素拦截的语句从而交由父容器进行拦截处理,这种方法和 Android 中的事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起来较外部拦截法稍显复杂。

我们在外部拦截法的基础上改一下;

(1)将 MyViewPager 类的 flag 属性置为 3,添加 internalIntercept 方法并修改一下 onInterceptTouchEvent 方法:

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {

    if (flag == 1) {
        return forbidInterceptTouchEvent(ev);
    } else if (flag == 2) {
        return externalIntercept(ev)
    } else if (flag == 3) {
        return internalIntercept(ev)
    }
    return super.onInterceptTouchEvent(ev)

}

fun internalIntercept(ev: MotionEvent?): Boolean {

    val action = ev!!.getAction() and MotionEvent.ACTION_MASK
    var intercepted: Boolean = true;
    when (action) {
        MotionEvent.ACTION_DOWN -> {
            intercepted = false
            super.onInterceptTouchEvent(ev)
            Log.d(MainActivity.TAG,"--MyViewPager--internalIntercept--MotionEvent.ACTION_DOWN")
        }
        MotionEvent.ACTION_MOVE -> {
            intercepted = true
            Log.d(MainActivity.TAG,"--MyViewPager--internalIntercept--MotionEvent.ACTION_MOVE")
        }
        MotionEvent.ACTION_UP -> {
            intercepted = false
            Log.d(MainActivity.TAG,"--MyViewPager--internalIntercept--MotionEvent.ACTION_UP")
        }
    }
    return intercepted

}

(2)在 MyListView 中重写一下 dispatchTouchEvent 方法:

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {

    if (MyViewPager.flag == 3) {
        internalIntercept(ev)
    }
    return super.dispatchTouchEvent(ev)

}

我们再次运行,向左滑动时也能进行滑动,日志并打印如下所示:

09-13 21:43:27.257 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyViewPager--internalIntercept--MotionEvent.ACTION_DOWN
09-13 21:43:27.258 32062-32062/com.xe.slidecollidedemo D/MainActivity: --internalIntercept--
09-13 21:43:27.259 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyListView--onTouchEvent--MotionEvent.ACTION_DOWN--true
09-13 21:43:27.269 32062-32062/com.xe.slidecollidedemo D/MainActivity: --internalIntercept--
09-13 21:43:27.270 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyListView--onTouchEvent--MotionEvent.ACTION_MOVE--true
09-13 21:43:27.285 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyViewPager--internalIntercept--MotionEvent.ACTION_MOVE
09-13 21:43:27.285 32062-32062/com.xe.slidecollidedemo D/MainActivity: --internalIntercept--
09-13 21:43:27.285 32062-32062/com.xe.slidecollidedemo D/MainActivity: s
09-13 21:43:27.303 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyViewPager--onTouchEvent--MotionEvent.ACTION_MOVE--true
09-13 21:43:27.386 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyViewPager--onTouchEvent--MotionEvent.ACTION_MOVE--true
09-13 21:43:27.391 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyViewPager--onTouchEvent--MotionEvent.ACTION_UP--true

当我们向下滑动时,也能进行滑动,并打印如下日志:

09-13 21:44:54.420 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyListView--onTouchEvent--MotionEvent.ACTION_MOVE--true
09-13 21:44:54.435 32062-32062/com.xe.slidecollidedemo D/MainActivity: --internalIntercept--
09-13 21:44:54.437 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyListView--onTouchEvent--MotionEvent.ACTION_MOVE--true
09-13 21:44:54.486 32062-32062/com.xe.slidecollidedemo D/MainActivity: --internalIntercept--
09-13 21:44:54.487 32062-32062/com.xe.slidecollidedemo D/MainActivity: --MyListView--onTouchEvent--MotionEvent.ACTION_UP--true

我们分析一下,父元素 MyViewPager 的 down 事件和 up 事件是不拦截事件的;当我们只向下滑动的时候,down 事件能传递到子元素 MyListView 中,并在 MyListView 的 dispatchTouchEvent 方法中调用 internalIntercept 方法,ternalIntercept 方法在 down 事件中调用 parent.requestDisallowInterceptTouchEvent(true) 代码,目的是不要执行父元素 MyViewPager 的 onInterceptTouchEvent 方法;当我们左右滑动时,子元素 MyListView 的 move 事件的 parent.requestDisallowInterceptTouchEvent(false) 代码就会被调用,该行代码的目的是让父元素 MyViewPager 的 onInterceptTouchEvent 方法执行,父元素 MyViewPager 的 move 事件刚好是拦截事件。

你可能感兴趣的:(Android中View的滑动冲突)