【日常小問題】解決BottomSheetDialogFragment中多個fragment滑動衝突

語言: CN / TW / HK

highlight: androidstudio theme: smartblue


提出問題

前段時間工作上遇到個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基礎上去修改,有點小遺憾的是這裡的版本有點陳舊,後期需要優化下,保持和現版本的功能大致形同。

先上個地址 : (https://github.com/laenger/ViewPagerBottomSheet)

其實改動很簡單,大致就是如下兩點

  • 在上面說的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)   }

可以看下最終效果

test.gif

附上demo地址:https://github.com/RainyJiang22/ViewPagerBottomSheetBehavior

對於BottomSheetBehavior的限制,以上方法僅供參考,如果還有更好的方式,歡迎各位大佬留言