持续创作,加速成长!这是我参与「掘金日新计划 · 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
}
那么使用就是这样:
如果想要嵌套滚动就在 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会有嵌套效果吗?,看看效果
啊?这?能嵌套了?默认的ViewPager就支持嵌套了?
是的,对于ViewPager嵌套问题,之前的老版本确实是需要自定义处理拦截事件,但是新版本的ViewPager已经帮我们处理了嵌套效果。
源码中对 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都行,没道理我不行,来运行试试
这…怎么又不行了? 现实连着扇了我两巴掌。那ViewPager2内部肯定没有写嵌套的兼容代码。
由于ViewPager2内部是RV实现的,这里只重新了RV的拦截事件
并没有嵌套滚动的逻辑,那我知道了,我们像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>
其他的代码没变化,运行效果:
这样就能达到和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是一样的效果:
那么 ViewPager2 嵌套 ViewPager 呢?
这种情况下如果我们不加中间层,和ViewPager2的嵌套是一样的,父布局直接无法分发事件下来。
所以我们还是需要加上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 确是不会接受到事件自己动起来。
其实效果就是请求拦截但是没有拦截到,还是子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
}
修改之后的效果如下:
虽然效果都能实现,但是我不相信真有兄弟在实战中会这么嵌套吧 😂 😅,搞这些骚操作还是为了更深入的理解和学习。
总结
新版本的ViewPager可以很方便的自带嵌套效果,我们使用起来确实很方便,ViewPager2也可以通过加一个Host中间层来实现同样的效果。包括Host在其他嵌套的场景下的使用,这个思路很重要。
但是ViewPager只能固定方向,而ViewPager2通过RV实现更加的灵活,不仅可以自定义方向,还能自适应高度,这一点特性在一些特定的场景下面就很方便。
一般普通的场景我们可以使用ViewPager快速实现即可,一些特殊的效果我们可以通过ViewPager2来实现!如果想要嵌套效果我们也可以通过Host中间层来解决事件传递问题。
看到大家都吐槽 ViewPager2 我就放心了,原来不是我一个人在痛苦 😂 😅 ,实际开发中大家都遇到了哪些 ViewPager2 的坑,欢迎大家来评论区来吐槽哦,收集一下看看都有哪些问题,方便大家学习交流,也防止各位高工们踩雷与避坑。
好了本文的全部代码与Demo都已经开源。有兴趣可以看这里,可供大家参考学习。
惯例,我如有讲解不到位或错漏的地方,希望同学们可以指出交流。
如果感觉本文对你有一点点的启发,还望你能点赞
支持一下,你的支持是我最大的动力。
Ok,这一期就此完结。
今天的文章ViewPager2:ViewPager都能自动嵌套滚动了,我不行?我麻了!该怎么做?分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20914.html