mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-05 18:25:20 +08:00
Add forward & backward
This commit is contained in:
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
46
app/src/main/java/eu/wewox/pagecurl/config/CurlConfig.kt
Normal file
46
app/src/main/java/eu/wewox/pagecurl/config/CurlConfig.kt
Normal 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),
|
||||
)
|
@@ -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()
|
||||
}
|
||||
|
||||
|
@@ -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),
|
||||
)
|
||||
|
@@ -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()
|
||||
// )
|
||||
}
|
||||
}
|
133
app/src/main/java/eu/wewox/pagecurl/page/PageCurl.kt
Normal file
133
app/src/main/java/eu/wewox/pagecurl/page/PageCurl.kt
Normal 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)
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user