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 },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
modifier = Modifier.animateItem(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource,

View file

@ -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) },

View file

@ -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) {

View file

@ -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 -> {

View file

@ -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) },

View file

@ -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,

View file

@ -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,

View file

@ -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(

View file

@ -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,

View file

@ -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,
)
}

View file

@ -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) },

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.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(),
)
}
},

View file

@ -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,

View file

@ -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(

View file

@ -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

View file

@ -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) },

View file

@ -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 <T> ListPreferenceWidget(
@ -69,8 +67,8 @@ fun <T> 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 = {

View file

@ -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 <T> 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))
}
}
},

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.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,

View file

@ -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

View file

@ -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 {

View file

@ -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]

View file

@ -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 <T> AnchoredDraggableState<T>.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 <T> AnchoredDraggableState<T>.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
}

View file

@ -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<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.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
}