From 8495cbf1686247df4bdd8ad9cb1dc75f4f208af6 Mon Sep 17 00:00:00 2001 From: Oleksandr Balan Date: Thu, 30 Jun 2022 00:30:22 +0200 Subject: [PATCH] Add state and snapTo method --- .../kotlin/eu/wewox/pagecurl/MainActivity.kt | 108 +++++++-------- .../pagecurl/components/SettingsPopup.kt | 93 +++++++++++++ .../eu/wewox/pagecurl/config/CurlConfig.kt | 15 +-- .../eu/wewox/pagecurl/page/CurlGesture.kt | 8 +- .../kotlin/eu/wewox/pagecurl/page/PageCurl.kt | 123 ++++++++++++++---- 5 files changed, 260 insertions(+), 87 deletions(-) create mode 100644 demo/src/main/kotlin/eu/wewox/pagecurl/components/SettingsPopup.kt diff --git a/demo/src/main/kotlin/eu/wewox/pagecurl/MainActivity.kt b/demo/src/main/kotlin/eu/wewox/pagecurl/MainActivity.kt index 8060404..9abb1e3 100644 --- a/demo/src/main/kotlin/eu/wewox/pagecurl/MainActivity.kt +++ b/demo/src/main/kotlin/eu/wewox/pagecurl/MainActivity.kt @@ -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) + ) + } } } } diff --git a/demo/src/main/kotlin/eu/wewox/pagecurl/components/SettingsPopup.kt b/demo/src/main/kotlin/eu/wewox/pagecurl/components/SettingsPopup.kt new file mode 100644 index 0000000..33f6dc6 --- /dev/null +++ b/demo/src/main/kotlin/eu/wewox/pagecurl/components/SettingsPopup.kt @@ -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 + ) +} diff --git a/pagecurl/src/main/kotlin/eu/wewox/pagecurl/config/CurlConfig.kt b/pagecurl/src/main/kotlin/eu/wewox/pagecurl/config/CurlConfig.kt index c57fd49..c7fe1c6 100644 --- a/pagecurl/src/main/kotlin/eu/wewox/pagecurl/config/CurlConfig.kt +++ b/pagecurl/src/main/kotlin/eu/wewox/pagecurl/config/CurlConfig.kt @@ -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( diff --git a/pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/CurlGesture.kt b/pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/CurlGesture.kt index 28b0d74..1d8696c 100644 --- a/pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/CurlGesture.kt +++ b/pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/CurlGesture.kt @@ -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(this) val target = decay.calculateTargetValue( diff --git a/pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/PageCurl.kt b/pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/PageCurl.kt index f13c8ce..a7aa84b 100644 --- a/pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/PageCurl.kt +++ b/pagecurl/src/main/kotlin/eu/wewox/pagecurl/page/PageCurl.kt @@ -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, + val backward: Animatable, + ) { + 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 = TwoWayConverter(