From f73e8e544175ea4961950a969d41aa9a1b09c14b Mon Sep 17 00:00:00 2001 From: Jingwei Hao Date: Thu, 7 Dec 2023 13:39:31 -0800 Subject: [PATCH] Return SizeOnlyPlaceholder when the placeholder queue is empty Co-authored-by: Jesse Wilson --- .../redwood/lazylayout/compose/LazyList.kt | 28 +++---------- .../redwood/lazylayout/view/ViewLazyList.kt | 8 ++++ .../widget/LazyListUpdateProcessor.kt | 39 +++++++++++++++++-- .../android/views/EmojiSearchActivity.kt | 4 ++ 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LazyList.kt b/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LazyList.kt index ec36e94eda..2437e69aab 100644 --- a/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LazyList.kt +++ b/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LazyList.kt @@ -20,8 +20,6 @@ package app.cash.redwood.lazylayout.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import app.cash.redwood.Modifier import app.cash.redwood.layout.api.Constraint @@ -45,23 +43,14 @@ internal fun LazyList( val itemCount = itemProvider.itemCount val itemsBefore = (state.firstIndex - state.preloadBeforeItemCount).coerceAtLeast(0) val itemsAfter = (itemCount - (state.lastIndex + state.preloadAfterItemCount).coerceAtMost(itemCount)).coerceAtLeast(0) - // TODO(jwilson): drop this down to 20 once this is fixed: - // https://github.com/cashapp/redwood/issues/1551 - var placeholderPoolSize by remember { mutableStateOf(30) } + val placeholderPoolSize = 30 LazyList( - isVertical, - itemsBefore = itemsBefore, - itemsAfter = itemsAfter, + isVertical = isVertical, onViewportChanged = { localFirstVisibleItemIndex, localLastVisibleItemIndex -> state.onUserScroll(localFirstVisibleItemIndex, localLastVisibleItemIndex) - - val visibleItemCount = localLastVisibleItemIndex - localFirstVisibleItemIndex - val proposedPlaceholderPoolSize = visibleItemCount + visibleItemCount / 2 - // We only ever want to increase the pool size. - if (placeholderPoolSize < proposedPlaceholderPoolSize) { - placeholderPoolSize = proposedPlaceholderPoolSize - } }, + itemsBefore = itemsBefore, + itemsAfter = itemsAfter, width = width, height = height, margin = margin, @@ -98,20 +87,13 @@ internal fun RefreshableLazyList( val itemCount = itemProvider.itemCount val itemsBefore = (state.firstIndex - state.preloadBeforeItemCount).coerceAtLeast(0) val itemsAfter = (itemCount - (state.lastIndex + state.preloadAfterItemCount).coerceAtMost(itemCount)).coerceAtLeast(0) - var placeholderPoolSize by remember { mutableStateOf(20) } + val placeholderPoolSize = 30 RefreshableLazyList( isVertical, itemsBefore = itemsBefore, itemsAfter = itemsAfter, onViewportChanged = { localFirstVisibleItemIndex, localLastVisibleItemIndex -> state.onUserScroll(localFirstVisibleItemIndex, localLastVisibleItemIndex) - - val visibleItemCount = localLastVisibleItemIndex - localFirstVisibleItemIndex - val proposedPlaceholderPoolSize = visibleItemCount + visibleItemCount / 2 - // We only ever want to increase the pool size. - if (placeholderPoolSize < proposedPlaceholderPoolSize) { - placeholderPoolSize = proposedPlaceholderPoolSize - } }, refreshing = refreshing, onRefresh = onRefresh, diff --git a/redwood-lazylayout-view/src/main/kotlin/app/cash/redwood/lazylayout/view/ViewLazyList.kt b/redwood-lazylayout-view/src/main/kotlin/app/cash/redwood/lazylayout/view/ViewLazyList.kt index 1c5c102476..882186c704 100644 --- a/redwood-lazylayout-view/src/main/kotlin/app/cash/redwood/lazylayout/view/ViewLazyList.kt +++ b/redwood-lazylayout-view/src/main/kotlin/app/cash/redwood/lazylayout/view/ViewLazyList.kt @@ -72,6 +72,14 @@ internal open class ViewLazyList private constructor( override val value: View get() = recyclerView private val processor = object : LazyListUpdateProcessor() { + override fun createPlaceholder(original: View): View { + return object : View(value.context) { + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + setMeasuredDimension(original.width, original.height) + } + } + } + override fun insertRows(index: Int, count: Int) { adapter.notifyItemRangeInserted(index, count) } diff --git a/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/LazyListUpdateProcessor.kt b/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/LazyListUpdateProcessor.kt index 3b1c6fe98a..1fee831343 100644 --- a/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/LazyListUpdateProcessor.kt +++ b/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/LazyListUpdateProcessor.kt @@ -15,6 +15,7 @@ */ package app.cash.redwood.lazylayout.widget +import app.cash.redwood.Modifier import app.cash.redwood.widget.Widget /** @@ -43,6 +44,12 @@ public abstract class LazyListUpdateProcessor { /** Pool of placeholder widgets. */ private val placeholdersQueue = ArrayDeque>() + /** + * The first placeholder ever returned. We use it to choose measured dimensions for created + * placeholders if the pool ever runs out. + */ + private var firstPlaceholder: Widget? = null + /** Loaded items that may or may not have a view bound. */ private var loadedItems = mutableListOf>() @@ -57,6 +64,7 @@ public abstract class LazyListUpdateProcessor { /** We expect placeholders to be added early and to never change. */ public val placeholder: Widget.Children = object : Widget.Children { override fun insert(index: Int, widget: Widget) { + if (firstPlaceholder == null) firstPlaceholder = widget placeholdersQueue += widget } @@ -253,7 +261,7 @@ public abstract class LazyListUpdateProcessor { // We have a binding. Give it loaded content. require(placeholder.isPlaceholder) - placeholdersQueue += placeholder.content!! + recyclePlaceholder(placeholder.content!!) placeholder.isPlaceholder = false placeholder.content = loadedContent return placeholder @@ -313,10 +321,35 @@ public abstract class LazyListUpdateProcessor { } private fun takePlaceholder(): Widget { - return placeholdersQueue.removeFirstOrNull() + val result = placeholdersQueue.removeFirstOrNull() + if (result != null) return result + + val created = createPlaceholder(firstPlaceholder!!.value) ?: throw IllegalStateException("no more placeholders!") + + return SizeOnlyPlaceholder(created) + } + + private fun recyclePlaceholder(placeholder: Widget) { + if (placeholder !is SizeOnlyPlaceholder) { + placeholdersQueue += placeholder + } + } + + private class SizeOnlyPlaceholder( + override val value: W, + ) : Widget { + override var modifier: Modifier = Modifier } + /** + * Returns an empty widget with the same dimensions as [original]. + * + * @param original a placeholder provided by the guest code. It is a live view that is currently + * in a layout. + */ + protected open fun createPlaceholder(original: W): W? = null + protected abstract fun insertRows(index: Int, count: Int) protected abstract fun deleteRows(index: Int, count: Int) @@ -383,7 +416,7 @@ public abstract class LazyListUpdateProcessor { if (itemsAfterIndex != -1) processor.itemsAfter.set(itemsAfterIndex, null) // When a placeholder is reused, recycle its widget. - processor.placeholdersQueue += content!! + processor.recyclePlaceholder(content!!) } } } diff --git a/samples/emoji-search/android-views/src/main/kotlin/com/example/redwood/emojisearch/android/views/EmojiSearchActivity.kt b/samples/emoji-search/android-views/src/main/kotlin/com/example/redwood/emojisearch/android/views/EmojiSearchActivity.kt index 03c34ee399..39a6a39380 100644 --- a/samples/emoji-search/android-views/src/main/kotlin/com/example/redwood/emojisearch/android/views/EmojiSearchActivity.kt +++ b/samples/emoji-search/android-views/src/main/kotlin/com/example/redwood/emojisearch/android/views/EmojiSearchActivity.kt @@ -104,6 +104,10 @@ class EmojiSearchActivity : ComponentActivity() { private var success = true private var snackbar: Snackbar? = null + override fun uncaughtException(exception: Throwable) { + Log.e("Treehouse", "uncaughtException", exception) + } + override fun codeLoadFailed(exception: Exception, startValue: Any?) { Log.w("Treehouse", "codeLoadFailed", exception) if (success) {