ViewPager2:ViewPager都能自动嵌套滚动了,我不行?我麻了!该怎么做?

ViewPager2:ViewPager都能自动嵌套滚动了,我不行?我麻了!该怎么做?事情是这样的,今天玩WY云音乐,发现他们的ViewPager非常的顺滑,多层嵌套下面的ViewPager都能顺滑的滑动。当时就有一个思考,如果使用ViewPager2嵌套实现会不会有不同呢?

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

记录ViewPager与ViewPager2嵌套的问题解决

前言

事情是这样的,今天玩WY云音乐,发现他们的ViewPager非常的顺滑,多层嵌套下面的ViewPager都能顺滑的滑动。当时就有一个思考,如果使用ViewPager2嵌套实现会不会有不同呢?

之前也看评论区有到说ViewPager2的嵌套滚动问题,然后我这里实验一下ViewPager多层嵌套下的滚动问题。

记录与测试一下 ViewPager 与 ViewPager2 的嵌套不同点。不同的ViewPager嵌套的不同点,ViewPager嵌套ViewPager2,与ViewPager2嵌套ViewPager有什么不同。

那我们直接开始吧。

ViewPager的嵌套滚动

从小爸妈就对我讲,黄梅戏可… 哎呀,什么鬼,串戏了😅 😂

不是,是学Android开始,老师就跟我们讲,ViewPager 嵌套 ViewPager ,我们要处理事件冲突的,很麻烦,我们需要自定义ViewPager自己处理,然后我们网上找的自定义ViewPager大致是这样:

public class MyViewPager extends ViewPager {

    int lastX = -1;
    int lastY = -1;

    public MyViewPager(Context context) {
        super(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getRawX();
        int y = (int) ev.getRawY();
        int dealtX = 0;
        int dealtY = 0;

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dealtX = 0;
                dealtY = 0;
                // 保证子View能够接收到Action_move事件
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                dealtX += Math.abs(x - lastX);
                dealtY += Math.abs(y - lastY);
 
                // 这里是否拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
                if (dealtX >= dealtY) { // 左右滑动请求父 View 不要拦截
                    getParent().requestDisallowInterceptTouchEvent(true);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
            case MotionEvent.ACTION_UP:
                break;

        }
        return super.dispatchTouchEvent(ev);
    }
}

ViewPager的嵌套滚动处理,实际上就是看子View是不是能滚动,如果子View需要滚动,就让父容器不要拦截,否则就让父容器拦截,

为了对比效果,我们先不用自定义ViewPager,我们先使用默认的ViewPager来实现对比一下:

    <androidx.viewpager.widget.ViewPager android:id="@+id/viewpager" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"/>

先设置一个ViewPager

    findViewById<ViewPager>(R.id.viewpager).apply {

        bindFragment(
            supportFragmentManager,
            listOf(VPItemFragment(Color.RED), VPItemFragment(Color.GREEN), VPItemFragment(Color.BLUE)),
            behavior = 1
            )
    }

扩展方法如下:

fun ViewPager.bindFragment( fm: FragmentManager, fragments: List<Fragment>, pageTitles: List<String>? = null, behavior: Int = 0 ): ViewPager {
    offscreenPageLimit = fragments.size - 1
    adapter = object : FragmentStatePagerAdapter(fm, behavior) {
        override fun getItem(p: Int) = fragments[p]
        override fun getCount() = fragments.size
        override fun getPageTitle(p: Int) = if (pageTitles == null) null else pageTitles[p]
    }
    return this
}

那么使用就是这样:

vp_01.gif

如果想要嵌套滚动就在 VPItemFragment 中设置一个子ViewPager容器。

        view.findViewById<ViewPager>(R.id.viewPager).apply {

            val fragmentList = listOf(VPItemChildFragment(0), VPItemChildFragment(1), VPItemChildFragment(2))
            bindFragment(
                childFragmentManager,
                fragmentList
               behavior = 1
            )



}

由于我们并没有使用自定义ViewPager,这样实现默认的ViewPager会有嵌套效果吗?,看看效果

vp_02.gif

啊?这?能嵌套了?默认的ViewPager就支持嵌套了?

是的,对于ViewPager嵌套问题,之前的老版本确实是需要自定义处理拦截事件,但是新版本的ViewPager已经帮我们处理了嵌套效果。

image.png

image.png

源码中对 onTouchEvent 和 onInterceptTouchEvent 已经处理了事件的拦截,默认就支持嵌套滚动了。所以之前的自定义 MyViewPager 目前来说没什么用。

ViewPager2的嵌套滚动

既然ViewPager默认就支持嵌套滚动了,那么我想ViewPager2肯定也行,毕竟它是ViewPager的升级版本嘛。

简单的试试?

我们把ViewPager换成ViewPager2,然后绑定到Fragment对象。

    override fun init() {

        findViewById<ViewPager2>(R.id.viewpager2).apply {

            bindFragment(
                supportFragmentManager,
                lifecycle,
                listOf(VPItemFragment(Color.RED), VPItemFragment(Color.GREEN), VPItemFragment(Color.BLUE)),
            )

            orientation = ViewPager2.ORIENTATION_HORIZONTAL
        }

    }

子Fragment内部再使用ViewPager2嵌套,区别于ViewPager,我把ViewPager2的背景设置为灰色。

        view.findViewById<ViewPager2>(R.id.viewPager2).apply {

            val fragmentList = listOf(VPItemChildFragment(0), VPItemChildFragment(1), VPItemChildFragment(2))
            bindFragment(
                childFragmentManager,
                lifecycle,
                fragmentList
            )

            orientation = ViewPager2.ORIENTATION_HORIZONTAL

ViewPager都行,没道理我不行,来运行试试

vp_04.gif

这…怎么又不行了? 现实连着扇了我两巴掌。那ViewPager2内部肯定没有写嵌套的兼容代码。

由于ViewPager2内部是RV实现的,这里只重新了RV的拦截事件

image.png

并没有嵌套滚动的逻辑,那我知道了,我们像ViewPager一样,继承自 ViewPager2 然后重写嵌套滚动的逻辑不就行了吗!还真不行, ViewPager2 是finnal的无法继承,那怎么办?

其实我们的目的就是嵌套的父容器的事件需要传递到子容器中,既然我们不能直接继承修改,那么我们可以加一个中间层,使用一个ViewGroup包裹我们嵌套的容器,在中间层中判断是否需要传递和拦截事件。

谷歌已经给出了推荐的代码

/** * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout. * * This solution has limitations when using multiple levels of nested scrollable elements * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2). */

class NestedScrollableHost : FrameLayout {

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    private var touchSlop = 0
    private var initialX = 0f
    private var initialY = 0f

    private val parentViewPager: ViewPager2?
        get() {
            var v: View? = parent as? View
            while (v != null && v !is ViewPager2) {
                v = v.parent as? View
            }
            return v as? ViewPager2
        }

    private val child: View? get() = if (childCount > 0) getChildAt(0) else null

    init {
        touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    }

    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
        val direction = -delta.sign.toInt()
        return when (orientation) {
            0 -> child?.canScrollHorizontally(direction) ?: false
            1 -> child?.canScrollVertically(direction) ?: false
            else -> throw IllegalArgumentException()
        }
    }

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        handleInterceptTouchEvent(e)
        return super.onInterceptTouchEvent(e)
    }

    private fun handleInterceptTouchEvent(e: MotionEvent) {
        val orientation = parentViewPager?.orientation ?: return
        // Early return if child can't scroll in same direction as parent
        if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
            return
        }

        if (e.action == MotionEvent.ACTION_DOWN) {
            initialX = e.x
            initialY = e.y
            parent.requestDisallowInterceptTouchEvent(true)
        } else if (e.action == MotionEvent.ACTION_MOVE) {
            val dx = e.x - initialX
            val dy = e.y - initialY
            val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
            // assuming ViewPager2 touch-slop is 2x touch-slop of child
            val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f

            if (scaledDx > touchSlop || scaledDy > touchSlop) {

                if (isVpHorizontal == (scaledDy > scaledDx)) {
                    // Gesture is perpendicular, allow all parents to intercept
                    parent.requestDisallowInterceptTouchEvent(false)
                } else {
                    // Gesture is parallel, query child if movement in that direction is possible
                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        // Child can scroll, disallow all parents to intercept
                        parent.requestDisallowInterceptTouchEvent(true)
                    } else {
                        // Child cannot scroll, allow all parents to intercept
                        parent.requestDisallowInterceptTouchEvent(false)
                    }
                }
            }
        }
    }
}

我们使用NestedScrollableHost把内部ViewPager2包裹起来就可以了,在onInterceptTouchEvent函数中,如果接受到了DOWN事件,就需要调用requestDisallowInterceptTouchEvent通知外层的ViewPager2不要拦截事件,让我们的Host来处理滑动事件。

当Move事件触发的时候,判断一下内部的ViewPager2是否能滑动?不能滑动就通知父布局要拦截事件。

使用,我们嵌套内部的ViewPager之后,在运行代码试试

    <com.guadou.kt_demo.demo.demo16_record.viewpager.NestedScrollableHost android:layout_width="match_parent" android:layout_height="match_parent">

        <androidx.viewpager2.widget.ViewPager2 android:id="@+id/viewPager2" android:layout_width="match_parent" android:layout_height="match_parent" />

    </com.guadou.kt_demo.demo.demo16_record.viewpager.NestedScrollableHost>

其他的代码没变化,运行效果:

vp_03.gif

这样就能达到和ViewPager一样的效果了。

这个思路也是很妙,在两者中间一个中间层,事件还不是我们自己说了算,想什么时候传递就什么时候传递,想哪个方向传递就哪个方向传递,可以请求父类不要拦截,也可以自己不往下分发,非常的灵活。

那么不仅仅是这一个ViewPager2的嵌套场景,其实ViewPager2嵌套RV,甚至ScrollView嵌套RV等场景都可以灵活的应用。

ViewPager与ViewPager2的相互嵌套

好的到此我们就结束了,什么? 你要 ViewPager 嵌套 ViewPager2 ?真会玩。

