mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-28 20:17:51 +02:00
@ -1,10 +1,13 @@
|
||||
package tachiyomi.presentation.core.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
@ -23,16 +26,21 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
@ -41,14 +49,15 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.presentation.core.util.PredictiveBack
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
||||
|
||||
@Composable
|
||||
fun AdaptiveSheet(
|
||||
isTabletUi: Boolean,
|
||||
@ -75,6 +84,7 @@ fun AdaptiveSheet(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
enabled = true,
|
||||
interactionSource = null,
|
||||
indication = null,
|
||||
onClick = internalOnDismissRequest,
|
||||
@ -85,6 +95,11 @@ fun AdaptiveSheet(
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.predictiveBackAnimation(
|
||||
enabled = remember { derivedStateOf { alpha > 0f } }.value,
|
||||
transformOrigin = TransformOrigin.Center,
|
||||
onBack = internalOnDismissRequest,
|
||||
)
|
||||
.requiredWidthIn(max = 460.dp)
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
@ -97,7 +112,6 @@ fun AdaptiveSheet(
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
content = {
|
||||
BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest)
|
||||
content()
|
||||
},
|
||||
)
|
||||
@ -107,16 +121,14 @@ fun AdaptiveSheet(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
||||
val anchoredDraggableState = remember {
|
||||
AnchoredDraggableState(
|
||||
initialValue = 1,
|
||||
positionalThreshold = { with(density) { 56.dp.toPx() } },
|
||||
velocityThreshold = { with(density) { 125.dp.toPx() } },
|
||||
snapAnimationSpec = sheetAnimationSpec,
|
||||
decayAnimationSpec = decayAnimationSpec,
|
||||
)
|
||||
val anchoredDraggableState = rememberSaveable(saver = AnchoredDraggableState.Saver()) {
|
||||
AnchoredDraggableState(initialValue = 1)
|
||||
}
|
||||
val flingBehavior = AnchoredDraggableDefaults.flingBehavior(
|
||||
state = anchoredDraggableState,
|
||||
positionalThreshold = { _: Float -> with(density) { 56.dp.toPx() } },
|
||||
animationSpec = sheetAnimationSpec,
|
||||
)
|
||||
val internalOnDismissRequest = {
|
||||
if (anchoredDraggableState.settledValue == 0) {
|
||||
scope.launch { anchoredDraggableState.animateTo(1) }
|
||||
@ -141,6 +153,11 @@ fun AdaptiveSheet(
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.predictiveBackAnimation(
|
||||
enabled = anchoredDraggableState.targetValue == 0,
|
||||
transformOrigin = TransformOrigin(0.5f, 1f),
|
||||
onBack = internalOnDismissRequest,
|
||||
)
|
||||
.widthIn(max = 460.dp)
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
@ -151,9 +168,9 @@ fun AdaptiveSheet(
|
||||
if (enableSwipeDismiss) {
|
||||
Modifier.nestedScroll(
|
||||
remember(anchoredDraggableState) {
|
||||
anchoredDraggableState.preUpPostDownNestedScrollConnection(
|
||||
onFling = { scope.launch { anchoredDraggableState.settle(it) } },
|
||||
)
|
||||
anchoredDraggableState.preUpPostDownNestedScrollConnection {
|
||||
scope.launch { anchoredDraggableState.settle(sheetAnimationSpec) }
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
@ -174,16 +191,13 @@ fun AdaptiveSheet(
|
||||
state = anchoredDraggableState,
|
||||
orientation = Orientation.Vertical,
|
||||
enabled = enableSwipeDismiss,
|
||||
flingBehavior = flingBehavior,
|
||||
)
|
||||
.navigationBarsPadding()
|
||||
.statusBarsPadding(),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
content = {
|
||||
BackHandler(
|
||||
enabled = anchoredDraggableState.targetValue == 0,
|
||||
onBack = internalOnDismissRequest,
|
||||
)
|
||||
content()
|
||||
},
|
||||
)
|
||||
@ -238,7 +252,11 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection(
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
onFling(available.toFloat())
|
||||
return available
|
||||
return if (targetValue != settledValue) {
|
||||
available
|
||||
} else {
|
||||
Velocity.Zero
|
||||
}
|
||||
}
|
||||
|
||||
private fun Float.toOffset(): Offset = Offset(0f, this)
|
||||
@ -249,3 +267,37 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection(
|
||||
@JvmName("offsetToFloat")
|
||||
private fun Offset.toFloat(): Float = this.y
|
||||
}
|
||||
|
||||
private fun Modifier.predictiveBackAnimation(
|
||||
enabled: Boolean,
|
||||
transformOrigin: TransformOrigin,
|
||||
onBack: () -> Unit,
|
||||
) = composed {
|
||||
var scale by remember { mutableFloatStateOf(1f) }
|
||||
PredictiveBackHandler(enabled = enabled) { progress ->
|
||||
try {
|
||||
progress.collect { backEvent ->
|
||||
scale = lerp(1f, 0.85f, PredictiveBack.transform(backEvent.progress))
|
||||
}
|
||||
// Completion
|
||||
onBack()
|
||||
} catch (e: CancellationException) {
|
||||
// Cancellation
|
||||
} finally {
|
||||
animate(
|
||||
initialValue = scale,
|
||||
targetValue = 1f,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
||||
) { value, _ ->
|
||||
scale = value
|
||||
}
|
||||
}
|
||||
}
|
||||
Modifier.graphicsLayer {
|
||||
this.scaleX = scale
|
||||
this.scaleY = scale
|
||||
this.transformOrigin = transformOrigin
|
||||
}
|
||||
}
|
||||
|
||||
private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
||||
|
@ -0,0 +1,9 @@
|
||||
package tachiyomi.presentation.core.util
|
||||
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
|
||||
private val PredictiveBackEasing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
|
||||
|
||||
object PredictiveBack {
|
||||
fun transform(progress: Float): Float = PredictiveBackEasing.transform(progress)
|
||||
}
|
Reference in New Issue
Block a user