乐趣区

关于android: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<View>?
constructor(list: List<View>){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<View>()
    for (i in 0..3) {val listView = MyListView(this)
        val dataList = java.util.ArrayList<String>()
        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"
        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 事件刚好是拦挡事件。

退出移动版