Android中View的滑动冲突

本文详细介绍了在Android开发中如何处理界面内外两层滑动冲突,主要讨论了三种情况:1)外部滑动方向与内部不一致,2)方向一致,3)嵌套情况。通过示例代码展示了如何运用外部拦截法和内部拦截法解决滑动冲突,包括自定义ViewPager和ListView的交互处理,以及在不同滑动方向上的事件拦截策略。
摘要由CSDN通过智能技术生成

阅读原文可获取demo 代码

原文链接:https://mp.weixin.qq.com/s/9smtvABi3kTkSUkmLFCanQ

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 如下所示:

<?xml version="1.0" encoding="utf-8"?>

<com.xe.views.MyViewPager
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”>

</com.xe.views.MyViewPager>

首先我们将 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 " M o t i o n E v e n t . A C T I O N M O V E − > s = " − − M y L i s t V i e w − − o n T o u c h E v e n t − − M o t i o n E v e n t . A C T I O N M O V E − − b" MotionEvent.ACTION_MOVE -> s = "--MyListView--onTouchEvent--MotionEvent.ACTION_MOVE-- b"MotionEvent.ACTIONMOVE>s="MyListViewonTouchEventMotionEvent.ACTIONMOVEb”
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 事件刚好是拦截事件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值