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 android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent 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.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.foundation.layout.padding
import androidx.compose.material.Surface import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text 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.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.graphics.Color
import eu.wewox.pagecurl.page.Page 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.ui.theme.PageCurlTheme
import eu.wewox.pagecurl.utils.Data
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
PageCurlTheme { PageCurlTheme {
// A surface container using the 'background' color from the theme var current by remember { mutableStateOf(0) }
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { val count = 6
Page() 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.CacheDrawScope
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset 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.Paint
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.asAndroidPath 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.drawscope.withTransform
import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb 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 androidx.compose.ui.unit.dp
import eu.wewox.pagecurl.config.CurlConfig
import eu.wewox.pagecurl.utils.Polygon import eu.wewox.pagecurl.utils.Polygon
import eu.wewox.pagecurl.utils.lineLineIntersection import eu.wewox.pagecurl.utils.lineLineIntersection
import eu.wewox.pagecurl.utils.rotate import eu.wewox.pagecurl.utils.rotate
import java.lang.Float.max import java.lang.Float.max
import kotlin.math.atan2 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( fun Modifier.drawCurl(
config: CurlConfig = CurlConfig(), config: CurlConfig = CurlConfig(),
posA: Offset?, posA: Offset,
posB: Offset?, posB: Offset,
): Modifier = drawWithCache { ): Modifier = drawWithCache {
fun drawOnlyContent() = fun drawOnlyContent() =
onDrawWithContent { onDrawWithContent {
drawContent() 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() return@drawWithCache drawOnlyContent()
} }

View File

@@ -1,29 +1,62 @@
package eu.wewox.pagecurl.page package eu.wewox.pagecurl.page
import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset 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.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( fun Modifier.curlGesture(
enabled: Boolean,
direction: CurlDirection,
onStart: () -> Unit,
onCurl: (Offset, Offset) -> Unit, onCurl: (Offset, Offset) -> Unit,
onEnd: () -> Unit,
onCancel: () -> 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 { forEachGesture {
awaitPointerEventScope { awaitPointerEventScope {
awaitFirstDown(requireUnconsumed = false) val down = awaitFirstDown(requireUnconsumed = false)
do { if (!startRect.contains(down.position)) {
val event = awaitPointerEvent() return@awaitPointerEventScope
val canceled = event.changes.any { it.positionChangeConsumed() } }
val posA = event.changes.getOrNull(0)?.position
val posB = event.changes.getOrNull(1)?.position val dragStart = down.position.copy(x = size.width.toFloat())
if (posA != null && posB != null) {
onCurl(posA, posB) onStart()
}
} while (!canceled && event.changes.any { it.pressed }) var dragCurrent = dragStart
onCancel() 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 { buildscript {
ext { 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. }// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '7.1.1' apply false id 'com.android.application' version '7.1.1' apply false
id 'com.android.library' 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) { task clean(type: Delete) {