I ran into this problem myself and I ended up creating my own LayoutManager to solve it. It's a pretty straightforward solution that can be broken down into three steps:
Set stackFromEnd
to true.
Determine whether forceTranscriptScroll
should be set to true whenever onItemsChanged
is called. Per the documentation, onItemsChanged
gets called whenever the contents of the adapter changes. If transcriptMode
is set to Disabled
, forceTranscriptScroll
will always be false
, if it's set to AlwaysScroll
, it will always be true
, and if it's set to Normal
, it will only be true if the last item in the adapter is completely visible.
In onLayoutCompleted
, scroll to the last item in the list if forceTranscriptScroll
is set to true
and the last item in the list isn't already completely visible.
Below is the code that accomplishes these three steps:
import android.content.Context
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class TranscriptEnabledLinearLayoutManager(context: Context, transcriptMode: TranscriptMode = TranscriptMode.Normal) :
LinearLayoutManager(context) {
enum class TranscriptMode {
Disabled, Normal, AlwaysScroll
}
private var transcriptMode: TranscriptMode = TranscriptMode.Disabled
set(value) {
field = value
// Step 1
stackFromEnd = field != TranscriptMode.Disabled
}
private var forceTranscriptScroll = false
init {
this.transcriptMode = transcriptMode
}
// Step 2
override fun onItemsChanged(recyclerView: RecyclerView) {
super.onItemsChanged(recyclerView)
forceTranscriptScroll = when (transcriptMode) {
TranscriptMode.Disabled -> false
TranscriptMode.Normal -> {
findLastCompletelyVisibleItemPosition() == itemCount - 1
}
TranscriptMode.AlwaysScroll -> true
}
}
// Step 3
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
val recyclerViewState = state ?: return
if (!recyclerViewState.isPreLayout && forceTranscriptScroll) {
// gets the position of the last item in the list. returns if list is empty
val lastAdapterItemPosition = recyclerViewState.itemCount.takeIf { it > 0 }
?.minus(1) ?: return
val lastCompletelyVisibleItem = findLastCompletelyVisibleItemPosition()
if (lastCompletelyVisibleItem != lastAdapterItemPosition ||
recyclerViewState.targetScrollPosition != lastAdapterItemPosition) {
scrollToPositionWithOffset(lastAdapterItemPosition, 0)
}
forceTranscriptScroll = false
}
}
}