Bump compose version

Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
This commit is contained in:
AntsyLich 2024-04-08 15:35:17 +06:00
parent 263e467cde
commit e473c7f09f
No known key found for this signature in database
25 changed files with 112 additions and 411 deletions

View file

@ -190,7 +190,7 @@ private fun ExtensionDetails(
key = { it.source.id }, key = { it.source.id },
) { source -> ) { source ->
SourceSwitchPreference( SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
source = source, source = source,
onClickSourcePreferences = onClickSourcePreferences, onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource, onClickSource = onClickSource,

View file

@ -58,7 +58,7 @@ private fun ExtensionFilterContent(
) { ) {
items(state.languages) { language -> items(state.languages) { language ->
SwitchPreferenceWidget( SwitchPreferenceWidget(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
title = LocaleHelper.getSourceDisplayName(language, context), title = LocaleHelper.getSourceDisplayName(language, context),
checked = language in state.enabledLanguages, checked = language in state.enabledLanguages,
onCheckedChanged = { onClickLang(language) }, onCheckedChanged = { onClickLang(language) },

View file

@ -187,14 +187,14 @@ private fun ExtensionContent(
} }
ExtensionHeader( ExtensionHeader(
textRes = header.textRes, textRes = header.textRes,
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
action = action, action = action,
) )
} }
is ExtensionUiModel.Header.Text -> { is ExtensionUiModel.Header.Text -> {
ExtensionHeader( ExtensionHeader(
text = header.text, text = header.text,
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
) )
} }
} }
@ -212,7 +212,7 @@ private fun ExtensionContent(
}, },
) { item -> ) { item ->
ExtensionItem( ExtensionItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
item = item, item = item,
onClickItem = { onClickItem = {
when (it) { when (it) {

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import eu.kanade.presentation.browse.components.GlobalSearchCardRow import eu.kanade.presentation.browse.components.GlobalSearchCardRow
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
@ -79,6 +80,7 @@ internal fun GlobalSearchContent(
} ?: source.name, } ?: source.name,
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang), subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
onClick = { onClickSource(source) }, onClick = { onClickSource(source) },
modifier = Modifier.animateItem(),
) { ) {
when (result) { when (result) {
SearchItemResult.Loading -> { SearchItemResult.Loading -> {

View file

@ -133,7 +133,7 @@ private fun MigrateSourceList(
key = { (source, _) -> "migrate-${source.id}" }, key = { (source, _) -> "migrate-${source.id}" },
) { (source, count) -> ) { (source, count) ->
MigrateSourceItem( MigrateSourceItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
source = source, source = source,
count = count, count = count,
onClickItem = { onClickItem(source) }, onClickItem = { onClickItem(source) },

View file

@ -68,7 +68,7 @@ private fun SourcesFilterContent(
contentType = "source-filter-header", contentType = "source-filter-header",
) { ) {
SourcesFilterHeader( SourcesFilterHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
language = language, language = language,
enabled = enabled, enabled = enabled,
onClickItem = onClickLanguage, onClickItem = onClickLanguage,
@ -81,7 +81,7 @@ private fun SourcesFilterContent(
contentType = { "source-filter-item" }, contentType = { "source-filter-item" },
) { source -> ) { source ->
SourcesFilterItem( SourcesFilterItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
source = source, source = source,
enabled = "${source.id}" !in state.disabledSources, enabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource, onClickItem = onClickSource,

View file

@ -74,12 +74,12 @@ fun SourcesScreen(
when (model) { when (model) {
is SourceUiModel.Header -> { is SourceUiModel.Header -> {
SourceHeader( SourceHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
language = model.language, language = model.language,
) )
} }
is SourceUiModel.Item -> SourceItem( is SourceUiModel.Item -> SourceItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
source = model.source, source = model.source,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,

View file

@ -32,9 +32,10 @@ fun GlobalSearchResultItem(
title: String, title: String,
subtitle: String, subtitle: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
Column { Column(modifier = modifier) {
Row( Row(
modifier = Modifier modifier = Modifier
.padding( .padding(

View file

@ -107,7 +107,7 @@ private fun CategoryContent(
key = { _, category -> "category-${category.id}" }, key = { _, category -> "category-${category.id}" },
) { index, category -> ) { index, category ->
CategoryListItem( CategoryListItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
category = category, category = category,
canMoveUp = index != 0, canMoveUp = index != 0,
canMoveDown = index != categories.lastIndex, canMoveDown = index != categories.lastIndex,

View file

@ -10,8 +10,7 @@ import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.shouldExpandFAB
import tachiyomi.presentation.core.util.isScrollingUp
@Composable @Composable
fun CategoryFloatingActionButton( fun CategoryFloatingActionButton(
@ -23,7 +22,7 @@ fun CategoryFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_add)) }, text = { Text(text = stringResource(MR.strings.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
onClick = onCreate, onClick = onCreate,
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(), expanded = lazyListState.shouldExpandFAB(),
modifier = modifier, modifier = modifier,
) )
} }

View file

@ -113,14 +113,14 @@ private fun HistoryScreenContent(
when (item) { when (item) {
is HistoryUiModel.Header -> { is HistoryUiModel.Header -> {
ListGroupHeader( ListGroupHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
text = relativeDateText(item.date), text = relativeDateText(item.date),
) )
} }
is HistoryUiModel.Item -> { is HistoryUiModel.Item -> {
val value = item.item val value = item.item
HistoryItem( HistoryItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
history = value, history = value,
onClickCover = { onClickCover(value) }, onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) }, onClickResume = { onClickResume(value) },

View file

@ -75,8 +75,7 @@ import tachiyomi.presentation.core.components.material.ExtendedFloatingActionBut
import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.shouldExpandFAB
import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import java.time.Instant import java.time.Instant
@ -346,7 +345,7 @@ private fun MangaScreenSmallImpl(
}, },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading, onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), expanded = chapterListState.shouldExpandFAB(),
) )
} }
}, },
@ -594,7 +593,7 @@ fun MangaScreenLargeImpl(
}, },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading, onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), expanded = chapterListState.shouldExpandFAB(),
) )
} }
}, },

View file

@ -42,7 +42,7 @@ import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
@ -649,7 +649,7 @@ private fun TagsChip(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
SuggestionChip( SuggestionChip(
modifier = modifier, modifier = modifier,
onClick = onClick, onClick = onClick,

View file

@ -31,8 +31,6 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@Composable @Composable
fun ScanlatorFilterDialog( fun ScanlatorFilterDialog(
@ -96,8 +94,8 @@ fun ScanlatorFilterDialog(
} }
} }
} }
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
} }
}, },
properties = DialogProperties( properties = DialogProperties(

View file

@ -28,11 +28,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR import tachiyomi.i18n.MR

View file

@ -46,7 +46,7 @@ fun ExtensionReposContent(
repos.forEach { repos.forEach {
item { item {
ExtensionRepoListItem( ExtensionRepoListItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
repo = it, repo = it,
onOpenWebsite = { onOpenWebsite(it) }, onOpenWebsite = { onOpenWebsite(it) },
onDelete = { onClickDelete(it.baseUrl) }, onDelete = { onClickDelete(it.baseUrl) },

View file

@ -26,8 +26,6 @@ import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@Composable @Composable
fun <T> ListPreferenceWidget( fun <T> ListPreferenceWidget(
@ -69,8 +67,8 @@ fun <T> ListPreferenceWidget(
} }
} }
} }
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
} }
}, },
confirmButton = { confirmButton = {

View file

@ -30,8 +30,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
private enum class State { private enum class State {
CHECKED, INVERSED, UNCHECKED CHECKED, INVERSED, UNCHECKED
@ -115,16 +113,8 @@ fun <T> TriStateListDialog(
} }
} }
if (!listState.isScrolledToStart()) { if (listState.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
HorizontalDivider( if (listState.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
modifier = Modifier.align(Alignment.TopCenter),
)
}
if (!listState.isScrolledToEnd()) {
HorizontalDivider(
modifier = Modifier.align(Alignment.BottomCenter),
)
}
} }
} }
}, },

View file

@ -43,8 +43,6 @@ import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@Composable @Composable
fun TrackStatusSelector( fun TrackStatusSelector(
@ -86,8 +84,8 @@ fun TrackStatusSelector(
} }
} }
} }
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
}, },
onConfirm = onConfirm, onConfirm = onConfirm,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,

