mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-29 12:37:50 +02:00
Bump compose version
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
This commit is contained in:
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user