mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-06 02:35:25 +08:00
Add next / prev animations
This commit is contained in:
@@ -11,22 +11,20 @@ import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||
@ExperimentalPageCurlApi
|
||||
public data class PageCurlConfig(
|
||||
val curl: CurlConfig = CurlConfig(),
|
||||
val interaction: InteractionConfig = InteractionConfig()
|
||||
val direction: PageCurlDirection = PageCurlDirection.StartToEnd,
|
||||
val interaction: InteractionConfig = InteractionConfig(forward = direction.forward()),
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public data class InteractionConfig(
|
||||
val forward: CurlDirection = CurlDirection(
|
||||
Rect(Offset(0.5f, 0.0f), Offset(1.0f, 1.0f)),
|
||||
Rect(Offset(0.0f, 0.0f), Offset(0.5f, 1.0f)),
|
||||
),
|
||||
val backward: CurlDirection = CurlDirection(forward.end, forward.start),
|
||||
val forward: DragDirection,
|
||||
val backward: DragDirection = DragDirection(forward.end, forward.start),
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public data class CurlDirection(
|
||||
public data class DragDirection(
|
||||
val start: Rect,
|
||||
val end: Rect
|
||||
val end: Rect,
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
@@ -48,3 +46,20 @@ public data class ShadowConfig(
|
||||
val radius: Dp = 15.dp,
|
||||
val offset: DpOffset = DpOffset((-5).dp, 0.dp),
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public enum class PageCurlDirection {
|
||||
StartToEnd,
|
||||
// TODO (Alex) Add support for reversed end-to-start direction
|
||||
// EndToStart,
|
||||
}
|
||||
|
||||
private fun left(): Rect = Rect(Offset(0.0f, 0.0f), Offset(0.5f, 1.0f))
|
||||
|
||||
private fun right(): Rect = Rect(Offset(0.5f, 0.0f), Offset(1.0f, 1.0f))
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
private fun PageCurlDirection.forward(): DragDirection =
|
||||
when (this) {
|
||||
PageCurlDirection.StartToEnd -> DragDirection(right(), left())
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package eu.wewox.pagecurl.page
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector4D
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.animation.core.calculateTargetValue
|
||||
import androidx.compose.animation.splineBasedDecay
|
||||
@@ -13,15 +15,60 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||
import eu.wewox.pagecurl.config.CurlDirection
|
||||
import eu.wewox.pagecurl.config.DragDirection
|
||||
import eu.wewox.pagecurl.utils.rotate
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.PI
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
internal fun Modifier.curlGesture(
|
||||
state: PageCurlState.InternalState,
|
||||
enabled: Boolean,
|
||||
scope: CoroutineScope,
|
||||
direction: DragDirection,
|
||||
start: Edge,
|
||||
end: Edge,
|
||||
edge: Animatable<Edge, AnimationVector4D>,
|
||||
onChange: () -> Unit,
|
||||
): Modifier =
|
||||
curlGesture(
|
||||
key = state,
|
||||
enabled = enabled,
|
||||
direction = direction,
|
||||
onStart = {
|
||||
scope.launch {
|
||||
state.animateJob?.cancel()
|
||||
edge.snapTo(start)
|
||||
}
|
||||
},
|
||||
onCurl = { a, b ->
|
||||
scope.launch {
|
||||
edge.animateTo(Edge(a, b))
|
||||
}
|
||||
},
|
||||
onEnd = {
|
||||
scope.launch {
|
||||
try {
|
||||
edge.animateTo(end)
|
||||
} finally {
|
||||
onChange()
|
||||
edge.snapTo(start)
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
scope.launch {
|
||||
edge.animateTo(start)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
internal fun Modifier.curlGesture(
|
||||
key: Any?,
|
||||
enabled: Boolean,
|
||||
direction: CurlDirection,
|
||||
direction: DragDirection,
|
||||
onStart: () -> Unit,
|
||||
onCurl: (Offset, Offset) -> Unit,
|
||||
onEnd: () -> Unit,
|
||||
|
@@ -1,27 +1,14 @@
|
||||
package eu.wewox.pagecurl.page
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector4D
|
||||
import androidx.compose.animation.core.TwoWayConverter
|
||||
import androidx.compose.animation.core.VisibilityThreshold
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||
import eu.wewox.pagecurl.config.CurlDirection
|
||||
import eu.wewox.pagecurl.config.PageCurlConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
@Composable
|
||||
@@ -42,23 +29,23 @@ public fun PageCurl(
|
||||
Box(
|
||||
Modifier
|
||||
.curlGesture(
|
||||
key = internalState,
|
||||
state = internalState,
|
||||
enabled = updatedCurrent < state.max - 1,
|
||||
scope = scope,
|
||||
direction = config.interaction.forward,
|
||||
start = internalState.rightCurl,
|
||||
end = internalState.leftCurl,
|
||||
animatable = internalState.forward,
|
||||
start = internalState.rightEdge,
|
||||
end = internalState.leftEdge,
|
||||
edge = internalState.forward,
|
||||
onChange = { state.current = updatedCurrent + 1 }
|
||||
)
|
||||
.curlGesture(
|
||||
key = internalState,
|
||||
state = internalState,
|
||||
enabled = updatedCurrent > 0,
|
||||
scope = scope,
|
||||
direction = config.interaction.backward,
|
||||
start = internalState.leftCurl,
|
||||
end = internalState.rightCurl,
|
||||
animatable = internalState.backward,
|
||||
start = internalState.leftEdge,
|
||||
end = internalState.rightEdge,
|
||||
edge = internalState.backward,
|
||||
onChange = { state.current = updatedCurrent - 1 }
|
||||
)
|
||||
) {
|
||||
@@ -67,141 +54,16 @@ public fun PageCurl(
|
||||
}
|
||||
|
||||
if (updatedCurrent < state.max) {
|
||||
Box(Modifier.drawCurl(config.curl, internalState.forward.value.a, internalState.forward.value.b)) {
|
||||
Box(Modifier.drawCurl(config.curl, internalState.forward.value.top, internalState.forward.value.bottom)) {
|
||||
content(updatedCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCurrent > 0) {
|
||||
Box(Modifier.drawCurl(config.curl, internalState.backward.value.a, internalState.backward.value.b)) {
|
||||
Box(Modifier.drawCurl(config.curl, internalState.backward.value.top, internalState.backward.value.bottom)) {
|
||||
content(updatedCurrent - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
@Composable
|
||||
public fun rememberPageCurlState(
|
||||
max: Int,
|
||||
initialCurrent: Int = 0,
|
||||
): PageCurlState =
|
||||
rememberSaveable(
|
||||
max, initialCurrent,
|
||||
saver = Saver(
|
||||
save = { it.current },
|
||||
restore = {
|
||||
PageCurlState(
|
||||
max = it,
|
||||
initialCurrent = initialCurrent
|
||||
)
|
||||
}
|
||||
)
|
||||
) {
|
||||
PageCurlState(
|
||||
max = max,
|
||||
initialCurrent = initialCurrent
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public class PageCurlState(
|
||||
public val max: Int,
|
||||
initialCurrent: Int = 0,
|
||||
) {
|
||||
public var current: Int by mutableStateOf(initialCurrent)
|
||||
internal set
|
||||
|
||||
internal var internalState: InternalState? by mutableStateOf(null)
|
||||
|
||||
internal fun setup(constraints: Constraints) {
|
||||
if (internalState?.constraints == constraints) {
|
||||
return
|
||||
}
|
||||
|
||||
val maxWidthPx = constraints.maxWidth.toFloat()
|
||||
val maxHeightPx = constraints.maxHeight.toFloat()
|
||||
|
||||
val left = Curl(Offset(0f, 0f), Offset(0f, maxHeightPx))
|
||||
val right = Curl(Offset(maxWidthPx, 0f), Offset(maxWidthPx, maxHeightPx))
|
||||
|
||||
val forward = Animatable(right, Curl.VectorConverter, Curl.VisibilityThreshold)
|
||||
val backward = Animatable(left, Curl.VectorConverter, Curl.VisibilityThreshold)
|
||||
|
||||
internalState = InternalState(constraints, left, right, forward, backward)
|
||||
}
|
||||
|
||||
public suspend fun snapTo(value: Int) {
|
||||
internalState?.reset()
|
||||
current = value
|
||||
}
|
||||
|
||||
internal data class InternalState(
|
||||
val constraints: Constraints,
|
||||
val leftCurl: Curl,
|
||||
val rightCurl: Curl,
|
||||
val forward: Animatable<Curl, AnimationVector4D>,
|
||||
val backward: Animatable<Curl, AnimationVector4D>,
|
||||
) {
|
||||
internal suspend fun reset() {
|
||||
forward.snapTo(rightCurl)
|
||||
backward.snapTo(leftCurl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
private fun Modifier.curlGesture(
|
||||
key: Any?,
|
||||
enabled: Boolean,
|
||||
scope: CoroutineScope,
|
||||
direction: CurlDirection,
|
||||
start: Curl,
|
||||
end: Curl,
|
||||
animatable: Animatable<Curl, AnimationVector4D>,
|
||||
onChange: () -> Unit,
|
||||
): Modifier =
|
||||
curlGesture(
|
||||
key = key,
|
||||
enabled = enabled,
|
||||
direction = direction,
|
||||
onStart = {
|
||||
scope.launch {
|
||||
animatable.snapTo(start)
|
||||
}
|
||||
},
|
||||
onCurl = { a, b ->
|
||||
scope.launch {
|
||||
animatable.animateTo(Curl(a, b))
|
||||
}
|
||||
},
|
||||
onEnd = {
|
||||
scope.launch {
|
||||
try {
|
||||
animatable.animateTo(end)
|
||||
} finally {
|
||||
onChange()
|
||||
animatable.snapTo(start)
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
scope.launch {
|
||||
animatable.animateTo(start)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
internal data class Curl(val a: Offset, val b: Offset) {
|
||||
companion object {
|
||||
val VectorConverter: TwoWayConverter<Curl, AnimationVector4D> =
|
||||
TwoWayConverter(
|
||||
convertToVector = { AnimationVector4D(it.a.x, it.a.y, it.b.x, it.b.y) },
|
||||
convertFromVector = { Curl(Offset(it.v1, it.v2), Offset(it.v3, it.v4)) }
|
||||
)
|
||||
|
||||
val VisibilityThreshold: Curl =
|
||||
Curl(Offset.VisibilityThreshold, Offset.VisibilityThreshold)
|
||||
}
|
||||
}
|
||||
|
181
pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/PageCurlState.kt
Normal file
181
pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/PageCurlState.kt
Normal file
@@ -0,0 +1,181 @@
|
||||
package eu.wewox.pagecurl.page
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector4D
|
||||
import androidx.compose.animation.core.TwoWayConverter
|
||||
import androidx.compose.animation.core.VisibilityThreshold
|
||||
import androidx.compose.animation.core.keyframes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
@Composable
|
||||
public fun rememberPageCurlState(
|
||||
max: Int,
|
||||
initialCurrent: Int = 0,
|
||||
): PageCurlState =
|
||||
rememberSaveable(
|
||||
max, initialCurrent,
|
||||
saver = Saver(
|
||||
save = { it.current },
|
||||
restore = {
|
||||
PageCurlState(
|
||||
max = it,
|
||||
initialCurrent = initialCurrent
|
||||
)
|
||||
}
|
||||
)
|
||||
) {
|
||||
PageCurlState(
|
||||
max = max,
|
||||
initialCurrent = initialCurrent
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public class PageCurlState(
|
||||
public val max: Int,
|
||||
initialCurrent: Int = 0,
|
||||
) {
|
||||
public var current: Int by mutableStateOf(initialCurrent)
|
||||
internal set
|
||||
|
||||
internal var internalState: InternalState? by mutableStateOf(null)
|
||||
|
||||
internal fun setup(constraints: Constraints) {
|
||||
if (internalState?.constraints == constraints) {
|
||||
return
|
||||
}
|
||||
|
||||
val maxWidthPx = constraints.maxWidth.toFloat()
|
||||
val maxHeightPx = constraints.maxHeight.toFloat()
|
||||
|
||||
val left = Edge(Offset(0f, 0f), Offset(0f, maxHeightPx))
|
||||
val right = Edge(Offset(maxWidthPx, 0f), Offset(maxWidthPx, maxHeightPx))
|
||||
|
||||
val forward = Animatable(right, Edge.VectorConverter, Edge.VisibilityThreshold)
|
||||
val backward = Animatable(left, Edge.VectorConverter, Edge.VisibilityThreshold)
|
||||
|
||||
internalState = InternalState(constraints, left, right, forward, backward)
|
||||
}
|
||||
|
||||
public suspend fun snapTo(value: Int) {
|
||||
current = value.coerceIn(0, max - 1)
|
||||
internalState?.reset()
|
||||
}
|
||||
|
||||
public suspend fun next(block: suspend Animatable<Edge, AnimationVector4D>.(Size) -> Unit = DefaultNext) {
|
||||
internalState?.animateTo(
|
||||
target = { current + 1 },
|
||||
animate = { forward.block(it) }
|
||||
)
|
||||
}
|
||||
|
||||
public suspend fun prev(block: suspend Animatable<Edge, AnimationVector4D>.(Size) -> Unit = DefaultPrev) {
|
||||
internalState?.animateTo(
|
||||
target = { current - 1 },
|
||||
animate = { backward.block(it) }
|
||||
)
|
||||
}
|
||||
|
||||
internal inner class InternalState(
|
||||
val constraints: Constraints,
|
||||
val leftEdge: Edge,
|
||||
val rightEdge: Edge,
|
||||
val forward: Animatable<Edge, AnimationVector4D>,
|
||||
val backward: Animatable<Edge, AnimationVector4D>,
|
||||
) {
|
||||
|
||||
var animateJob: Job? = null
|
||||
|
||||
suspend fun reset() {
|
||||
forward.snapTo(rightEdge)
|
||||
backward.snapTo(leftEdge)
|
||||
}
|
||||
|
||||
suspend fun animateTo(
|
||||
target: () -> Int,
|
||||
animate: suspend InternalState.(Size) -> Unit
|
||||
) {
|
||||
animateJob?.cancel()
|
||||
|
||||
val targetIndex = target()
|
||||
if (targetIndex < 0 || targetIndex >= max) {
|
||||
return
|
||||
}
|
||||
|
||||
coroutineScope {
|
||||
animateJob = launch {
|
||||
try {
|
||||
reset()
|
||||
animate(Size(constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat()))
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
snapTo(target())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public data class Edge(val top: Offset, val bottom: Offset) {
|
||||
internal companion object {
|
||||
val VectorConverter: TwoWayConverter<Edge, AnimationVector4D> =
|
||||
TwoWayConverter(
|
||||
convertToVector = { AnimationVector4D(it.top.x, it.top.y, it.bottom.x, it.bottom.y) },
|
||||
convertFromVector = { Edge(Offset(it.v1, it.v2), Offset(it.v3, it.v4)) }
|
||||
)
|
||||
|
||||
val VisibilityThreshold: Edge =
|
||||
Edge(Offset.VisibilityThreshold, Offset.VisibilityThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
private val DefaultNext: suspend Animatable<Edge, AnimationVector4D>.(Size) -> Unit = { size ->
|
||||
animateTo(
|
||||
targetValue = size.start,
|
||||
animationSpec = keyframes {
|
||||
durationMillis = DefaultAnimDuration
|
||||
size.end at 0
|
||||
size.middle at DefaultMidPointDuration
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val DefaultPrev: suspend Animatable<Edge, AnimationVector4D>.(Size) -> Unit = { size ->
|
||||
animateTo(
|
||||
targetValue = size.end,
|
||||
animationSpec = keyframes {
|
||||
durationMillis = DefaultAnimDuration
|
||||
size.start at 0
|
||||
size.middle at DefaultAnimDuration - DefaultMidPointDuration
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private const val DefaultAnimDuration: Int = 450
|
||||
private const val DefaultMidPointDuration: Int = 150
|
||||
|
||||
private val Size.start: Edge
|
||||
get() = Edge(Offset(0f, 0f), Offset(0f, height))
|
||||
|
||||
private val Size.middle: Edge
|
||||
get() = Edge(Offset(width, height / 2f), Offset(width / 2f, height))
|
||||
|
||||
private val Size.end: Edge
|
||||
get() = Edge(Offset(width, height), Offset(width, height))
|
Reference in New Issue
Block a user