Skip to content

Commit

Permalink
[emojisearch] Preserve scroll position for Android view (#1599)
Browse files Browse the repository at this point in the history
* [emojisearch] Preserve scroll position for Android view
  • Loading branch information
jingwei99 authored Oct 16, 2023
1 parent 9be0922 commit db2d039
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 33 deletions.
1 change: 1 addition & 0 deletions redwood-lazylayout-compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api libs.jetbrains.compose.runtime.saveable
api projects.redwoodLazylayoutWidget
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ internal fun LazyList(
var lastVisibleItemIndex by remember { mutableStateOf(0) }
val itemsBefore = remember(state.firstVisibleItemIndex) { (state.firstVisibleItemIndex - OffscreenItemsBufferCount / 2).coerceAtLeast(0) }
val itemsAfter = remember(lastVisibleItemIndex, itemProvider.itemCount) { (itemProvider.itemCount - (lastVisibleItemIndex + OffscreenItemsBufferCount / 2).coerceAtMost(itemProvider.itemCount)).coerceAtLeast(0) }
val scrollItemIndex = remember(state.scrollToItemTriggeredId) { ScrollItemIndex(state.scrollToItemTriggeredId, state.firstVisibleItemIndex) }
// TODO(jwilson): drop this down to 20 once this is fixed:
// https://github.com/cashapp/redwood/issues/1551
var placeholderPoolSize by remember { mutableStateOf(30) }
Expand All @@ -57,21 +56,22 @@ internal fun LazyList(
itemsBefore = itemsBefore,
itemsAfter = itemsAfter,
onViewportChanged = { localFirstVisibleItemIndex, localLastVisibleItemIndex ->
state.onScrolled(localFirstVisibleItemIndex)

val visibleItemCount = localLastVisibleItemIndex - localFirstVisibleItemIndex
val proposedPlaceholderPoolSize = visibleItemCount + visibleItemCount / 2
// We only ever want to increase the pool size.
if (placeholderPoolSize < proposedPlaceholderPoolSize) {
placeholderPoolSize = proposedPlaceholderPoolSize
}
state.firstVisibleItemIndex = localFirstVisibleItemIndex
lastVisibleItemIndex = localLastVisibleItemIndex
},
width = width,
height = height,
margin = margin,
crossAxisAlignment = crossAxisAlignment,
modifier = modifier,
scrollItemIndex = scrollItemIndex,
scrollItemIndex = ScrollItemIndex(0, state.scrollItemIndex),
placeholder = { repeat(placeholderPoolSize) { placeholder() } },
items = {
for (index in itemsBefore until itemProvider.itemCount - itemsAfter) {
Expand Down Expand Up @@ -102,20 +102,20 @@ internal fun RefreshableLazyList(
var lastVisibleItemIndex by remember { mutableStateOf(0) }
val itemsBefore = remember(state.firstVisibleItemIndex) { (state.firstVisibleItemIndex - OffscreenItemsBufferCount / 2).coerceAtLeast(0) }
val itemsAfter = remember(lastVisibleItemIndex, itemProvider.itemCount) { (itemProvider.itemCount - (lastVisibleItemIndex + OffscreenItemsBufferCount / 2).coerceAtMost(itemProvider.itemCount)).coerceAtLeast(0) }
val scrollItemIndex = remember(state.scrollToItemTriggeredId) { ScrollItemIndex(state.scrollToItemTriggeredId, state.firstVisibleItemIndex) }
var placeholderPoolSize by remember { mutableStateOf(20) }
RefreshableLazyList(
isVertical,
itemsBefore = itemsBefore,
itemsAfter = itemsAfter,
onViewportChanged = { localFirstVisibleItemIndex, localLastVisibleItemIndex ->
state.onScrolled(localFirstVisibleItemIndex)

val visibleItemCount = localLastVisibleItemIndex - localFirstVisibleItemIndex
val proposedPlaceholderPoolSize = visibleItemCount + visibleItemCount / 2
// We only ever want to increase the pool size.
if (placeholderPoolSize < proposedPlaceholderPoolSize) {
placeholderPoolSize = proposedPlaceholderPoolSize
}
state.firstVisibleItemIndex = localFirstVisibleItemIndex
lastVisibleItemIndex = localLastVisibleItemIndex
},
refreshing = refreshing,
Expand All @@ -125,7 +125,7 @@ internal fun RefreshableLazyList(
margin = margin,
crossAxisAlignment = crossAxisAlignment,
modifier = modifier,
scrollItemIndex = scrollItemIndex,
scrollItemIndex = ScrollItemIndex(0, state.scrollItemIndex),
placeholder = { repeat(placeholderPoolSize) { placeholder() } },
pullRefreshContentColor = pullRefreshContentColor,
items = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,71 @@ package app.cash.redwood.lazylayout.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue

@Composable
public fun rememberLazyListState(
initialFirstVisibleItemIndex: Int = 0,
): LazyListState {
return remember {
LazyListState(
initialFirstVisibleItemIndex,
)
public fun rememberLazyListState(): LazyListState {
return rememberSaveable(saver = LazyListState.Saver) {
LazyListState()
}
}

public class LazyListState(
firstVisibleItemIndex: Int = 0,
) {
public var firstVisibleItemIndex: Int by mutableStateOf(firstVisibleItemIndex)
public class LazyListState {
/** We only restore the scroll position once. */
private var hasRestoredScrollPosition = false

/** The scroll position to restore. */
private var restoredIndex: Int = -1

/**
* The value published to the host platform. This starts as 0 and changes exactly once to
* trigger exactly one scroll.
*/
public var scrollItemIndex: Int by mutableStateOf(0)
internal set

internal var scrollToItemTriggeredId by mutableStateOf(0)
public var firstVisibleItemIndex: Int = 0
private set

public fun restoreIndex(index: Int) {
require(index >= 0)

if (this.restoredIndex != -1) return
this.restoredIndex = index

// Scroll to the target item.
if (hasRestoredScrollPosition) {
scrollItemIndex = restoredIndex
}
}

public fun scrollToItem(
index: Int,
) {
firstVisibleItemIndex = index
scrollToItemTriggeredId++
public fun maybeRestoreScrollPosition() {
if (this.hasRestoredScrollPosition) return
this.hasRestoredScrollPosition = true

// Scroll to the target item.
if (restoredIndex != -1) {
scrollItemIndex = restoredIndex
}
}

public fun onScrolled(firstVisibleItemIndex: Int) {
this.firstVisibleItemIndex = firstVisibleItemIndex
}

public companion object {
/**
* The default [Saver] implementation for [LazyListState].
*/
public val Saver: Saver<LazyListState, *> = Saver(
save = { it.firstVisibleItemIndex },
restore = {
LazyListState().apply {
restoreIndex(it)
}
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import androidx.annotation.ColorInt
import androidx.core.view.children
import androidx.core.view.doOnDetach
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePaddingRelative
Expand Down Expand Up @@ -58,6 +59,8 @@ internal open class ViewLazyList private constructor(

private var crossAxisAlignment = CrossAxisAlignment.Start

private var userHasScrolled = false

private val density = Density(recyclerView.context.resources)
private val linearLayoutManager = object : LinearLayoutManager(recyclerView.context) {
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams? = when (orientation) {
Expand Down Expand Up @@ -101,9 +104,11 @@ internal open class ViewLazyList private constructor(
addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val viewsOnScreen = recyclerView.children.map { recyclerView.getChildViewHolder(it).bindingAdapterPosition }
userHasScrolled = true // Prevent guest code from hijacking the scrollbar.
onViewportChanged?.invoke(
linearLayoutManager.findFirstVisibleItemPosition(),
linearLayoutManager.findLastVisibleItemPosition(),
viewsOnScreen.min(),
viewsOnScreen.max(),
)
}
},
Expand Down Expand Up @@ -154,6 +159,7 @@ internal open class ViewLazyList private constructor(
}

override fun scrollItemIndex(scrollItemIndex: ScrollItemIndex) {
if (userHasScrolled) return
recyclerView.scrollToPosition(scrollItemIndex.index)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ private fun LazyColumn(

var searchTerm by rememberSaveable(stateSaver = searchTermSaver) { mutableStateOf(TextFieldState("")) }

val lazyListState = rememberLazyListState()

LaunchedEffect(searchTerm) {
lazyListState.restoreIndex(0)
}

LaunchedEffect(refreshSignal) {
try {
refreshing = true
Expand All @@ -108,7 +114,9 @@ private fun LazyColumn(
val labelToUrl = Json.decodeFromString<Map<String, String>>(emojisJson)

allEmojis.clear()
allEmojis.addAll(labelToUrl.map { (key, value) -> EmojiImage(key, value) })
var index = 0
allEmojis.addAll(labelToUrl.map { (key, value) -> EmojiImage("${index++}. $key", value) })
lazyListState.maybeRestoreScrollPosition()
} finally {
refreshing = false
}
Expand All @@ -121,12 +129,6 @@ private fun LazyColumn(
}
}

val lazyListState = rememberLazyListState()

LaunchedEffect(searchTerm) {
lazyListState.scrollToItem(0)
}

Column(
width = Constraint.Fill,
height = Constraint.Fill,
Expand Down

0 comments on commit db2d039

Please sign in to comment.