Add forward & backward

This commit is contained in:
Oleksandr Balan
2022-04-26 17:38:51 +02:00
parent c0faeb5cfd
commit 1de8caee0f
7 changed files with 303 additions and 110 deletions

View File

@@ -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)
)
}
}
}
}

View File

@@ -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),
)

View File

@@ -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()
}

View File

@@ -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),
)

View File

@@ -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<Offset?>(null) }
var posB by remember { mutableStateOf<Offset?>(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()
// )
}
}

View File

@@ -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<Curl, AnimationVector4D>,
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<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)
}
}

View File

@@ -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) {