Skip to content
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

Merged
merged 2 commits into from
Oct 16, 2023

Conversation

jingwei99
Copy link

@jingwei99 jingwei99 commented Oct 16, 2023

Preserve scroll position works end to end with this PR

scrollposition.mov

initialFirstVisibleItemIndex,
)
public fun rememberLazyListState(): LazyListState {
return rememberSaveable(saver = LazyListState.Saver) {
Copy link
Author

@jingwei99 jingwei99 Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change absorbs #1494 (🙏 to @-veyndan)

@jingwei99 jingwei99 force-pushed the jingwei.preserve_scroll_position branch from 04f469f to 431a693 Compare October 16, 2023 20:04
@@ -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) })
Copy link
Author

Choose a reason for hiding this comment

The 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

@jingwei99 jingwei99 marked this pull request as ready for review October 16, 2023 20:11
@jingwei99
Copy link
Author

test is failing, I'll get to it

@@ -46,9 +46,8 @@ internal fun LazyList(
) {
val itemProvider = rememberLazyListItemProvider(content)
var lastVisibleItemIndex by remember { mutableStateOf(0) }
val itemsBefore = remember(state.firstVisibleItemIndex) { (state.firstVisibleItemIndex - OffscreenItemsBufferCount / 2).coerceAtLeast(0) }
val itemsBefore = remember(state.scrollItemIndex) { (state.scrollItemIndex - OffscreenItemsBufferCount / 2).coerceAtLeast(0) }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooooh I think we might have broken this.

This should probably be a different mutableState for proper on-demand loading

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As-is this only updates once, and I think it needs to update with every scroll.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whereas the automated scrolling does not need to do that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

var placeholderPoolSize by remember { mutableStateOf(20) }
RefreshableLazyList(
isVertical,
itemsBefore = itemsBefore,
itemsAfter = itemsAfter,
onViewportChanged = { localFirstVisibleItemIndex, localLastVisibleItemIndex ->
// Update LazyListState.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Update LazyListState.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Collaborator

@swankjesse swankjesse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scrolling behavior looks great.

Does this break lazy loading?

@jingwei99
Copy link
Author

Does this break lazy loading?

fixed!

@jingwei99 jingwei99 enabled auto-merge (squash) October 16, 2023 22:46
@jingwei99 jingwei99 merged commit db2d039 into trunk Oct 16, 2023
8 checks passed
@jingwei99 jingwei99 deleted the jingwei.preserve_scroll_position branch October 16, 2023 22:54
@@ -13,6 +13,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api libs.jetbrains.compose.runtime.saveable
Copy link
Collaborator

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 the Saver to be private.

Copy link
Author

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here it is: #1600

@@ -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 }
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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 onViewDetachedFromWindow and onBindViewHolder), it's somewhat ugly though.

There might be other alternatives.

Previous calls : linearLayoutManager.findFirstVisibleItemPosition() and linearLayoutManager.findLastVisibleItemPosition() are not giving the accurate indices.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous calls : linearLayoutManager.findFirstVisibleItemPosition() and linearLayoutManager.findLastVisibleItemPosition() are not giving the accurate indices.

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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you find any bug reports on it?

from my time-boxed googling, there wasn't report with similar symptom

@@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good point ... yes, it is at the moment

@veyndan
Copy link
Contributor

veyndan commented Oct 17, 2023

I haven't properly reviewed the PR yet, but I'm noticing a crash in Emoji Search after these changes:

  • Run Emoji Search using Android Views
  • Type "medal" into the search box
  • Click on one of the medal emojis
  • Press the system back button
  • The app crashes with the exception app.cash.zipline.ZiplineException: IllegalArgumentException: Failed requirement.

import androidx.compose.runtime.setValue

@Composable
public fun rememberLazyListState(
initialFirstVisibleItemIndex: Int = 0,
Copy link
Contributor

Choose a reason for hiding this comment

The 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 LazyListState implementation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the init for initialFirstVisibleItemIndex now is at down below (to 0)

@@ -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 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous calls : linearLayoutManager.findFirstVisibleItemPosition() and linearLayoutManager.findLastVisibleItemPosition() are not giving the accurate indices.

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?

public var firstVisibleItemIndex: Int = 0
private set

public fun restoreIndex(index: Int) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The 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"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants