mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-05 18:25:20 +08:00
Add state and snapTo method
This commit is contained in:
@@ -8,78 +8,84 @@ import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.wewox.pagecurl.components.SettingsPopup
|
||||
import eu.wewox.pagecurl.config.PageCurlConfig
|
||||
import eu.wewox.pagecurl.page.PageCurl
|
||||
import eu.wewox.pagecurl.page.rememberPageCurlState
|
||||
import eu.wewox.pagecurl.ui.theme.PageCurlTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
PageCurlTheme {
|
||||
var current by remember { mutableStateOf(0) }
|
||||
val count = 6
|
||||
PageCurl(
|
||||
current = current,
|
||||
count = count,
|
||||
onCurrentChange = {
|
||||
current = it
|
||||
},
|
||||
config = PageCurlConfig(),
|
||||
) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
) {
|
||||
when (index) {
|
||||
0 -> {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.img_sleep),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
count - 1 -> {
|
||||
Text(
|
||||
text = "The End",
|
||||
fontSize = 34.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
Text(
|
||||
text = if (index % 2 == 1) Data.Lorem1 else Data.Lorem2,
|
||||
fontSize = 22.sp,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = index.toString(),
|
||||
color = Color.White,
|
||||
Column {
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = rememberPageCurlState(max = 6)
|
||||
|
||||
PageCurl(
|
||||
state = state,
|
||||
config = PageCurlConfig(),
|
||||
) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.background(Color.Black, RoundedCornerShape(topStartPercent = 100))
|
||||
.padding(16.dp)
|
||||
)
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
) {
|
||||
when (index) {
|
||||
0 -> {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.img_sleep),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
Text(
|
||||
text = if (index % 2 == 1) Data.Lorem1 else Data.Lorem2,
|
||||
fontSize = 22.sp,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = index.toString(),
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.background(Color.Black, RoundedCornerShape(topStartPercent = 100))
|
||||
.padding(16.dp)
|
||||
)
|
||||
|
||||
SettingsPopup(
|
||||
onSnapToFirst = {
|
||||
scope.launch {
|
||||
state.snapTo(0)
|
||||
}
|
||||
},
|
||||
onSnapToLast = {
|
||||
scope.launch {
|
||||
state.snapTo(state.max - 1)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,93 @@
|
||||
package eu.wewox.pagecurl.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Popup
|
||||
|
||||
@Composable
|
||||
internal fun SettingsPopup(
|
||||
onSnapToFirst: () -> Unit,
|
||||
onSnapToLast: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var showPopup by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(100.dp)
|
||||
.clickableWithoutRipple {
|
||||
showPopup = true
|
||||
}
|
||||
)
|
||||
|
||||
if (showPopup) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Popup(
|
||||
alignment = Alignment.Center,
|
||||
onDismissRequest = { showPopup = false }
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
backgroundColor = MaterialTheme.colors.primary,
|
||||
elevation = 16.dp
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onSnapToFirst()
|
||||
showPopup = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "Go to first",
|
||||
color = MaterialTheme.colors.onPrimary
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
onSnapToLast()
|
||||
showPopup = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "Go to last",
|
||||
color = MaterialTheme.colors.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.clickableWithoutRipple(onClick: () -> Unit) = composed {
|
||||
clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
@@ -16,21 +16,18 @@ public data class PageCurlConfig(
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public data class InteractionConfig(
|
||||
val forward: CurlDirection.Forward = CurlDirection.Forward(
|
||||
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.Backward = CurlDirection.Backward(forward.end, forward.start),
|
||||
val backward: CurlDirection = CurlDirection(forward.end, forward.start),
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public sealed interface CurlDirection {
|
||||
public val start: Rect
|
||||
public val end: Rect
|
||||
|
||||
public data class Forward(override val start: Rect, override val end: Rect) : CurlDirection
|
||||
public data class Backward(override val start: Rect, override val end: Rect) : CurlDirection
|
||||
}
|
||||
public data class CurlDirection(
|
||||
val start: Rect,
|
||||
val end: Rect
|
||||
)
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
public data class CurlConfig(
|
||||
|
@@ -19,13 +19,14 @@ import kotlin.math.PI
|
||||
|
||||
@ExperimentalPageCurlApi
|
||||
internal fun Modifier.curlGesture(
|
||||
key: Any?,
|
||||
enabled: Boolean,
|
||||
direction: CurlDirection,
|
||||
onStart: () -> Unit,
|
||||
onCurl: (Offset, Offset) -> Unit,
|
||||
onEnd: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
): Modifier = pointerInput(enabled) {
|
||||
): Modifier = pointerInput(enabled, key) {
|
||||
if (!enabled) {
|
||||
return@pointerInput
|
||||
}
|
||||
@@ -53,6 +54,11 @@ internal fun Modifier.curlGesture(
|
||||
onCurl(dragCurrent - vector, dragCurrent + vector)
|
||||
}
|
||||
|
||||
if (dragCurrent == dragStart) {
|
||||
onCancel()
|
||||
return@awaitPointerEventScope
|
||||
}
|
||||
|
||||
val velocity = velocityTracker.calculateVelocity()
|
||||
val decay = splineBasedDecay<Offset>(this)
|
||||
val target = decay.calculateTargetValue(
|
||||
|
@@ -8,11 +8,15 @@ 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.remember
|
||||
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
|
||||
@@ -22,58 +26,54 @@ import kotlinx.coroutines.launch
|
||||
@ExperimentalPageCurlApi
|
||||
@Composable
|
||||
public fun PageCurl(
|
||||
current: Int,
|
||||
count: Int,
|
||||
state: PageCurlState,
|
||||
modifier: Modifier = Modifier,
|
||||
onCurrentChange: (Int) -> Unit,
|
||||
config: PageCurlConfig = PageCurlConfig(),
|
||||
content: @Composable (Int) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val updatedCurrent by rememberUpdatedState(current)
|
||||
val updatedCurrent by rememberUpdatedState(state.current)
|
||||
|
||||
BoxWithConstraints(modifier) {
|
||||
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))
|
||||
state.setup(constraints)
|
||||
|
||||
val forward = remember { Animatable(right, Curl.VectorConverter, Curl.VisibilityThreshold) }
|
||||
val backward = remember { Animatable(left, Curl.VectorConverter, Curl.VisibilityThreshold) }
|
||||
val internalState by rememberUpdatedState(state.internalState ?: return@BoxWithConstraints)
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.curlGesture(
|
||||
enabled = updatedCurrent < count - 1,
|
||||
key = internalState,
|
||||
enabled = updatedCurrent < state.max - 1,
|
||||
scope = scope,
|
||||
direction = config.interaction.forward,
|
||||
start = right,
|
||||
end = left,
|
||||
animatable = forward,
|
||||
onChange = { onCurrentChange(updatedCurrent + 1) }
|
||||
start = internalState.rightCurl,
|
||||
end = internalState.leftCurl,
|
||||
animatable = internalState.forward,
|
||||
onChange = { state.current = updatedCurrent + 1 }
|
||||
)
|
||||
.curlGesture(
|
||||
key = internalState,
|
||||
enabled = updatedCurrent > 0,
|
||||
scope = scope,
|
||||
direction = config.interaction.backward,
|
||||
start = left,
|
||||
end = right,
|
||||
animatable = backward,
|
||||
onChange = { onCurrentChange(updatedCurrent - 1) }
|
||||
start = internalState.leftCurl,
|
||||
end = internalState.rightCurl,
|
||||
animatable = internalState.backward,
|
||||
onChange = { state.current = updatedCurrent - 1 }
|
||||
)
|
||||
) {
|
||||
if (updatedCurrent + 1 < count) {
|
||||
if (updatedCurrent + 1 < state.max) {
|
||||
content(updatedCurrent + 1)
|
||||
}
|
||||
|
||||
if (updatedCurrent < count) {
|
||||
Box(Modifier.drawCurl(config.curl, forward.value.a, forward.value.b)) {
|
||||
if (updatedCurrent < state.max) {
|
||||
Box(Modifier.drawCurl(config.curl, internalState.forward.value.a, internalState.forward.value.b)) {
|
||||
content(updatedCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCurrent > 0) {
|
||||
Box(Modifier.drawCurl(config.curl, backward.value.a, backward.value.b)) {
|
||||
Box(Modifier.drawCurl(config.curl, internalState.backward.value.a, internalState.backward.value.b)) {
|
||||
content(updatedCurrent - 1)
|
||||
}
|
||||
}
|
||||
@@ -81,8 +81,79 @@ public fun PageCurl(
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
@@ -92,6 +163,7 @@ private fun Modifier.curlGesture(
|
||||
onChange: () -> Unit,
|
||||
): Modifier =
|
||||
curlGesture(
|
||||
key = key,
|
||||
enabled = enabled,
|
||||
direction = direction,
|
||||
onStart = {
|
||||
@@ -121,8 +193,7 @@ private fun Modifier.curlGesture(
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
private data class Curl(val a: Offset, val b: Offset) {
|
||||
internal data class Curl(val a: Offset, val b: Offset) {
|
||||
companion object {
|
||||
val VectorConverter: TwoWayConverter<Curl, AnimationVector4D> =
|
||||
TwoWayConverter(
|
||||
|
Reference in New Issue
Block a user