其实和ViewPager2嵌套ViewPager2一样的道理,加Host中间层。即可

    <androidx.viewpager.widget.ViewPager android:id="@+id/viewpager" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"/>
    <TextView android:id="@+id/ll_root" android:layout_width="match_parent" android:layout_height="@dimen/d_100dp" android:gravity="center" android:text="标记此父容器的背景颜色" />


    <com.guadou.kt_demo.demo.demo16_record.viewpager.NestedScrollableHost android:layout_width="match_parent" android:layout_height="match_parent">

        <androidx.viewpager2.widget.ViewPager2 android:id="@+id/viewPager2" android:layout_width="match_parent" android:layout_height="match_parent" />

    </com.guadou.kt_demo.demo.demo16_record.viewpager.NestedScrollableHost>
    

ViewPager嵌套ViewPager2是一样的效果:

vp_05.gif

那么 ViewPager2 嵌套 ViewPager 呢?

这种情况下如果我们不加中间层,和ViewPager2的嵌套是一样的,父布局直接无法分发事件下来。

vp_06.gif

所以我们还是需要加上Host中间层,帮助我们分发事件

    <com.guadou.kt_demo.demo.demo16_record.viewpager.NestedScrollableHost android:layout_width="match_parent" android:layout_height="match_parent">

        <androidx.viewpager.widget.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" />

    </com.guadou.kt_demo.demo.demo16_record.viewpager.NestedScrollableHost>

加上中间层就好了吗,这个嘛其实情况就不同了,之前的情况下是 ViewPager 是父布局,那么我们请求父布局不要拦截,他就自己处理了,但是当我们的父布局是 ViewPager2 的情况下,我们请求他不要拦截,其实执行的逻辑是一致的,但是 ViewPager2 确是不会接受到事件自己动起来。

vp_07.gif

其实效果就是请求拦截但是没有拦截到,还是子View在响应Touch事件,此时我们需要在中间层自己处理事件的拦截。关于View事件的分发,我想大家应该都比我我强,我就不献丑了。

我们如果想要ViewPager2为父布局,在请求拦截的时候可以自动滚动,我们直接修改中间层的 dispathTouchEvent 和 onInterceptTouchEvent 方法都是可以的,由于之前的代码是处理的 onInterceptTouchEvent 方法,所以我们还是在这个基础上修改,如果子View不能滚动了,那么我们中间层不往下分发事件即可,此时事件会传递到 ViewPager2 的 OnTouchEvent 中即可自动滚动了。

主要修改方法如下:

  private fun handleInterceptTouchEvent(e: MotionEvent): Boolean {

        val orientation = parentViewPager?.orientation ?: return false

        if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
            return false
        }

        if (e.action == MotionEvent.ACTION_DOWN) {
            initialX = e.x
            initialY = e.y
            parent.requestDisallowInterceptTouchEvent(true)

        } else if (e.action == MotionEvent.ACTION_MOVE) {

            val dx = e.x - initialX
            val dy = e.y - initialY
            val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL

            val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f

            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                return if (isVpHorizontal == (scaledDy > scaledDx)) {
                    //垂直的手势拦截
                    parent.requestDisallowInterceptTouchEvent(false)
                    true
                } else {

                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        //子View能滚动,不拦截事件
                        parent.requestDisallowInterceptTouchEvent(true)
                        false
                    } else {
                        //子View不能滚动,直接就拦截事件
                        parent.requestDisallowInterceptTouchEvent(false)
                        true
                    }
                }
            }
        }

        return false
    }

修改之后的效果如下:

vp_08.gif

虽然效果都能实现,但是我不相信真有兄弟在实战中会这么嵌套吧 😂 😅,搞这些骚操作还是为了更深入的理解和学习。

总结

新版本的ViewPager可以很方便的自带嵌套效果,我们使用起来确实很方便,ViewPager2也可以通过加一个Host中间层来实现同样的效果。包括Host在其他嵌套的场景下的使用,这个思路很重要。

但是ViewPager只能固定方向,而ViewPager2通过RV实现更加的灵活,不仅可以自定义方向,还能自适应高度,这一点特性在一些特定的场景下面就很方便。

一般普通的场景我们可以使用ViewPager快速实现即可,一些特殊的效果我们可以通过ViewPager2来实现!如果想要嵌套效果我们也可以通过Host中间层来解决事件传递问题。

看到大家都吐槽 ViewPager2 我就放心了,原来不是我一个人在痛苦 😂 😅 ,实际开发中大家都遇到了哪些 ViewPager2 的坑,欢迎大家来评论区来吐槽哦,收集一下看看都有哪些问题,方便大家学习交流,也防止各位高工们踩雷与避坑。

好了本文的全部代码与Demo都已经开源。有兴趣可以看这里,可供大家参考学习。

惯例,我如有讲解不到位或错漏的地方,希望同学们可以指出交流。

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。

ViewPager2:ViewPager都能自动嵌套滚动了,我不行?我麻了!该怎么做?

今天的文章ViewPager2:ViewPager都能自动嵌套滚动了,我不行?我麻了!该怎么做?分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20914.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注