From 03d2455f411998d6f26e567e369d2ad6c2bf9ebe Mon Sep 17 00:00:00 2001 From: dahunsi Date: Thu, 14 Sep 2023 18:01:56 -0700 Subject: [PATCH] Use lazy vertical staggered grid in feed to maximize space utilization Change-Id: I66fff57bd0f11f5f73e7e5c9b5c51b0fa6a76926 --- .../component/scrollbar/ScrollbarExt.kt | 39 +++++++++++++++++++ .../component/scrollbar/ThumbExt.kt | 14 +++++++ .../apps/nowinandroid/core/ui/NewsFeed.kt | 14 +++---- .../feature/bookmarks/BookmarksScreen.kt | 18 ++++----- .../feature/foryou/ForYouScreen.kt | 23 +++++------ .../feature/search/SearchScreen.kt | 28 ++++++------- gradle/libs.versions.toml | 2 +- 7 files changed, 93 insertions(+), 45 deletions(-) diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt index 26f0bb2ae..94bb486c3 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.grid.LazyGridItemInfo import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.runtime.Composable /** @@ -102,3 +104,40 @@ fun LazyGridState.scrollbarState( }, reverseLayout = { layoutInfo.reverseLayout }, ) + +/** + * Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState] + * + * @param itemsAvailable the total amount of items available to scroll in the staggered grid. + * @param itemIndex a lookup function for index of an item in the staggered grid relative + * to [itemsAvailable]. + */ +@Composable +fun LazyStaggeredGridState.scrollbarState( + itemsAvailable: Int, + itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index, +): ScrollbarState = + scrollbarState( + itemsAvailable = itemsAvailable, + visibleItems = { layoutInfo.visibleItemsInfo }, + firstVisibleItemIndex = { visibleItems -> + interpolateFirstItemIndex( + visibleItems = visibleItems, + itemSize = { layoutInfo.orientation.valueOf(it.size) }, + offset = { layoutInfo.orientation.valueOf(it.offset) }, + nextItemOnMainAxis = { first -> + visibleItems.find { it != first && it.lane == first.lane } + }, + itemIndex = itemIndex, + ) + }, + itemPercentVisible = itemPercentVisible@{ itemInfo -> + itemVisibilityPercentage( + itemSize = layoutInfo.orientation.valueOf(itemInfo.size), + itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), + viewportStartOffset = layoutInfo.viewportStartOffset, + viewportEndOffset = layoutInfo.viewportEndOffset, + ) + }, + reverseLayout = { false }, + ) \ No newline at end of file diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt index 4d187e269..15406a571 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ThumbExt.kt @@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollb import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -50,6 +51,19 @@ fun LazyGridState.rememberDraggableScroller( scroll = ::scrollToItem, ) +/** + * Remembers a function to react to [Scrollbar] thumb position displacements for a + * [LazyStaggeredGridState] + * @param itemsAvailable the amount of items in the staggered grid. + */ +@Composable +fun LazyStaggeredGridState.rememberDraggableScroller( + itemsAvailable: Int, +): (Float) -> Unit = rememberDraggableScroller( + itemsAvailable = itemsAvailable, + scroll = ::scrollToItem, +) + /** * Generic function to react to [Scrollbar] thumb displacements in a lazy layout. * @param itemsAvailable the total amount of items available to scroll in the layout. diff --git a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt index 4a9f5d7c9..85f9951b8 100644 --- a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt +++ b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt @@ -23,10 +23,10 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyGridScope -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -47,7 +47,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource * An extension on [LazyListScope] defining a feed with news resources. * Depending on the [feedState], this might emit no items. */ -fun LazyGridScope.newsFeed( +fun LazyStaggeredGridScope.newsFeed( feedState: NewsFeedUiState, onNewsResourcesCheckedChanged: (String, Boolean) -> Unit, onNewsResourceViewed: (String) -> Unit, @@ -129,7 +129,7 @@ sealed interface NewsFeedUiState { @Composable private fun NewsFeedLoadingPreview() { NiaTheme { - LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) { + LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Adaptive(300.dp)) { newsFeed( feedState = NewsFeedUiState.Loading, onNewsResourcesCheckedChanged = { _, _ -> }, @@ -148,7 +148,7 @@ private fun NewsFeedContentPreview( userNewsResources: List, ) { NiaTheme { - LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) { + LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Adaptive(300.dp)) { newsFeed( feedState = NewsFeedUiState.Success(userNewsResources), onNewsResourcesCheckedChanged = { _, _ -> }, diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index e46ada015..7d51c6e84 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -35,10 +35,10 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.lazy.grid.GridCells.Adaptive -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -175,17 +175,17 @@ private fun BookmarksGrid( onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, ) { - val scrollableState = rememberLazyGridState() + val scrollableState = rememberLazyStaggeredGridState() TrackScrollJank(scrollableState = scrollableState, stateName = "bookmarks:grid") Box( modifier = modifier .fillMaxSize(), ) { - LazyVerticalGrid( - columns = Adaptive(300.dp), + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(300.dp), contentPadding = PaddingValues(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), + verticalItemSpacing = 24.dp, state = scrollableState, modifier = Modifier .fillMaxSize() @@ -197,7 +197,7 @@ private fun BookmarksGrid( onNewsResourceViewed = onNewsResourceViewed, onTopicClick = onTopicClick, ) - item(span = { GridItemSpan(maxLineSpan) }) { + item(span = StaggeredGridItemSpan.FullLine) { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) } } diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index a24a91f1a..65b5ecbc4 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -49,13 +49,14 @@ import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridCells.Adaptive -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -153,7 +154,7 @@ internal fun ForYouScreen( val itemsAvailable = feedItemsSize(feedState, onboardingUiState) - val state = rememberLazyGridState() + val state = rememberLazyStaggeredGridState() val scrollbarState = state.scrollbarState( itemsAvailable = itemsAvailable, ) @@ -163,11 +164,11 @@ internal fun ForYouScreen( modifier = modifier .fillMaxSize(), ) { - LazyVerticalGrid( - columns = Adaptive(300.dp), + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(300.dp), contentPadding = PaddingValues(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), + verticalItemSpacing = 24.dp, modifier = Modifier .testTag("forYou:feed"), state = state, @@ -197,7 +198,7 @@ internal fun ForYouScreen( onTopicClick = onTopicClick, ) - item(span = { GridItemSpan(maxLineSpan) }, contentType = "bottomSpacing") { + item(span = StaggeredGridItemSpan.FullLine, contentType = "bottomSpacing") { Column { Spacer(modifier = Modifier.height(8.dp)) // Add space for the content to clear the "offline" snackbar. @@ -255,7 +256,7 @@ internal fun ForYouScreen( * Depending on the [onboardingUiState], this might emit no items. * */ -private fun LazyGridScope.onboarding( +private fun LazyStaggeredGridScope.onboarding( onboardingUiState: OnboardingUiState, onTopicCheckedChanged: (String, Boolean) -> Unit, saveFollowedTopics: () -> Unit, @@ -268,7 +269,7 @@ private fun LazyGridScope.onboarding( -> Unit is OnboardingUiState.Shown -> { - item(span = { GridItemSpan(maxLineSpan) }, contentType = "onboarding") { + item(span = StaggeredGridItemSpan.FullLine, contentType = "onboarding") { Column(modifier = interestsItemModifier) { Text( text = stringResource(R.string.onboarding_guidance_title), diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt index 944d17630..65b65f61d 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt +++ b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt @@ -35,11 +35,11 @@ import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells.Adaptive -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.KeyboardActions @@ -297,16 +297,16 @@ private fun SearchResultBody( onTopicClick: (String) -> Unit, searchQuery: String = "", ) { - val state = rememberLazyGridState() + val state = rememberLazyStaggeredGridState() Box( modifier = Modifier .fillMaxSize(), ) { - LazyVerticalGrid( - columns = Adaptive(300.dp), + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(300.dp), contentPadding = PaddingValues(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), + verticalItemSpacing = 24.dp, modifier = Modifier .fillMaxSize() .testTag("search:newsResources"), @@ -314,9 +314,7 @@ private fun SearchResultBody( ) { if (topics.isNotEmpty()) { item( - span = { - GridItemSpan(maxLineSpan) - }, + span = StaggeredGridItemSpan.FullLine, ) { Text( text = buildAnnotatedString { @@ -331,9 +329,7 @@ private fun SearchResultBody( val topicId = followableTopic.topic.id item( key = "topic-$topicId", // Append a prefix to distinguish a key for news resources - span = { - GridItemSpan(maxLineSpan) - }, + span = StaggeredGridItemSpan.FullLine, ) { InterestsItem( name = followableTopic.topic.name, @@ -353,9 +349,7 @@ private fun SearchResultBody( if (newsResources.isNotEmpty()) { item( - span = { - GridItemSpan(maxLineSpan) - }, + span = StaggeredGridItemSpan.FullLine, ) { Text( text = buildAnnotatedString { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 08391f2e6..3a2eb355d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidGradlePlugin = "8.1.1" androidxActivity = "1.8.0-alpha06" androidxAppCompat = "1.5.1" androidxBrowser = "1.4.0" -androidxComposeBom = "2023.06.01" +androidxComposeBom = "2023.09.00" androidxComposeCompiler = "1.5.0" androidxComposeRuntimeTracing = "1.0.0-alpha03" androidxCore = "1.9.0"