From 1de8caee0f9c30540e546a9f40b2bb208852cad6 Mon Sep 17 00:00:00 2001 From: Oleksandr Balan Date: Tue, 26 Apr 2022 17:38:51 +0200 Subject: [PATCH] Add forward & backward --- .../java/eu/wewox/pagecurl/MainActivity.kt | 75 ++++++++-- .../eu/wewox/pagecurl/config/CurlConfig.kt | 46 ++++++ .../java/eu/wewox/pagecurl/page/CurlDraw.kt | 32 ++--- .../eu/wewox/pagecurl/page/CurlGesture.kt | 59 ++++++-- .../main/java/eu/wewox/pagecurl/page/Page.kt | 64 --------- .../java/eu/wewox/pagecurl/page/PageCurl.kt | 133 ++++++++++++++++++ build.gradle | 4 +- 7 files changed, 303 insertions(+), 110 deletions(-) create mode 100644 app/src/main/java/eu/wewox/pagecurl/config/CurlConfig.kt delete mode 100644 app/src/main/java/eu/wewox/pagecurl/page/Page.kt create mode 100644 app/src/main/java/eu/wewox/pagecurl/page/PageCurl.kt diff --git a/app/src/main/java/eu/wewox/pagecurl/MainActivity.kt b/app/src/main/java/eu/wewox/pagecurl/MainActivity.kt index 24a2319..c17db36 100644 --- a/app/src/main/java/eu/wewox/pagecurl/MainActivity.kt +++ b/app/src/main/java/eu/wewox/pagecurl/MainActivity.kt @@ -3,24 +3,83 @@ package eu.wewox.pagecurl import android.os.Bundle import androidx.activity.ComponentActivity 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.fillMaxSize -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text -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.tooling.preview.Preview -import eu.wewox.pagecurl.page.Page +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.config.PageCurlConfig +import eu.wewox.pagecurl.page.PageCurl import eu.wewox.pagecurl.ui.theme.PageCurlTheme +import eu.wewox.pagecurl.utils.Data class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { PageCurlTheme { - // A surface container using the 'background' color from the theme - Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { - Page() + 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, + modifier = Modifier + .align(Alignment.BottomEnd) + .background(Color.Black, RoundedCornerShape(topStartPercent = 100)) + .padding(16.dp) + ) + } } } } diff --git a/app/src/main/java/eu/wewox/pagecurl/config/CurlConfig.kt b/app/src/main/java/eu/wewox/pagecurl/config/CurlConfig.kt new file mode 100644 index 0000000..d5e40d8 --- /dev/null +++ b/app/src/main/java/eu/wewox/pagecurl/config/CurlConfig.kt @@ -0,0 +1,46 @@ +package eu.wewox.pagecurl.config + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +data class PageCurlConfig( + val curl: CurlConfig = CurlConfig(), + val interaction: InteractionConfig = InteractionConfig() +) + +data class InteractionConfig( + val forward: CurlDirection.Forward = CurlDirection.Forward( + 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), +) + +sealed interface CurlDirection { + val start: Rect + val end: Rect + + data class Forward(override val start: Rect, override val end: Rect) : CurlDirection + data class Backward(override val start: Rect, override val end: Rect) : CurlDirection +} + +data class CurlConfig( + val backPage: BackPageConfig = BackPageConfig(), + val shadow: ShadowConfig = ShadowConfig(), +) + +data class BackPageConfig( + val color: Color = Color.White, + val contentAlpha: Float = 0.1f, +) + +data class ShadowConfig( + val color: Color = Color.Black, + val alpha: Float = 0.2f, + val radius: Dp = 15.dp, + val offset: DpOffset = DpOffset((-5).dp, 0.dp), +) diff --git a/app/src/main/java/eu/wewox/pagecurl/page/CurlDraw.kt b/app/src/main/java/eu/wewox/pagecurl/page/CurlDraw.kt index bdd4d11..b32da04 100644 --- a/app/src/main/java/eu/wewox/pagecurl/page/CurlDraw.kt +++ b/app/src/main/java/eu/wewox/pagecurl/page/CurlDraw.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asAndroidPath @@ -18,43 +18,29 @@ import androidx.compose.ui.graphics.drawscope.rotateRad import androidx.compose.ui.graphics.drawscope.withTransform import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import eu.wewox.pagecurl.config.CurlConfig import eu.wewox.pagecurl.utils.Polygon import eu.wewox.pagecurl.utils.lineLineIntersection import eu.wewox.pagecurl.utils.rotate import java.lang.Float.max import kotlin.math.atan2 -data class CurlConfig( - val backPage: BackPageConfig = BackPageConfig(), - val shadow: ShadowConfig = ShadowConfig() -) { - data class BackPageConfig( - val color: Color = Color.White, - val contentAlpha: Float = 0.1f, - ) - - data class ShadowConfig( - val color: Color = Color.Black, - val alpha: Float = 0.2f, - val radius: Dp = 15.dp, - val offset: DpOffset = DpOffset((-5).dp, 0.dp), - ) -} - fun Modifier.drawCurl( config: CurlConfig = CurlConfig(), - posA: Offset?, - posB: Offset?, + posA: Offset, + posB: Offset, ): Modifier = drawWithCache { fun drawOnlyContent() = onDrawWithContent { drawContent() } - if (posA == null || posB == null) { + if (posA == size.toRect().topLeft && posB == size.toRect().bottomLeft) { + return@drawWithCache onDrawWithContent { } + } + + if (posA == size.toRect().topRight && posB == size.toRect().bottomRight) { return@drawWithCache drawOnlyContent() } diff --git a/app/src/main/java/eu/wewox/pagecurl/page/CurlGesture.kt b/app/src/main/java/eu/wewox/pagecurl/page/CurlGesture.kt index 47cfd1d..11077f8 100644 --- a/app/src/main/java/eu/wewox/pagecurl/page/CurlGesture.kt +++ b/app/src/main/java/eu/wewox/pagecurl/page/CurlGesture.kt @@ -1,29 +1,62 @@ package eu.wewox.pagecurl.page import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.drag import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.input.pointer.consumeAllChanges import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.positionChangeConsumed +import androidx.compose.ui.unit.IntSize +import eu.wewox.pagecurl.config.CurlDirection +import eu.wewox.pagecurl.utils.rotate +import kotlin.math.PI fun Modifier.curlGesture( + enabled: Boolean, + direction: CurlDirection, + onStart: () -> Unit, onCurl: (Offset, Offset) -> Unit, + onEnd: () -> Unit, onCancel: () -> Unit, -): Modifier = pointerInput(true) { +): Modifier = pointerInput(enabled) { + if (!enabled) { + return@pointerInput + } + + val startRect by lazy { direction.start.multiply(size) } + val endRect by lazy { direction.end.multiply(size) } forEachGesture { awaitPointerEventScope { - awaitFirstDown(requireUnconsumed = false) - do { - val event = awaitPointerEvent() - val canceled = event.changes.any { it.positionChangeConsumed() } - val posA = event.changes.getOrNull(0)?.position - val posB = event.changes.getOrNull(1)?.position - if (posA != null && posB != null) { - onCurl(posA, posB) - } - } while (!canceled && event.changes.any { it.pressed }) - onCancel() + val down = awaitFirstDown(requireUnconsumed = false) + if (!startRect.contains(down.position)) { + return@awaitPointerEventScope + } + + val dragStart = down.position.copy(x = size.width.toFloat()) + + onStart() + + var dragCurrent = dragStart + drag(down.id) { change -> + dragCurrent = change.position + change.consumeAllChanges() + val vector = (dragStart - dragCurrent).rotate(PI.toFloat() / 2) + onCurl(dragCurrent - vector, dragCurrent + vector) + } + + if (endRect.contains(dragCurrent)) { + onEnd() + } else { + onCancel() + } } } } + +private fun Rect.multiply(size: IntSize): Rect = + Rect( + topLeft = Offset(size.width * left, size.height * top), + bottomRight = Offset(size.width * right, size.height * bottom), + ) diff --git a/app/src/main/java/eu/wewox/pagecurl/page/Page.kt b/app/src/main/java/eu/wewox/pagecurl/page/Page.kt deleted file mode 100644 index b84b9d2..0000000 --- a/app/src/main/java/eu/wewox/pagecurl/page/Page.kt +++ /dev/null @@ -1,64 +0,0 @@ -package eu.wewox.pagecurl.page - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -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.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import eu.wewox.pagecurl.utils.Data - -@Composable -fun Page() { - Text( - text = Data.Lorem2, - fontSize = 22.sp, - modifier = Modifier - .fillMaxSize() - .background(Color.White) - .padding(16.dp) - ) - - var posA by remember { mutableStateOf(null) } - var posB by remember { mutableStateOf(null) } - - Box( - Modifier - .curlGesture( - onCurl = { a, b -> - posA = a - posB = b - }, - onCancel = { - posA = null - posB = null - } - ) - .drawCurl(CurlConfig(), posA, posB) - ) { - Text( - text = Data.Lorem1, - fontSize = 22.sp, - modifier = Modifier - .fillMaxSize() - .background(Color.White) - .padding(16.dp) - ) - // Image( - // painter = painterResource(R.drawable.img_sleep), - // contentDescription = null, - // contentScale = ContentScale.Crop, - // modifier = Modifier - // .fillMaxSize() - // ) - } -} diff --git a/app/src/main/java/eu/wewox/pagecurl/page/PageCurl.kt b/app/src/main/java/eu/wewox/pagecurl/page/PageCurl.kt new file mode 100644 index 0000000..f5296aa --- /dev/null +++ b/app/src/main/java/eu/wewox/pagecurl/page/PageCurl.kt @@ -0,0 +1,133 @@ +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.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import eu.wewox.pagecurl.config.CurlDirection +import eu.wewox.pagecurl.config.PageCurlConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun PageCurl( + current: Int, + count: Int, + modifier: Modifier = Modifier, + onCurrentChange: (Int) -> Unit, + config: PageCurlConfig = PageCurlConfig(), + content: @Composable (Int) -> Unit +) { + val scope = rememberCoroutineScope() + val updatedCurrent by rememberUpdatedState(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)) + + val forward = remember { Animatable(right, Curl.VectorConverter, Curl.VisibilityThreshold) } + val backward = remember { Animatable(left, Curl.VectorConverter, Curl.VisibilityThreshold) } + + Box( + Modifier + .curlGesture( + enabled = updatedCurrent < count - 1, + scope = scope, + direction = config.interaction.forward, + start = right, + end = left, + animatable = forward, + onChange = { onCurrentChange(updatedCurrent + 1) } + ) + .curlGesture( + enabled = updatedCurrent > 0, + scope = scope, + direction = config.interaction.backward, + start = left, + end = right, + animatable = backward, + onChange = { onCurrentChange(updatedCurrent - 1) } + ) + ) { + if (updatedCurrent + 1 < count) { + content(updatedCurrent + 1) + } + + if (updatedCurrent < count) { + Box(Modifier.drawCurl(config.curl, forward.value.a, forward.value.b)) { + content(updatedCurrent) + } + } + + if (updatedCurrent > 0) { + Box(Modifier.drawCurl(config.curl, backward.value.a, backward.value.b)) { + content(updatedCurrent - 1) + } + } + } + } +} + +private fun Modifier.curlGesture( + enabled: Boolean, + scope: CoroutineScope, + direction: CurlDirection, + start: Curl, + end: Curl, + animatable: Animatable, + onChange: () -> Unit, +): Modifier = + curlGesture( + 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) + } + }, + ) + + +private data class Curl(val a: Offset, val b: Offset) { + companion object { + val VectorConverter: TwoWayConverter = + 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) + } +} diff --git a/build.gradle b/build.gradle index c770b6f..3ac42df 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ buildscript { ext { - compose_version = '1.0.1' + compose_version = '1.2.0-alpha07' } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id 'com.android.application' version '7.1.1' apply false id 'com.android.library' version '7.1.1' apply false - id 'org.jetbrains.kotlin.android' version '1.5.21' apply false + id 'org.jetbrains.kotlin.android' version '1.6.10' apply false } task clean(type: Delete) {