-
Notifications
You must be signed in to change notification settings - Fork 74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[emojisearch] Preserve scroll position for Android view #1599
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did we remove the default value here? It's kinda nice to have it as it aligns with what Compose UI does with their There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the init for |
||
): LazyListState { | ||
return remember { | ||
LazyListState( | ||
initialFirstVisibleItemIndex, | ||
) | ||
public fun rememberLazyListState(): LazyListState { | ||
return rememberSaveable(saver = LazyListState.Saver) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this change absorbs #1494 (🙏 to @-veyndan) |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we change this back to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this function is not really a scroll to item anymore, it has it's own logic, and what it does is "restore when there's criteria are met" |
||
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 |
---|---|---|
|
@@ -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 | ||
|
@@ -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) { | ||
|
@@ -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 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a decently expensive operation to perform on every frame of scrolling. It involves allocation of the list and boxing of the position integers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is. There was an alternative of passing in a mutable of viewHolder and positions to adapter to keep track of positions of visible views (by adding/removing keys when There might be other alternatives. Previous calls : There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'd be curious to investigate this more, because it's kinda surprising that a core API would fail like this. Did you find any bug reports on it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
from my time-boxed googling, there wasn't report with similar symptom |
||
userHasScrolled = true // Prevent guest code from hijacking the scrollbar. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this mean it's impossible to programmatically change the scroll position once the user has interacted with the list a single time? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's a good point ... yes, it is at the moment |
||
onViewportChanged?.invoke( | ||
linearLayoutManager.findFirstVisibleItemPosition(), | ||
linearLayoutManager.findLastVisibleItemPosition(), | ||
viewsOnScreen.min(), | ||
viewsOnScreen.max(), | ||
) | ||
} | ||
}, | ||
|
@@ -154,6 +159,7 @@ internal open class ViewLazyList private constructor( | |
} | ||
|
||
override fun scrollItemIndex(scrollItemIndex: ScrollItemIndex) { | ||
if (userHasScrolled) return | ||
recyclerView.scrollToPosition(scrollItemIndex.index) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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) }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. print out index to make debugging/seeing what's going on with the list easier |
||
lazyListState.maybeRestoreScrollPosition() | ||
} finally { | ||
refreshing = false | ||
} | ||
|
@@ -121,12 +129,6 @@ private fun LazyColumn( | |
} | ||
} | ||
|
||
val lazyListState = rememberLazyListState() | ||
|
||
LaunchedEffect(searchTerm) { | ||
lazyListState.scrollToItem(0) | ||
} | ||
|
||
Column( | ||
width = Constraint.Fill, | ||
height = Constraint.Fill, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be
implementation
if you change theSaver
to beprivate
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will make a follow up PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here it is: #1600