View file

@ -54,7 +54,7 @@ internal fun LazyListScope.updatesLastUpdatedItem(
item(key = "updates-lastUpdated") { item(key = "updates-lastUpdated") {
Box( Box(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItem()
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
) { ) {
Text( Text(
@ -91,14 +91,14 @@ internal fun LazyListScope.updatesUiItems(
when (item) { when (item) {
is UpdatesUiModel.Header -> { is UpdatesUiModel.Header -> {
ListGroupHeader( ListGroupHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
text = relativeDateText(item.date), text = relativeDateText(item.date),
) )
} }
is UpdatesUiModel.Item -> { is UpdatesUiModel.Item -> {
val updatesItem = item.item val updatesItem = item.item
UpdatesUiItem( UpdatesUiItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
update = updatesItem.update, update = updatesItem.update,
selected = updatesItem.selected, selected = updatesItem.selected,
readProgress = updatesItem.update.lastPageRead readProgress = updatesItem.update.lastPageRead

View file

@ -7,9 +7,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
@Composable @Composable
fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean { fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean {

View file

@ -1,7 +1,6 @@
[versions] [versions]
compiler = "1.5.12" compiler = "1.5.12"
# 2024.04.00-alpha01 has several bugs with the new animateItem() modifier compose-bom = "2024.05.00-alpha01"
compose-bom = "2024.03.00-alpha02"
accompanist = "0.35.0-alpha" accompanist = "0.35.0-alpha"
[libraries] [libraries]

View file

@ -153,7 +153,9 @@ fun AdaptiveSheet(
if (enableSwipeDismiss) { if (enableSwipeDismiss) {
Modifier.nestedScroll( Modifier.nestedScroll(
remember(anchoredDraggableState) { remember(anchoredDraggableState) {
anchoredDraggableState.preUpPostDownNestedScrollConnection() anchoredDraggableState.preUpPostDownNestedScrollConnection(
onFling = { scope.launch { anchoredDraggableState.settle(it) } }
)
}, },
) )
} else { } else {
@ -201,11 +203,12 @@ fun AdaptiveSheet(
} }
} }
private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection() = private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection(
object : NestedScrollConnection { onFling: (velocity: Float) -> Unit
) = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat() val delta = available.toFloat()
return if (delta < 0 && source == NestedScrollSource.Drag) { return if (delta < 0 && source == NestedScrollSource.UserInput) {
dispatchRawDelta(delta).toOffset() dispatchRawDelta(delta).toOffset()
} else { } else {
Offset.Zero Offset.Zero
@ -217,7 +220,7 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection()
available: Offset, available: Offset,
source: NestedScrollSource, source: NestedScrollSource,
): Offset { ): Offset {
return if (source == NestedScrollSource.Drag) { return if (source == NestedScrollSource.UserInput) {
dispatchRawDelta(available.toFloat()).toOffset() dispatchRawDelta(available.toFloat()).toOffset()
} else { } else {
Offset.Zero Offset.Zero
@ -227,7 +230,7 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection()
override suspend fun onPreFling(available: Velocity): Velocity { override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = available.toFloat() val toFling = available.toFloat()
return if (toFling < 0 && offset > anchors.minAnchor()) { return if (toFling < 0 && offset > anchors.minAnchor()) {
settle(toFling) onFling(toFling)
// since we go to the anchor with tween settling, consume all for the best UX // since we go to the anchor with tween settling, consume all for the best UX
available available
} else { } else {
@ -236,13 +239,8 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection()
} }
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
val toFling = available.toFloat() onFling(available.toFloat())
return if (toFling > 0) { return available
settle(toFling)
available
} else {
Velocity.Zero
}
} }
private fun Float.toOffset(): Offset = Offset(0f, this) private fun Float.toOffset(): Offset = Offset(0f, this)

View file

@ -1,34 +1,16 @@
package tachiyomi.presentation.core.components.material package tachiyomi.presentation.core.components.material
import androidx.compose.animation.core.animate
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier 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 androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.pow
/** /**
* @param refreshing Whether the layout is currently refreshing * @param refreshing Whether the layout is currently refreshing
@ -46,242 +28,26 @@ fun PullRefresh(
indicatorPadding: PaddingValues = PaddingValues(0.dp), indicatorPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val state = rememberPullToRefreshState( val state = rememberPullToRefreshState()
Box(
modifier = modifier
.pullToRefresh(
isRefreshing = refreshing, isRefreshing = refreshing,
extraVerticalOffset = indicatorPadding.calculateTopPadding(), state = state,
enabled = enabled, enabled = enabled,
onRefresh = onRefresh, onRefresh = onRefresh,
) )
) {
Box(modifier.nestedScroll(state.nestedScrollConnection)) {
content() content()
val contentPadding = remember(indicatorPadding) { PullToRefreshDefaults.Indicator(
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 modifier = Modifier
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
.padding(contentPadding), .padding(indicatorPadding),
isRefreshing = refreshing,
state = state,
containerColor = MaterialTheme.colorScheme.surfaceVariant, 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<PullToRefreshStateImpl, Boolean>(
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
}

View file

@ -3,63 +3,16 @@ package tachiyomi.presentation.core.util
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable @Composable
fun LazyListState.isScrolledToStart(): Boolean { fun LazyListState.shouldExpandFAB(): Boolean {
return remember { return remember {
derivedStateOf { derivedStateOf {
val firstItem = layoutInfo.visibleItemsInfo.firstOrNull() (firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0) ||
firstItem == null || firstItem.offset == layoutInfo.viewportStartOffset 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 .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
} }