Android ViewPager with RecyclerView works incorrectly inside BottomSheet

后端 未结 7 1019
故里飘歌
故里飘歌 2020-12-13 02:51

When I try to scroll list, sometimes this works incorrect - BottomSheet intercepts the scroll event and hides.

How to reproduce this:

  1. Open Bottom Sheet
相关标签:
7条回答
  • 2020-12-13 03:09

    I have also been in this situation recently, and I've used the following custom viewpager class instead of the viewpager(on XML), and it worked very well, I think it will help you and others):

    
    import android.content.Context
    import android.util.AttributeSet
    import android.view.View
    import androidx.viewpager.widget.ViewPager
    import java.lang.reflect.Field
    
    class BottomSheetViewPager(context: Context, attrs: AttributeSet?) : ViewPager(context, attrs) {
        constructor(context: Context) : this(context, null)
        private val positionField: Field =
            ViewPager.LayoutParams::class.java.getDeclaredField("position").also {
                it.isAccessible = true
            }
    
        init {
            addOnPageChangeListener(object : SimpleOnPageChangeListener() {
                override fun onPageSelected(position: Int) {
                    requestLayout()
                }
            })
        }
    
        override fun getChildAt(index: Int): View {
            val stackTrace = Throwable().stackTrace
            val calledFromFindScrollingChild = stackTrace.getOrNull(1)?.let {
                it.className == "com.google.android.material.bottomsheet.BottomSheetBehavior" &&
                        it.methodName == "findScrollingChild"
            }
            if (calledFromFindScrollingChild != true) {
                return super.getChildAt(index)
            }
    
            val currentView = getCurrentView() ?: return super.getChildAt(index)
            return if (index == 0) {
                currentView
            } else {
                var view = super.getChildAt(index)
                if (view == currentView) {
                   view = super.getChildAt(0)
                }
                return view
            }
        }
    
        private fun getCurrentView(): View? {
            for (i in 0 until childCount) {
                val child = super.getChildAt(i)
                val lp = child.layoutParams as? ViewPager.LayoutParams
                if (lp != null) {
                    val position = positionField.getInt(lp)
                    if (!lp.isDecor && currentItem == position) {
                        return child
                    }
                }
            }
            return null
        }
    }
    
    
    0 讨论(0)
  • 2020-12-13 03:10

    Looks like all that's required is updating nestedScrollingChildRef appropriately.

    Simply setting it to the target parameter in onStartNestedScroll is working for me:

    package com.google.android.material.bottomsheet
    
    class ViewPagerBottomSheetBehavior<V : View>(context: Context, attrs: AttributeSet?) : BottomSheetBehavior<V>(context, attrs) {
    
        override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: V, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
            nestedScrollingChildRef = WeakReference(target)
            return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type)
        }
    }
    
    0 讨论(0)
  • 2020-12-13 03:16

    I came across the same limitation but were able to solve it.

    The reason for the effect you described is that BottomSheetBehavior (as of v24.2.0) only supports one scrolling child which is identified during layout in the following way:

    private View findScrollingChild(View view) {
        if (view instanceof NestedScrollingChild) {
            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;
    }
    

    You can see that it essentially finds the first scrolling child using DFS.

    I slightly enhanced this implementation and assembled a small library as well as an example app. You can find it here: https://github.com/laenger/ViewPagerBottomSheet

    Simply add the maven repo url to your build.gradle:

    repositories {
        maven { url "https://raw.github.com/laenger/maven-releases/master/releases" }
    }
    

    Add the library to the dependencies:

    dependencies {
        compile "biz.laenger.android:vpbs:0.0.2"
    }
    

    Use ViewPagerBottomSheetBehavior for your bottom sheet view:

    app:layout_behavior="@string/view_pager_bottom_sheet_behavior"
    

    Setup any nested ViewPager inside the bottom sheet:

    BottomSheetUtils.setupViewPager(bottomSheetViewPager)
    

    (This also works when the ViewPager is the bottom sheet view and for further nested ViewPagers)

    0 讨论(0)
  • 2020-12-13 03:22

    I have the solution for AndroidX, Kotlin. Tested and working on 'com.google.android.material:material:1.1.0-alpha06'.

    I also used this: MEDIUM BLOG as a guide.

    Here is My ViewPagerBottomSheetBehavior Kotlin Class:

    package com.google.android.material.bottomsheet
    import android.content.Context
    import android.util.AttributeSet
    import android.view.View
    import androidx.annotation.VisibleForTesting
    import androidx.viewpager.widget.ViewPager
    import java.lang.ref.WeakReference
    class ViewPagerBottomSheetBehavior<V : View>
        : com.google.android.material.bottomsheet.BottomSheetBehavior<V>,
        ViewPager.OnPageChangeListener {
    
        constructor() : super()
        constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    
        override fun onPageScrollStateChanged(state: Int) {}
        override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
        override fun onPageSelected(position: Int) {
            val container = viewRef?.get() ?: return
            nestedScrollingChildRef = WeakReference(findScrollingChild(container))
        }
    
        @VisibleForTesting
        override fun findScrollingChild(view: View?): View? {
            return if (view is ViewPager) {
                view.focusedChild?.let { findScrollingChild(it) }
            } else {
                super.findScrollingChild(view)
            }
        }
    }
    

    The final solutios was adding the super constructors in the Class:

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

    Remember, you have to add ViewPagerBottomSheetBehavior Kotlin Class in the next path: Path Class Image reference because, you must override a private method>

    @VisibleForTesting
    override fun findScrollingChild(view: View?): View? {
        return if (view is ViewPager) {
            view.focusedChild?.let { findScrollingChild(it) }
        } else {
            super.findScrollingChild(view)
        }
    }
    

    After that, you can use it as a View attribute, like this>

            <androidx.constraintlayout.widget.ConstraintLayout
              app:layout_behavior="com.google.android.material.bottomsheet.ViewPagerBottomSheetBehavior"
                android:layout_height="match_parent"
                android:layout_width="match_parent">
            <include
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    layout="@layout/you_content_with_a_viewPager_scroll"
            />
        </androidx.constraintlayout.widget.ConstraintLayout>
    
    0 讨论(0)
  • 2020-12-13 03:23

    This post saved my life: https://medium.com/@hanru.yeh/funny-solution-that-makes-bottomsheetdialog-support-viewpager-with-nestedscrollingchilds-bfdca72235c3

    Show my fix for ViewPager inside bottomsheet.

    package com.google.android.material.bottomsheet
    
    import android.view.View
    import androidx.annotation.VisibleForTesting
    import androidx.viewpager.widget.ViewPager
    import java.lang.ref.WeakReference
    
    
    class BottomSheetBehaviorFix<V : View> : BottomSheetBehavior<V>(), ViewPager.OnPageChangeListener {
    
        override fun onPageScrollStateChanged(state: Int) {}
    
        override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
    
        override fun onPageSelected(position: Int) {
            val container = viewRef?.get() ?: return
            nestedScrollingChildRef = WeakReference(findScrollingChild(container))
        }
    
        @VisibleForTesting
        override fun findScrollingChild(view: View): View? {
            return if (view is ViewPager) {
                view.focusedChild?.let { findScrollingChild(it) }
            } else {
                super.findScrollingChild(view)
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-13 03:32

    Assuming page is a NestedScrollView, I was able to solve the problem by toggling its isNestedScrollingEnabled property depending on whether or not it's the incoming or outgoing page.

    val viewPager = findViewById<ViewPager>(R.id.viewPager)
    
    viewPager.setPageTransformer(false) { page, position ->
        if (position == 0.0f) {
            page.isNestedScrollingEnabled = true
        } else if (position % 1 == 0.0f) {
            page.isNestedScrollingEnabled = false
        }
    }
    
    0 讨论(0)
提交回复
热议问题