'RecyclerView in BottomSheet not working as expected

I have a problem with RecyclerView directly inside of layout with bottomsheetbehaviour. The problem is that when bottom sheet is expanded and content is scrolled down, when I go to scroll back up it causes Bottom Sheet to start collapsing, instead of RecyclerView first being scrolled back to top.

Here's a video to demonstrate the problem. As you can see the problem appears when I scroll down on expanded bottom sheet. It immediately start to collapse instead of "waiting" for RecyclerView to scroll to top first.

Here is my layout code

<?xml version="1.0" encoding="utf-8"?>

<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:id="@+id/scheduleRoot"
    android:layout_height="match_parent"
    tools:context=".schedule.ScheduleFragment">


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/scheduleSheet"
        app:behavior_peekHeight="300dp"
        android:elevation="16dp"
        android:clickable="false"
        android:focusable="false"
        android:background="@drawable/bg_bottom_sheet"
        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/scheduleRecyclerView"
            android:clickable="true"
            android:focusable="true"
            android:layout_marginTop="8dp"/>

    </LinearLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Any help is appreciated!



Solution 1:[1]

I just encountered same problem, but I fixed it by adding this to onCreate:

androidx.core.view.ViewCompat.setNestedScrollingEnabled(recyclerview, false);

Solution 2:[2]

Your recyclerview item has overighted the scrolling state, so this error generates. The layout you provided does not have enough data to determine the cause. You change the item is a unique view to check

Solution 3:[3]

I had similar issue: Maybe the solution to my problem will give you some ideas. My bottom sheet was expanded to full height with recycler view in it; the bottom sheet was collapsing on user-drag, even though the first item in recycler view wasn't visible yet.

So, what I did:

  1. You can enable/disable bottom sheet dragging by "isDraggable" = true/false
  2. Add OnScrollListener for recycler view.
  3. Override onScrolled and check layoutManager.findFirstVisibleItemPosition() in it
  4. If first item is visible - update bottom sheet behavior.isDraggable = true, i also added small delay before setting behavior.isDraggable = true, because bottom sheet was collapsing too fast, but you might not need it

Maybe it's not optimal but it was fitting my needs and maybe will help you.

Solution 4:[4]

I played with this for a long time and tried way too many solutions. For me, this worked best:

        val layoutManager = LinearLayoutManager(requireContext())
        binding.recyclerView.layoutManager = layoutManager
        binding.recyclerView.setOnScrollChangeListener { _, _, _, _, _ ->
            if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
                bottomSheetBehavior.isDraggable = layoutManager.findFirstCompletelyVisibleItemPosition() == 0
            } else {
                bottomSheetBehavior.isDraggable = true
            }
        }

The key to the solution is is controlling users ability drag the bottom sheet while the recyclerview is partially scrolled. The method only allows scrolling again once the top most cell is fully visible.

Its not ideal as the user may want to grab the very top of the bottom sheet (assuming its not part of the recycler view) and dismiss the bottom sheet regardless of its scroll position. Im just accepting.

Whatever you do, do not try these, as they just disable any recycling functionality and all cells are loaded at instantiation having a really bad impact on performance:

wrap_content

or:

binding.recyclerView.isNestedScrollingEnabled = false

Solution 5:[5]

Your implementation might need more coding and with the provided code we might not able to give you good feedback.

Try this documentation https://material.io/develop/android/components/bottom-sheet-behavior/

Plus I found this another implementation. https://www.youtube.com/watch?v=WeaylHAwIIk

Solution 6:[6]

Enable the scroll state of BottomSheet to allow scroll if recyclerview 0th item is visible.

activity_main.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data />

    <androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#A8A7A7"
    tools:context=".MainActivity">

    <LinearLayout
        android:id="@+id/parent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#fff"
        android:orientation="vertical"
        app:behavior_hideable="true"
        app:behavior_peekHeight="80dp"
        app:layout_behavior="com.asadmukhtar.recyclerviewinsidebottomsheet.LockableBottomSheetBehavior"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:text="Drag Me"
            android:textColor="#000"
            android:textSize="20sp"
            android:textStyle="bold" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_items"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

LockableBottomSheet file that used for handling allow dragging option or not.

class LockableBottomSheetBehavior<V : View?> : BottomSheetBehavior<V> {
    private var mAllowUserDragging = true

    constructor()

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

    fun setAllowUserDragging(allowUserDragging: Boolean) {
        mAllowUserDragging = allowUserDragging
    }

    override fun onInterceptTouchEvent(
        parent: CoordinatorLayout,
        child: V,
        event: MotionEvent
    ): Boolean {
        return if (!mAllowUserDragging) {
            false
        } else super.onInterceptTouchEvent(parent, child, event)
    }
}

MainActivity.java

var bottomSheetBehavior: LockableBottomSheetBehavior<*>? = null
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding =
        DataBindingUtil.setContentView(this, R.layout.activity_main)

    setUpBottomSheetBehaviour()

    binding.rvItems.layoutManager = LinearLayoutManager(this)
    binding.rvItems.adapter = RecyclerViewAdapter(this)

    binding.rvItems.addOnScrollListener(object : RecyclerView.OnScrollListener() {

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)

            val firstPosition = (binding.rvItems.layoutManager as LinearLayoutManager)
                .findFirstVisibleItemPosition()

            updateBottomSheetLockState(firstPosition == 0)
        }
    })

}

fun updateBottomSheetLockState(allow: Boolean) {
    bottomSheetBehavior?.setAllowUserDragging(allow)
}


fun updateBottomSheetState(state: Int) {
    if (bottomSheetBehavior != null) {
        bottomSheetBehavior?.state = state
    }
}

private fun setUpBottomSheetBehaviour() {
    val bottomSheetBehavior: BottomSheetBehavior<LinearLayout> =
        BottomSheetBehavior.from(binding.parent)
    this.bottomSheetBehavior = bottomSheetBehavior as LockableBottomSheetBehavior<*>
    updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED)
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ilyasse Salama
Solution 2 Mr. Lemon
Solution 3 Adrian Mole
Solution 4 paul_f
Solution 5 José L Crisostomo Sánchez
Solution 6 Asad Mukhtar