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 dcd5067d3..277dc5e65 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -190,7 +190,7 @@ private fun ExtensionDetails( key = { it.source.id }, ) { source -> SourceSwitchPreference( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), 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 c65f0d0b1..7f7e18baa 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.animateItemPlacement(), + modifier = Modifier.animateItem(), 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 d08a2d93b..a3cb94800 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -187,14 +187,14 @@ private fun ExtensionContent( } ExtensionHeader( textRes = header.textRes, - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), action = action, ) } is ExtensionUiModel.Header.Text -> { ExtensionHeader( text = header.text, - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), ) } } @@ -212,7 +212,7 @@ private fun ExtensionContent( }, ) { item -> ExtensionItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), item = item, onClickItem = { when (it) { 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 7f79dd3ed..da4db5e98 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -4,6 +4,7 @@ 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 @@ -79,6 +80,7 @@ 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 404d11476..3f8a67c4c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -133,7 +133,7 @@ private fun MigrateSourceList( key = { (source, _) -> "migrate-${source.id}" }, ) { (source, count) -> MigrateSourceItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), source = source, count = count, onClickItem = { onClickItem(source) }, 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 e334a451e..9f74c3799 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt @@ -68,7 +68,7 @@ private fun SourcesFilterContent( contentType = "source-filter-header", ) { SourcesFilterHeader( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), language = language, enabled = enabled, onClickItem = onClickLanguage, @@ -81,7 +81,7 @@ private fun SourcesFilterContent( contentType = { "source-filter-item" }, ) { source -> SourcesFilterItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), 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 d0411b7a8..56644b3d8 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -74,12 +74,12 @@ fun SourcesScreen( when (model) { is SourceUiModel.Header -> { SourceHeader( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), language = model.language, ) } is SourceUiModel.Item -> SourceItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), source = model.source, onClickItem = onClickItem, onLongClickItem = onLongClickItem, 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 29ef3b97b..5de9a2142 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 @@ -32,9 +32,10 @@ fun GlobalSearchResultItem( title: String, subtitle: String, onClick: () -> Unit, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { - Column { + Column(modifier = 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 34a6a1217..df553a203 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.animateItemPlacement(), + modifier = Modifier.animateItem(), 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 58d7f163a..a151e9b2f 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,8 +10,7 @@ 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.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrollingUp +import tachiyomi.presentation.core.util.shouldExpandFAB @Composable fun CategoryFloatingActionButton( @@ -23,7 +22,7 @@ fun CategoryFloatingActionButton( text = { Text(text = stringResource(MR.strings.action_add)) }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) }, onClick = onCreate, - expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(), + expanded = lazyListState.shouldExpandFAB(), modifier = modifier, ) } 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 c15604829..dba526b54 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -113,14 +113,14 @@ private fun HistoryScreenContent( when (item) { is HistoryUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), text = relativeDateText(item.date), ) } is HistoryUiModel.Item -> { val value = item.item HistoryItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), 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 9a6e3f6fd..efe44ceb4 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -75,8 +75,7 @@ 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.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrollingUp +import tachiyomi.presentation.core.util.shouldExpandFAB import tachiyomi.source.local.isLocal import java.time.Instant @@ -346,7 +345,7 @@ private fun MangaScreenSmallImpl( }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, - expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), + expanded = chapterListState.shouldExpandFAB(), ) } }, @@ -594,7 +593,7 @@ fun MangaScreenLargeImpl( }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, - expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), + expanded = chapterListState.shouldExpandFAB(), ) } }, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index ac4946c99..f1b660626 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -42,7 +42,7 @@ import androidx.compose.material.icons.outlined.Sync import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement +import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle @@ -649,7 +649,7 @@ private fun TagsChip( modifier: Modifier = Modifier, onClick: () -> Unit, ) { - CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { + CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) { SuggestionChip( modifier = modifier, onClick = onClick, 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 d7c46e90d..747ac09e0 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 @@ -31,8 +31,6 @@ 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( @@ -96,8 +94,8 @@ fun ScanlatorFilterDialog( } } } - if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (state.canScrollForward) 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 79e45159f..8b3d9c07b 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 20a924d11..e6b2a227f 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.animateItemPlacement(), + modifier = Modifier.animateItem(), 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 c8e757491..7daf65168 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,8 +26,6 @@ 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( @@ -69,8 +67,8 @@ fun ListPreferenceWidget( } } } - if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (state.canScrollForward) 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 f6e195e4d..be5029ac3 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,8 +30,6 @@ 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 @@ -115,16 +113,8 @@ fun TriStateListDialog( } } - if (!listState.isScrolledToStart()) { - HorizontalDivider( - modifier = Modifier.align(Alignment.TopCenter), - ) - } - if (!listState.isScrolledToEnd()) { - HorizontalDivider( - modifier = Modifier.align(Alignment.BottomCenter), - ) - } + if (listState.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (listState.canScrollForward) 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 053ba7bbc..c6fda5cc5 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt @@ -43,8 +43,6 @@ 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( @@ -86,8 +84,8 @@ fun TrackStatusSelector( } } } - if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (state.canScrollForward) 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 a69252ffc..9f5c92479 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 - .animateItemPlacement() + .animateItem() .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), ) { Text( @@ -91,14 +91,14 @@ internal fun LazyListScope.updatesUiItems( when (item) { is UpdatesUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), text = relativeDateText(item.date), ) } is UpdatesUiModel.Item -> { val updatesItem = item.item UpdatesUiItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), 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 6bee9203e..80c2f90b9 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Permissions.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Permissions.kt @@ -7,9 +7,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/gradle/compose.versions.toml b/gradle/compose.versions.toml index 58c1ed191..1249e384f 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,7 +1,6 @@ [versions] compiler = "1.5.12" -# 2024.04.00-alpha01 has several bugs with the new animateItem() modifier -compose-bom = "2024.03.00-alpha02" +compose-bom = "2024.05.00-alpha01" 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 fa6eb375a..54d2ad700 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,7 +153,9 @@ fun AdaptiveSheet( if (enableSwipeDismiss) { Modifier.nestedScroll( remember(anchoredDraggableState) { - anchoredDraggableState.preUpPostDownNestedScrollConnection() + anchoredDraggableState.preUpPostDownNestedScrollConnection( + onFling = { scope.launch { anchoredDraggableState.settle(it) } } + ) }, ) } else { @@ -201,55 +203,51 @@ fun AdaptiveSheet( } } -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 - } +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 } - - 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()) { - 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 fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + return if (source == NestedScrollSource.UserInput) { + 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 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 9642cec71..b1987185b 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,34 +1,16 @@ 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.PullToRefreshContainer -import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState 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 @@ -46,242 +28,26 @@ fun PullRefresh( indicatorPadding: PaddingValues = PaddingValues(0.dp), content: @Composable () -> Unit, ) { - val state = rememberPullToRefreshState( - isRefreshing = refreshing, - extraVerticalOffset = indicatorPadding.calculateTopPadding(), - enabled = enabled, - onRefresh = onRefresh, - ) - - Box(modifier.nestedScroll(state.nestedScrollConnection)) { + val state = rememberPullToRefreshState() + Box( + modifier = modifier + .pullToRefresh( + isRefreshing = refreshing, + state = state, + enabled = enabled, + onRefresh = onRefresh, + ) + ) { content() - 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, + PullToRefreshDefaults.Indicator( modifier = Modifier .align(Alignment.TopCenter) - .padding(contentPadding), + .padding(indicatorPadding), + isRefreshing = refreshing, + state = state, containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + color = 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 1abb3205a..cdabf1b49 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,63 +3,16 @@ 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.isScrolledToStart(): Boolean { +fun LazyListState.shouldExpandFAB(): Boolean { return remember { derivedStateOf { - val firstItem = layoutInfo.visibleItemsInfo.firstOrNull() - firstItem == null || firstItem.offset == layoutInfo.viewportStartOffset + (firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0) || + lastScrolledBackward || + !canScrollForward } - }.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 + } + .value }