提出问题
前段时间工作上遇到个UI需求,需要在BottomSheetDialogFragment中嵌套多个fragment,且每个fragment都有个列表需要滑动,但是出现了个问题,弹窗在滑动的时候和列表滑动的时候出现冲突,在外部弹窗完全展开的时候,内部fragment列表无法进行滑动
思考了下,总结分析了需要解决的问题
- 如何保证在弹窗随手势完全展开的时候,列表继续滑动;在收缩的时候,列表滑动回到顶部后,再收缩弹窗?
- 因为Fragment中的RecyclerView接收不到滑动事件,导致无法滑动么?
源码分析
然后我们带着问题去分析,同时在网上搜索了大量相关的问题解答,发现基本都是因为BottomSheetBehavior的ViewPager嵌套RecyclerView导致滑动失效,可以举一反三,其实本质的问题都是类似的
因为BottomSheetDialogFragment手势滑动是通过BottomSheetBehavior来实现,通过启发我们去看了下BottomSheetBehavior的源码,它的代码量不
首先看到nestedScrollingChildRef,它是从绑定了BottomSheetBehavior的child中找可以嵌套滑动的控件
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
然后看下findScrollingChild方法,发现它只支持内部有一个可以上下滑动的控件,多个的话就取第一个
@Nullable
@VisibleForTesting
View findScrollingChild(View view) {
if (ViewCompat.isNestedScrollingEnabled(view)) {
return view;
}
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
for (int i = 0, count = group.getChildCount(); i < count; i++) {
View scrollingChild = findScrollingChild(group.getChildAt(i));
if (scrollingChild != null) {
return scrollingChild;
}
}
}
return null;
}
而且mNestedScrollingChildRef在处理touch事件的时候会用到,那么我们只要修改为在fragment切换的时候重新设置下这个mNestedScrollingChildRef就可以了,乍一想,好像问题变得简单起来了
解决方案
结合网上大部分的解决方案,我们都需要去重新修改BottomSheetBehavior,因为原来的behavior已经不适用这种情况
这里看到很久之前的git就有人提交过Viewpager导致BottomSheet滑动冲突的问题,所幸就在大佬提供的BottomSheetBehavior基础上去修改,有点小遗憾的是这里的版本有点陈旧,后期需要优化下,保持和现版本的功能大致形同。
其实改动很简单,大致就是如下两点
- 在上面说的findScrollingChild方法中,找到viewpager拿到当前的子child
- 然后在viewpager切换页面的时候重新设置下mNestedScrollingChildRef即可
@VisibleForTesting
View findScrollingChild(View view) {
if (ViewCompat.isNestedScrollingEnabled(view)) {
return view;
}
if (view instanceof ViewPager) {
ViewPager viewPager = (ViewPager) view;
View currentViewPagerChild = ViewPagerUtils.getCurrentView(viewPager);
if (currentViewPagerChild == null) {
return null;
}
View scrollingChild = findScrollingChild(currentViewPagerChild);
if (scrollingChild != null) {
return scrollingChild;
}
} else if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
for (int i = 0, count = group.getChildCount(); i < count; i++) {
View scrollingChild = findScrollingChild(group.getChildAt(i));
if (scrollingChild != null) {
return scrollingChild;
}
}
}
return null;
}
....
mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
看到这里,问题基本就迎刃而解了,其实我只要在切换fragment的时候重新设置mNesetdScrollingChildRef,让它去改变需要滑动的子view,就稍微做了以下改动
首先写了个方法去重新刷新mNesetdScrollingChildRef
public void invalidateScrollingChild(View scrollingChildView) {
mNestedScrollingChildRef = new WeakReference<>(scrollingChildView);
}
然后我另外写了个接口去返回当前可滑动的子view,可以是RecyclerView,ListView等等
interface CallBackScrollChild {
fun backScrollChild(scrollChild: View)
}
在fragment的onVisible可见方法中调用
override fun onVisible() {
super.onVisible()
binding?.rvFirst?.let { callBackScrollChild?.backScrollChild(it) }
}
需要在bottomSheetFragment中调用当前behavior中的invalidateScrollingChild方法
override fun backScrollChild(scrollChild: View) {
behavior?.invalidateScrollingChild(scrollChild)
}
可以看下最终效果