Max Gierlachowski
Max Gierlachowski

Reputation: 446

RecyclerView should stay at bottom with stackFromEnd

so basically this question already exists on the web: How do I keep my RecyclerView scrolling to the bottom with new items? Almost always you will find the answer: add stackFromEnd=true or reverseLayout=true. And in my case I tried every possible combination of the two and it just didn't work. I also saw solutions like this:

    viewAdapter.registerAdapterDataObserver(object : AdapterDataObserver() {
        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
            //(layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(viewAdapter.itemCount - 1, 0) or
            //(layoutManager as? LinearLayoutManager)?.smoothScrollToPosition(this@ChatMessagesList, null, viewAdapter.itemCount - 1) or
            //[email protected](viewAdapter.itemCount - 1)
        }
    })

which also didn't help me. First I will describe my setup and then the expected behaviour and current behaviour:

I have an xml that looks like this:

<merge 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:id="@+id/root_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

    <ImageView
        android:id="@+id/backgroundImage"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:contentDescription="@string/image_image_description"
        android:scaleType="centerCrop"
        android:src="@drawable/chat_background"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:id="@+id/overlay"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#CCFFFFFF"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <app.jooy.messenger.ui.components.generic.chat.chat_header.ChatHeader
        android:id="@+id/chat_header"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <app.jooy.messenger.ui.components.generic.chat.chat_message_list.ChatMessagesList
        android:id="@+id/chat_messages_list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/chat_options_chooser"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/chat_header" />

    <app.jooy.messenger.ui.components.generic.chat.chat_options_chooser.ChatOptionsChooser
        android:id="@+id/chat_options_chooser"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintBottom_toTopOf="@id/chat_emoji_bar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <app.jooy.messenger.ui.components.generic.chat.chat_emoji_bar.ChatEmojiBar
        android:id="@+id/chat_emoji_bar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintBottom_toTopOf="@id/chat_action_bar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <app.jooy.messenger.ui.components.generic.chat.chat_action_bar.ChatActionBar
        android:id="@+id/chat_action_bar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@color/colorActionBarBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</merge>

The important view is the chat_messages_list RecyclerView which is my RecyclerView whose implementation follows (removed some things for simplicity):

@ExperimentalCoroutinesApi
@ReactiveComponent
class ChatMessagesList @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ReactiveRecyclerView<ChatMessagesList.ViewState, ChatMessagesList.ViewAction, ChatMessagesList.ViewStructure>(
    ViewState(),
    context,
    attrs,
    defStyleAttr
) {

    private val viewAdapter = ChatMessagesListAdapter()

    init {

        setHasFixedSize(true)
        layoutManager = object : LinearLayoutManager(context) {
            init {
                stackFromEnd = true
            }
        }
        viewAdapter.registerAdapterDataObserver(object : AdapterDataObserver() {
            override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
                //(layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(0, 0)
                //(layoutManager as? LinearLayoutManager)?.smoothScrollToPosition(this@ChatMessagesList, null, viewAdapter.itemCount - 1)
                //[email protected](0)
            }
        })

        registerTypes(viewAdapter)
        adapter = viewAdapter

        addItemDecoration(getDecoration())

        setPadding(0, 6.toPx(), 0, 6.toPx())
        clipToPadding = false

    }

}

Further I can tell you that my adapter definitly calls the correct notifyItemRangeInserted, ... calls.

Expected behaviour: The RecyclerView should start with the first item at the bottom of the screen and always if a new one is added it should be added at the bottom and scrolled to. In the end the new items at the bottom should look like they are pushed in from the bottom.

Current behaviour: The items stack from the bottom and until the visible space inside the RecyclerView is not full everything is animated beautifully, but as soon as the visible space is full, items are added at the bottom but not scrolled to. As some would expect the functionality I want to achive is the one of a chat.

Further notes: If I add [email protected](viewAdapter.itemCount - 1) to the onItemRangeInserted callback it goes to the bottom but without a animation and it just jumps there.

Upvotes: 0

Views: 469

Answers (1)

Max Gierlachowski
Max Gierlachowski

Reputation: 446

so I kind of figured it out myself. I still don't know why particular strategies work and some do not work at all.

I set reverseLayout=true on the LinearLayoutManager (which means I had to reverse my items in the list) and then I added an onItemRangeInserted listener like this:

    layoutManager = object : LinearLayoutManager(context) {
        init {
            reverseLayout = true
        }
    }
    viewAdapter.registerAdapterDataObserver(object : AdapterDataObserver() {
        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
            if (positionStart == 0) {
                layoutManager?.scrollToPosition(0)
            }
        }
    })

Upvotes: 1

Related Questions