From c3fb5c0bec8749850630d97ebc5b377df2f0c490 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sun, 5 May 2024 12:58:03 -0400 Subject: [PATCH] Revert "Bump compose version" This reverts commit 5550ddad4ef023b335024d9648a59c36ae242168. --- .../browse/ExtensionDetailsScreen.kt | 2 +- .../browse/ExtensionFilterScreen.kt | 2 +- .../presentation/browse/ExtensionsScreen.kt | 6 +- .../kanade/presentation/browse/FeedScreen.kt | 2 +- .../presentation/browse/GlobalSearchScreen.kt | 2 - .../browse/MigrateSourceScreen.kt | 2 +- .../browse/MigrationListScreen.kt | 3 +- .../presentation/browse/SourceFeedScreen.kt | 2 +- .../browse/SourcesFilterScreen.kt | 6 +- .../presentation/browse/SourcesScreen.kt | 4 +- .../components/GlobalSearchResultItems.kt | 6 +- .../presentation/category/CategoryScreen.kt | 2 +- .../CategoryFloatingActionButton.kt | 5 +- .../biometric/BiometricTimesContent.kt | 2 +- .../components/genre/SortTagContent.kt | 2 +- .../sources/SourceCategoryContent.kt | 2 +- .../presentation/history/HistoryScreen.kt | 4 +- .../kanade/presentation/manga/MangaScreen.kt | 7 +- .../manga/components/NamespaceTags.kt | 4 +- .../manga/components/ScanlatorFilterDialog.kt | 6 +- .../more/onboarding/PermissionStep.kt | 2 +- .../components/ExtensionReposContent.kt | 2 +- .../settings/widget/ListPreferenceWidget.kt | 6 +- .../settings/widget/TriStateListDialog.kt | 14 +- .../track/TrackInfoDialogSelector.kt | 6 +- .../presentation/updates/UpdatesUiItem.kt | 6 +- .../kanade/presentation/util/Permissions.kt | 2 +- .../browse/source/browse/AutoCompleteItem.kt | 3 +- .../java/exh/ui/batchadd/BatchAddScreen.kt | 2 +- gradle/compose.versions.toml | 3 +- .../core/components/AdaptiveSheet.kt | 92 +++--- .../core/components/material/PullRefresh.kt | 270 ++++++++++++++++-- .../presentation/core/util/LazyListState.kt | 59 +++- 33 files changed, 420 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 6829abcb3..a246146d9 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -198,7 +198,7 @@ private fun ExtensionDetails( key = { it.source.id }, ) { source -> SourceSwitchPreference( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), source = source, onClickSourcePreferences = onClickSourcePreferences, onClickSource = onClickSource, diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt index 7f7e18baa..c65f0d0b1 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt @@ -58,7 +58,7 @@ private fun ExtensionFilterContent( ) { items(state.languages) { language -> SwitchPreferenceWidget( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), title = LocaleHelper.getSourceDisplayName(language, context), checked = language in state.enabledLanguages, onCheckedChanged = { onClickLang(language) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index b62bfef9d..f1619177e 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -188,14 +188,14 @@ private fun ExtensionContent( } ExtensionHeader( textRes = header.textRes, - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), action = action, ) } is ExtensionUiModel.Header.Text -> { ExtensionHeader( text = header.text, - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), ) } } @@ -213,7 +213,7 @@ private fun ExtensionContent( }, ) { item -> ExtensionItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), item = item, onClickItem = { when (it) { diff --git a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index 6f3d06469..fa95f208a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -103,7 +103,7 @@ fun FeedScreen( key = { it.feed.id }, ) { item -> GlobalSearchResultItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), title = item.title, subtitle = item.subtitle, onLongClick = { diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index b3c11a1c8..0ea1adf2e 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.State -import androidx.compose.ui.Modifier import eu.kanade.presentation.browse.components.GlobalSearchCardRow import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem @@ -81,7 +80,6 @@ internal fun GlobalSearchContent( } ?: source.name, subtitle = LocaleHelper.getLocalizedDisplayName(source.lang), onClick = { onClickSource(source) }, - modifier = Modifier.animateItem(), ) { when (result) { SearchItemResult.Loading -> { diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index 5f7dae988..792e2e610 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -144,7 +144,7 @@ private fun MigrateSourceList( key = { (source, _) -> "migrate-${source.id}" }, ) { (source, count) -> MigrateSourceItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), source = source, count = count, onClickItem = { onClickItem(source) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrationListScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrationListScreen.kt index 4798a4a56..a9d3547c9 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrationListScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrationListScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowForward +import androidx.compose.material.icons.outlined.ArrowForward import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.CopyAll import androidx.compose.material.icons.outlined.Done @@ -94,7 +95,7 @@ fun MigrationListScreen( Row( Modifier .fillMaxWidth() - .animateItem() + .animateItemPlacement() .padding(horizontal = 16.dp) .height(IntrinsicSize.Min), horizontalArrangement = Arrangement.SpaceBetween, diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index 08c58bd10..0c156e352 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -153,7 +153,7 @@ fun SourceFeedList( key = { it.id }, ) { item -> GlobalSearchResultItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), title = item.title, subtitle = null, onLongClick = if (item is SourceFeedUI.SourceSavedSearch) { diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt index f2a9486d2..49765a836 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt @@ -79,7 +79,7 @@ private fun SourcesFilterContent( contentType = "source-filter-header", ) { SourcesFilterHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), language = language, enabled = enabled, onClickItem = onClickLanguage, @@ -95,7 +95,7 @@ private fun SourcesFilterContent( sources.none { it.id.toString() in state.disabledSources } } SourcesFilterToggle( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), isEnabled = toggleEnabled, onClickItem = { onClickSources(!toggleEnabled, sources) @@ -109,7 +109,7 @@ private fun SourcesFilterContent( contentType = { "source-filter-item" }, ) { source -> SourcesFilterItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), source = source, enabled = "${source.id}" !in state.disabledSources, onClickItem = onClickSource, diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt index 31223fdac..d4de0b9fe 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -81,7 +81,7 @@ fun SourcesScreen( when (model) { is SourceUiModel.Header -> { SourceHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), language = model.language, // SY --> isCategory = model.isCategory, @@ -89,7 +89,7 @@ fun SourcesScreen( ) } is SourceUiModel.Item -> SourceItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), source = model.source, // SY --> showLatest = state.showLatest, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt index 9cd382d52..e8536ccb9 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt @@ -30,6 +30,9 @@ import tachiyomi.presentation.core.i18n.stringResource @Composable fun GlobalSearchResultItem( + // SY --> + modifier: Modifier = Modifier, + // SY <-- title: String, // SY --> subtitle: String?, @@ -38,10 +41,9 @@ fun GlobalSearchResultItem( // SY --> onLongClick: (() -> Unit)? = null, // SY <-- - modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { - Column(modifier = modifier) { + Column(modifier) { Row( modifier = Modifier .padding( diff --git a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt index 9bb8c8d53..8ede51f1d 100644 --- a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt @@ -107,7 +107,7 @@ private fun CategoryContent( key = { _, category -> "category-${category.id}" }, ) { index, category -> CategoryListItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), category = category, canMoveUp = index != 0, canMoveDown = index != categories.lastIndex, diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt index a151e9b2f..58d7f163a 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt @@ -10,7 +10,8 @@ import androidx.compose.ui.Modifier import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.shouldExpandFAB +import tachiyomi.presentation.core.util.isScrolledToEnd +import tachiyomi.presentation.core.util.isScrollingUp @Composable fun CategoryFloatingActionButton( @@ -22,7 +23,7 @@ fun CategoryFloatingActionButton( text = { Text(text = stringResource(MR.strings.action_add)) }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) }, onClick = onCreate, - expanded = lazyListState.shouldExpandFAB(), + expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(), modifier = modifier, ) } diff --git a/app/src/main/java/eu/kanade/presentation/category/components/biometric/BiometricTimesContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/biometric/BiometricTimesContent.kt index 0f8cd9c73..d8465e295 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/biometric/BiometricTimesContent.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/biometric/BiometricTimesContent.kt @@ -26,7 +26,7 @@ fun BiometricTimesContent( ) { items(timeRanges, key = { it.formattedString }) { timeRange -> BiometricTimesListItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), timeRange = timeRange, onDelete = { onClickDelete(timeRange) }, ) diff --git a/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagContent.kt index dcce5e0ea..4700e1883 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagContent.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagContent.kt @@ -27,7 +27,7 @@ fun SortTagContent( ) { itemsIndexed(tags, key = { _, tag -> tag }) { index, tag -> SortTagListItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), tag = tag, canMoveUp = index != 0, canMoveDown = index != tags.lastIndex, diff --git a/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt index e3925c7aa..2f5b26b3e 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt @@ -26,7 +26,7 @@ fun SourceCategoryContent( ) { items(categories, key = { it }) { category -> SourceCategoryListItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), category = category, onRename = { onClickRename(category) }, onDelete = { onClickDelete(category) }, diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index 66f31f3d7..a973a2bb6 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -114,14 +114,14 @@ private fun HistoryScreenContent( when (item) { is HistoryUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), text = relativeDateText(item.date), ) } is HistoryUiModel.Item -> { val value = item.item HistoryItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), history = value, onClickCover = { onClickCover(value) }, onClickResume = { onClickResume(value) }, diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 61af3e5ab..0f1cc335b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -102,7 +102,8 @@ import tachiyomi.presentation.core.components.material.ExtendedFloatingActionBut import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.shouldExpandFAB +import tachiyomi.presentation.core.util.isScrolledToEnd +import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.source.local.isLocal import java.time.Instant import java.time.ZoneId @@ -430,7 +431,7 @@ private fun MangaScreenSmallImpl( }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, - expanded = chapterListState.shouldExpandFAB(), + expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), ) } }, @@ -754,7 +755,7 @@ fun MangaScreenLargeImpl( }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, - expanded = chapterListState.shouldExpandFAB(), + expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), ) } }, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/NamespaceTags.kt b/app/src/main/java/eu/kanade/presentation/manga/components/NamespaceTags.kt index 29a9209f0..6838732ed 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/NamespaceTags.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/NamespaceTags.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.LocalMinimumInteractiveComponentSize +import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Surface @@ -141,7 +141,7 @@ fun TagsChip( border: ChipBorder? = SuggestionChipDefaults.suggestionChipBorder(), borderM3: BorderStroke? = SuggestionChipDefaultsM3.suggestionChipBorder(enabled = true), ) { - CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) { + CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { if (onClick != null) { SuggestionChip( modifier = modifier, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt index 36ae6fa32..497e66907 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt @@ -32,6 +32,8 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.isScrolledToEnd +import tachiyomi.presentation.core.util.isScrolledToStart @Composable fun ScanlatorFilterDialog( @@ -95,8 +97,8 @@ fun ScanlatorFilterDialog( } } } - if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } }, properties = DialogProperties( diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt index 8b3d9c07b..79e45159f 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt @@ -28,11 +28,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.core.content.getSystemService import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.compose.LocalLifecycleOwner import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import tachiyomi.i18n.MR diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt index e6b2a227f..20a924d11 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt @@ -46,7 +46,7 @@ fun ExtensionReposContent( repos.forEach { item { ExtensionRepoListItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), repo = it, onOpenWebsite = { onOpenWebsite(it) }, onDelete = { onClickDelete(it.baseUrl) }, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt index 7daf65168..c8e757491 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.unit.dp import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.isScrolledToEnd +import tachiyomi.presentation.core.util.isScrolledToStart @Composable fun ListPreferenceWidget( @@ -67,8 +69,8 @@ fun ListPreferenceWidget( } } } - if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } }, confirmButton = { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt index be5029ac3..f6e195e4d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt @@ -30,6 +30,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.isScrolledToEnd +import tachiyomi.presentation.core.util.isScrolledToStart private enum class State { CHECKED, INVERSED, UNCHECKED @@ -113,8 +115,16 @@ fun TriStateListDialog( } } - if (listState.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (listState.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (!listState.isScrolledToStart()) { + HorizontalDivider( + modifier = Modifier.align(Alignment.TopCenter), + ) + } + if (!listState.isScrolledToEnd()) { + HorizontalDivider( + modifier = Modifier.align(Alignment.BottomCenter), + ) + } } } }, diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt index c6fda5cc5..053ba7bbc 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt @@ -43,6 +43,8 @@ import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.isScrolledToEnd +import tachiyomi.presentation.core.util.isScrolledToStart @Composable fun TrackStatusSelector( @@ -84,8 +86,8 @@ fun TrackStatusSelector( } } } - if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) }, onConfirm = onConfirm, onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt index 3735ad5c7..3b4b3f554 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -54,7 +54,7 @@ internal fun LazyListScope.updatesLastUpdatedItem( item(key = "updates-lastUpdated") { Box( modifier = Modifier - .animateItem() + .animateItemPlacement() .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), ) { Text( @@ -94,14 +94,14 @@ internal fun LazyListScope.updatesUiItems( when (item) { is UpdatesUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), text = relativeDateText(item.date), ) } is UpdatesUiModel.Item -> { val updatesItem = item.item UpdatesUiItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemPlacement(), update = updatesItem.update, selected = updatesItem.selected, readProgress = updatesItem.update.lastPageRead diff --git a/app/src/main/java/eu/kanade/presentation/util/Permissions.kt b/app/src/main/java/eu/kanade/presentation/util/Permissions.kt index 798f2ff24..b4f4f4bd3 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Permissions.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Permissions.kt @@ -9,9 +9,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.compose.LocalLifecycleOwner @Composable fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/AutoCompleteItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/AutoCompleteItem.kt index 19cb1652e..9c773ac62 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/AutoCompleteItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/AutoCompleteItem.kt @@ -17,7 +17,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.InputChip import androidx.compose.material3.InputChipDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -155,7 +154,7 @@ fun AutoCompleteTextField( null }, modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryEditable) + .menuAnchor() .fillMaxWidth() .runOnEnterKeyPressed { submit() }, singleLine = true, diff --git a/app/src/main/java/exh/ui/batchadd/BatchAddScreen.kt b/app/src/main/java/exh/ui/batchadd/BatchAddScreen.kt index b8377ec8c..0ee8e6be9 100644 --- a/app/src/main/java/exh/ui/batchadd/BatchAddScreen.kt +++ b/app/src/main/java/exh/ui/batchadd/BatchAddScreen.kt @@ -76,7 +76,7 @@ class BatchAddScreen : Screen() { text = stringResource(SYMR.strings.eh_batch_add_description), ) }, - keyboardOptions = KeyboardOptions(autoCorrectEnabled = false), + keyboardOptions = KeyboardOptions(autoCorrect = false), textStyle = MaterialTheme.typography.bodyLarge, ) diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 1249e384f..58c1ed191 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,6 +1,7 @@ [versions] compiler = "1.5.12" -compose-bom = "2024.05.00-alpha01" +# 2024.04.00-alpha01 has several bugs with the new animateItem() modifier +compose-bom = "2024.03.00-alpha02" accompanist = "0.35.0-alpha" [libraries] diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt index 54d2ad700..fa6eb375a 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt @@ -153,9 +153,7 @@ fun AdaptiveSheet( if (enableSwipeDismiss) { Modifier.nestedScroll( remember(anchoredDraggableState) { - anchoredDraggableState.preUpPostDownNestedScrollConnection( - onFling = { scope.launch { anchoredDraggableState.settle(it) } } - ) + anchoredDraggableState.preUpPostDownNestedScrollConnection() }, ) } else { @@ -203,51 +201,55 @@ fun AdaptiveSheet( } } -private fun AnchoredDraggableState.preUpPostDownNestedScrollConnection( - onFling: (velocity: Float) -> Unit -) = object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.UserInput) { - dispatchRawDelta(delta).toOffset() - } else { - Offset.Zero +private fun AnchoredDraggableState.preUpPostDownNestedScrollConnection() = + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.Drag) { + dispatchRawDelta(delta).toOffset() + } else { + Offset.Zero + } } - } - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - return if (source == NestedScrollSource.UserInput) { - dispatchRawDelta(available.toFloat()).toOffset() - } else { - Offset.Zero + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + return if (source == NestedScrollSource.Drag) { + dispatchRawDelta(available.toFloat()).toOffset() + } else { + Offset.Zero + } } - } - override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = available.toFloat() - return if (toFling < 0 && offset > anchors.minAnchor()) { - onFling(toFling) - // since we go to the anchor with tween settling, consume all for the best UX - available - } else { - Velocity.Zero + override suspend fun onPreFling(available: Velocity): Velocity { + val toFling = available.toFloat() + return if (toFling < 0 && offset > anchors.minAnchor()) { + settle(toFling) + // since we go to the anchor with tween settling, consume all for the best UX + available + } else { + Velocity.Zero + } } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + val toFling = available.toFloat() + return if (toFling > 0) { + settle(toFling) + available + } else { + Velocity.Zero + } + } + + private fun Float.toOffset(): Offset = Offset(0f, this) + + @JvmName("velocityToFloat") + private fun Velocity.toFloat() = this.y + + @JvmName("offsetToFloat") + private fun Offset.toFloat(): Float = this.y } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - onFling(available.toFloat()) - return available - } - - private fun Float.toOffset(): Offset = Offset(0f, this) - - @JvmName("velocityToFloat") - private fun Velocity.toFloat() = this.y - - @JvmName("offsetToFloat") - private fun Offset.toFloat(): Float = this.y -} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt index b1987185b..9642cec71 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt @@ -1,16 +1,34 @@ package tachiyomi.presentation.core.components.material +import androidx.compose.animation.core.animate import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults -import androidx.compose.material3.pulltorefresh.pullToRefresh -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.pulltorefresh.PullToRefreshContainer +import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +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 import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp +import kotlin.math.abs +import kotlin.math.pow /** * @param refreshing Whether the layout is currently refreshing @@ -28,26 +46,242 @@ fun PullRefresh( indicatorPadding: PaddingValues = PaddingValues(0.dp), content: @Composable () -> Unit, ) { - val state = rememberPullToRefreshState() - Box( - modifier = modifier - .pullToRefresh( - isRefreshing = refreshing, - state = state, - enabled = enabled, - onRefresh = onRefresh, - ) - ) { + val state = rememberPullToRefreshState( + isRefreshing = refreshing, + extraVerticalOffset = indicatorPadding.calculateTopPadding(), + enabled = enabled, + onRefresh = onRefresh, + ) + + Box(modifier.nestedScroll(state.nestedScrollConnection)) { content() - PullToRefreshDefaults.Indicator( + val contentPadding = remember(indicatorPadding) { + object : PaddingValues { + override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = + indicatorPadding.calculateLeftPadding(layoutDirection) + + override fun calculateTopPadding(): Dp = 0.dp + + override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = + indicatorPadding.calculateRightPadding(layoutDirection) + + override fun calculateBottomPadding(): Dp = + indicatorPadding.calculateBottomPadding() + } + } + PullToRefreshContainer( + state = state, modifier = Modifier .align(Alignment.TopCenter) - .padding(indicatorPadding), - isRefreshing = refreshing, - state = state, + .padding(contentPadding), containerColor = MaterialTheme.colorScheme.surfaceVariant, - color = MaterialTheme.colorScheme.onSurfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, ) } } + +@Composable +private fun rememberPullToRefreshState( + isRefreshing: Boolean, + extraVerticalOffset: Dp, + positionalThreshold: Dp = 64.dp, + enabled: () -> Boolean = { true }, + onRefresh: () -> Unit, +): PullToRefreshStateImpl { + val density = LocalDensity.current + val extraVerticalOffsetPx = with(density) { extraVerticalOffset.toPx() } + val positionalThresholdPx = with(density) { positionalThreshold.toPx() } + return rememberSaveable( + extraVerticalOffset, + positionalThresholdPx, + enabled, + onRefresh, + saver = PullToRefreshStateImpl.Saver( + extraVerticalOffset = extraVerticalOffsetPx, + positionalThreshold = positionalThresholdPx, + enabled = enabled, + onRefresh = onRefresh, + ), + ) { + PullToRefreshStateImpl( + initialRefreshing = isRefreshing, + extraVerticalOffset = extraVerticalOffsetPx, + positionalThreshold = positionalThresholdPx, + enabled = enabled, + onRefresh = onRefresh, + ) + }.also { + LaunchedEffect(isRefreshing) { + if (isRefreshing && !it.isRefreshing) { + it.startRefreshAnimated() + } else if (!isRefreshing && it.isRefreshing) { + it.endRefreshAnimated() + } + } + } +} + +/** + * Creates a [PullToRefreshState]. + * + * @param positionalThreshold The positional threshold, in pixels, in which a refresh is triggered + * @param extraVerticalOffset Extra vertical offset, in pixels, for the "refreshing" state + * @param initialRefreshing The initial refreshing value of [PullToRefreshState] + * @param enabled a callback used to determine whether scroll events are to be handled by this + * @param onRefresh a callback to run when pull-to-refresh action is triggered by user + * [PullToRefreshState] + */ +private class PullToRefreshStateImpl( + initialRefreshing: Boolean, + private val extraVerticalOffset: Float, + override val positionalThreshold: Float, + enabled: () -> Boolean, + private val onRefresh: () -> Unit, +) : PullToRefreshState { + + override val progress get() = adjustedDistancePulled / positionalThreshold + override var verticalOffset by mutableFloatStateOf(if (initialRefreshing) refreshingVerticalOffset else 0f) + + override var isRefreshing by mutableStateOf(initialRefreshing) + + private val refreshingVerticalOffset: Float + get() = positionalThreshold + extraVerticalOffset + + override fun startRefresh() { + isRefreshing = true + verticalOffset = refreshingVerticalOffset + } + + suspend fun startRefreshAnimated() { + isRefreshing = true + animateTo(refreshingVerticalOffset) + } + + override fun endRefresh() { + verticalOffset = 0f + isRefreshing = false + } + + suspend fun endRefreshAnimated() { + animateTo(0f) + isRefreshing = false + } + + override var nestedScrollConnection = object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset = when { + !enabled() -> Offset.Zero + // Swiping up + source == NestedScrollSource.Drag && available.y < 0 -> { + consumeAvailableOffset(available) + } + else -> Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset = when { + !enabled() -> Offset.Zero + // Swiping down + source == NestedScrollSource.Drag && available.y > 0 -> { + consumeAvailableOffset(available) + } + else -> Offset.Zero + } + + override suspend fun onPreFling(available: Velocity): Velocity { + return Velocity(0f, onRelease(available.y)) + } + } + + /** Helper method for nested scroll connection */ + fun consumeAvailableOffset(available: Offset): Offset { + val y = if (isRefreshing) { + 0f + } else { + val newOffset = (distancePulled + available.y).coerceAtLeast(0f) + val dragConsumed = newOffset - distancePulled + distancePulled = newOffset + verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress.coerceIn(0f, 1f)) + dragConsumed + } + return Offset(0f, y) + } + + /** Helper method for nested scroll connection. Calls onRefresh callback when triggered */ + suspend fun onRelease(velocity: Float): Float { + if (isRefreshing) return 0f // Already refreshing, do nothing + // Trigger refresh + if (adjustedDistancePulled > positionalThreshold) { + onRefresh() + startRefreshAnimated() + } else { + animateTo(0f) + } + + val consumed = when { + // We are flinging without having dragged the pull refresh (for example a fling inside + // a list) - don't consume + distancePulled == 0f -> 0f + // If the velocity is negative, the fling is upwards, and we don't want to prevent the + // the list from scrolling + velocity < 0f -> 0f + // We are showing the indicator, and the fling is downwards - consume everything + else -> velocity + } + distancePulled = 0f + return consumed + } + + suspend fun animateTo(offset: Float) { + animate(initialValue = verticalOffset, targetValue = offset) { value, _ -> + verticalOffset = value + } + } + + /** Provides custom vertical offset behavior for [PullToRefreshContainer] */ + fun calculateVerticalOffset(): Float = when { + // If drag hasn't gone past the threshold, the position is the adjustedDistancePulled. + adjustedDistancePulled <= positionalThreshold -> adjustedDistancePulled + else -> { + // How far beyond the threshold pull has gone, as a percentage of the threshold. + val overshootPercent = abs(progress) - 1.0f + // Limit the overshoot to 200%. Linear between 0 and 200. + val linearTension = overshootPercent.coerceIn(0f, 2f) + // Non-linear tension. Increases with linearTension, but at a decreasing rate. + val tensionPercent = linearTension - linearTension.pow(2) / 4 + // The additional offset beyond the threshold. + val extraOffset = positionalThreshold * tensionPercent + positionalThreshold + extraOffset + } + } + + companion object { + /** The default [Saver] for [PullToRefreshStateImpl]. */ + fun Saver( + extraVerticalOffset: Float, + positionalThreshold: Float, + enabled: () -> Boolean, + onRefresh: () -> Unit, + ) = Saver( + save = { it.isRefreshing }, + restore = { isRefreshing -> + PullToRefreshStateImpl( + initialRefreshing = isRefreshing, + extraVerticalOffset = extraVerticalOffset, + positionalThreshold = positionalThreshold, + enabled = enabled, + onRefresh = onRefresh, + ) + }, + ) + } + + private var distancePulled by mutableFloatStateOf(0f) + private val adjustedDistancePulled: Float get() = distancePulled * 0.5f +} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt index cdabf1b49..1abb3205a 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt @@ -3,16 +3,63 @@ package tachiyomi.presentation.core.util import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue @Composable -fun LazyListState.shouldExpandFAB(): Boolean { +fun LazyListState.isScrolledToStart(): Boolean { return remember { derivedStateOf { - (firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0) || - lastScrolledBackward || - !canScrollForward + val firstItem = layoutInfo.visibleItemsInfo.firstOrNull() + firstItem == null || firstItem.offset == layoutInfo.viewportStartOffset } - } - .value + }.value +} + +@Composable +fun LazyListState.isScrolledToEnd(): Boolean { + return remember { + derivedStateOf { + val lastItem = layoutInfo.visibleItemsInfo.lastOrNull() + lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset + } + }.value +} + +@Composable +fun LazyListState.isScrollingUp(): Boolean { + var previousIndex by remember { mutableIntStateOf(firstVisibleItemIndex) } + var previousScrollOffset by remember { mutableIntStateOf(firstVisibleItemScrollOffset) } + return remember { + derivedStateOf { + if (previousIndex != firstVisibleItemIndex) { + previousIndex > firstVisibleItemIndex + } else { + previousScrollOffset >= firstVisibleItemScrollOffset + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + }.value +} + +@Composable +fun LazyListState.isScrollingDown(): Boolean { + var previousIndex by remember { mutableIntStateOf(firstVisibleItemIndex) } + var previousScrollOffset by remember { mutableIntStateOf(firstVisibleItemScrollOffset) } + return remember { + derivedStateOf { + if (previousIndex != firstVisibleItemIndex) { + previousIndex < firstVisibleItemIndex + } else { + previousScrollOffset <= firstVisibleItemScrollOffset + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + }.value }