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

    推荐阅读