mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-07 11:10:02 +08:00
Add shadow and config
This commit is contained in:
@@ -1,23 +1,53 @@
|
|||||||
package eu.wewox.pagecurl.page
|
package eu.wewox.pagecurl.page
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.CacheDrawScope
|
||||||
|
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.graphics.Color
|
||||||
|
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.drawscope.ContentDrawScope
|
||||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||||
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
import androidx.compose.ui.graphics.drawscope.rotateRad
|
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.toArgb
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.wewox.pagecurl.utils.Polygon
|
||||||
|
import eu.wewox.pagecurl.utils.lineLineIntersection
|
||||||
import java.lang.Float.max
|
import java.lang.Float.max
|
||||||
import kotlin.math.atan2
|
import kotlin.math.atan2
|
||||||
|
|
||||||
|
data class CurlConfig(
|
||||||
|
val shadow: ShadowConfig = ShadowConfig()
|
||||||
|
) {
|
||||||
|
data class ShadowConfig(
|
||||||
|
val color: Color = Color.Black,
|
||||||
|
val alpha: Float = 0.2f,
|
||||||
|
val radius: Dp = 40.dp,
|
||||||
|
val offsetX: Dp = 0.dp,
|
||||||
|
val offsetY: Dp = 0.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun Modifier.drawCurl(
|
fun Modifier.drawCurl(
|
||||||
|
config: CurlConfig = CurlConfig(),
|
||||||
posA: Offset?,
|
posA: Offset?,
|
||||||
posB: Offset?,
|
posB: Offset?,
|
||||||
): Modifier = drawWithContent {
|
): Modifier = drawWithCache {
|
||||||
|
fun drawOnlyContent() =
|
||||||
|
onDrawWithContent {
|
||||||
|
drawContent()
|
||||||
|
}
|
||||||
|
|
||||||
if (posA == null || posB == null) {
|
if (posA == null || posB == null) {
|
||||||
drawContent()
|
return@drawWithCache drawOnlyContent()
|
||||||
return@drawWithContent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val topIntersection = lineLineIntersection(
|
val topIntersection = lineLineIntersection(
|
||||||
@@ -29,51 +59,128 @@ fun Modifier.drawCurl(
|
|||||||
posA, posB
|
posA, posB
|
||||||
)
|
)
|
||||||
if (topIntersection == null || bottomIntersection == null) {
|
if (topIntersection == null || bottomIntersection == null) {
|
||||||
drawContent()
|
return@drawWithCache drawOnlyContent()
|
||||||
return@drawWithContent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val topCurlOffset = Offset(max(0f, topIntersection.x), topIntersection.y)
|
val topCurlOffset = Offset(max(0f, topIntersection.x), topIntersection.y)
|
||||||
val bottomCurlOffset = Offset(max(0f, bottomIntersection.x), bottomIntersection.y)
|
val bottomCurlOffset = Offset(max(0f, bottomIntersection.x), bottomIntersection.y)
|
||||||
|
|
||||||
val lineVector = topCurlOffset - bottomCurlOffset
|
val clippedContent = prepareClippedContent(topCurlOffset, bottomCurlOffset)
|
||||||
|
val shadowBelowCurl = prepareShadowBelowCurl(config.shadow, topCurlOffset, bottomCurlOffset)
|
||||||
|
val curl = prepareCurl(topCurlOffset, bottomCurlOffset)
|
||||||
|
|
||||||
|
onDrawWithContent {
|
||||||
|
clippedContent()
|
||||||
|
shadowBelowCurl()
|
||||||
|
curl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CacheDrawScope.prepareClippedContent(
|
||||||
|
topCurlOffset: Offset,
|
||||||
|
bottomCurlOffset: Offset,
|
||||||
|
): ContentDrawScope.() -> Unit {
|
||||||
val path = Path()
|
val path = Path()
|
||||||
path.lineTo(topCurlOffset.x, topCurlOffset.y)
|
path.lineTo(topCurlOffset.x, topCurlOffset.y)
|
||||||
path.lineTo(bottomCurlOffset.x, bottomCurlOffset.y)
|
path.lineTo(bottomCurlOffset.x, bottomCurlOffset.y)
|
||||||
path.lineTo(0f, size.height)
|
path.lineTo(0f, size.height)
|
||||||
clipPath(path) { this@drawWithContent.drawContent() }
|
return result@{
|
||||||
|
clipPath(path) {
|
||||||
withTransform({
|
this@result.drawContent()
|
||||||
scale(-1f, 1f, pivot = bottomCurlOffset)
|
}
|
||||||
|
|
||||||
val angle = atan2(-lineVector.y, lineVector.x)
|
|
||||||
rotateRad(Math.PI.toFloat() + 2 * angle, pivot = bottomCurlOffset)
|
|
||||||
|
|
||||||
val path2 = Path()
|
|
||||||
path2.moveTo(topCurlOffset.x, topCurlOffset.y)
|
|
||||||
path2.lineTo(max(size.width, topCurlOffset.x), topCurlOffset.y)
|
|
||||||
path2.lineTo(max(size.width, bottomCurlOffset.x), bottomCurlOffset.y)
|
|
||||||
path2.lineTo(bottomCurlOffset.x, bottomCurlOffset.y)
|
|
||||||
clipPath(path2)
|
|
||||||
}) {
|
|
||||||
this@drawWithContent.drawContent()
|
|
||||||
drawRect(Color.White.copy(alpha = 0.8f))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun lineLineIntersection(
|
private fun CacheDrawScope.prepareShadowBelowCurl(
|
||||||
line1a: Offset,
|
shadow: CurlConfig.ShadowConfig,
|
||||||
line1b: Offset,
|
topCurlOffset: Offset,
|
||||||
line2a: Offset,
|
bottomCurlOffset: Offset,
|
||||||
line2b: Offset,
|
): ContentDrawScope.() -> Unit {
|
||||||
): Offset? {
|
val shadowColor = shadow.color.copy(alpha = shadow.alpha).toArgb()
|
||||||
val denominator = (line1a.x - line1b.x) * (line2a.y - line2b.y) - (line1a.y - line1b.y) * (line2a.x - line2b.x)
|
val transparent = shadow.color.copy(alpha = 0f).toArgb()
|
||||||
if (denominator == 0f) return null
|
|
||||||
|
|
||||||
val x = ((line1a.x * line1b.y - line1a.y * line1b.x) * (line2a.x - line2b.x) -
|
val paint = Paint()
|
||||||
(line1a.x - line1b.x) * (line2a.x * line2b.y - line2a.y * line2b.x)) / denominator
|
val frameworkPaint = paint.asFrameworkPaint()
|
||||||
val y = ((line1a.x * line1b.y - line1a.y * line1b.x) * (line2a.y - line2b.y) -
|
frameworkPaint.color = transparent
|
||||||
(line1a.y - line1b.y) * (line2a.x * line2b.y - line2a.y * line2b.x)) / denominator
|
|
||||||
return Offset(x, y)
|
val radius = shadow.radius.toPx()
|
||||||
|
frameworkPaint.setShadowLayer(
|
||||||
|
shadow.radius.toPx(),
|
||||||
|
shadow.offsetX.toPx(),
|
||||||
|
shadow.offsetY.toPx(),
|
||||||
|
shadowColor
|
||||||
|
)
|
||||||
|
|
||||||
|
val bitmap = Bitmap.createBitmap((size.width + radius * 4).toInt(), (size.height + radius * 4).toInt(), Bitmap.Config.ARGB_8888)
|
||||||
|
bitmap.eraseColor(Color.Transparent.toArgb())
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
|
||||||
|
val path = Polygon(
|
||||||
|
sequence {
|
||||||
|
if (topCurlOffset.x < size.width) {
|
||||||
|
yield(topCurlOffset)
|
||||||
|
yield(Offset(size.width, topCurlOffset.y))
|
||||||
|
} else {
|
||||||
|
val a = lineLineIntersection(topCurlOffset, bottomCurlOffset, Offset(size.width, 0f), Offset(size.width, size.height))!!
|
||||||
|
yield(a)
|
||||||
|
yield(a)
|
||||||
|
}
|
||||||
|
if (bottomCurlOffset.x < size.width) {
|
||||||
|
yield(Offset(size.width, size.height))
|
||||||
|
yield(bottomCurlOffset)
|
||||||
|
} else {
|
||||||
|
val a = lineLineIntersection(topCurlOffset, bottomCurlOffset, Offset(size.width, 0f), Offset(size.width, size.height))!!
|
||||||
|
yield(a)
|
||||||
|
yield(a)
|
||||||
|
}
|
||||||
|
}.toList()
|
||||||
|
).translate(
|
||||||
|
Offset(2 * radius, 2 * radius)
|
||||||
|
).offset(
|
||||||
|
radius
|
||||||
|
).toPath()
|
||||||
|
|
||||||
|
canvas.drawPath(path.asAndroidPath(), frameworkPaint)
|
||||||
|
|
||||||
|
return {
|
||||||
|
val lineVector = topCurlOffset - bottomCurlOffset
|
||||||
|
val angle = atan2(-lineVector.y, lineVector.x)
|
||||||
|
|
||||||
|
withTransform({
|
||||||
|
scale(-1f, 1f, pivot = bottomCurlOffset)
|
||||||
|
|
||||||
|
rotateRad(Math.PI.toFloat() + 2 * angle, pivot = bottomCurlOffset)
|
||||||
|
}) {
|
||||||
|
drawIntoCanvas {
|
||||||
|
it.nativeCanvas.drawBitmap(bitmap, -2 * radius, -2 * radius, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CacheDrawScope.prepareCurl(
|
||||||
|
topCurlOffset: Offset,
|
||||||
|
bottomCurlOffset: Offset,
|
||||||
|
): ContentDrawScope.() -> Unit {
|
||||||
|
val lineVector = topCurlOffset - bottomCurlOffset
|
||||||
|
val angle = atan2(-lineVector.y, lineVector.x)
|
||||||
|
|
||||||
|
val path = Path()
|
||||||
|
path.moveTo(topCurlOffset.x, topCurlOffset.y)
|
||||||
|
path.lineTo(max(size.width, topCurlOffset.x), topCurlOffset.y)
|
||||||
|
path.lineTo(max(size.width, bottomCurlOffset.x), bottomCurlOffset.y)
|
||||||
|
path.lineTo(bottomCurlOffset.x, bottomCurlOffset.y)
|
||||||
|
|
||||||
|
return result@{
|
||||||
|
withTransform({
|
||||||
|
scale(-1f, 1f, pivot = bottomCurlOffset)
|
||||||
|
|
||||||
|
rotateRad(Math.PI.toFloat() + 2 * angle, pivot = bottomCurlOffset)
|
||||||
|
|
||||||
|
clipPath(path)
|
||||||
|
}) {
|
||||||
|
this@result.drawContent()
|
||||||
|
drawRect(Color.White.copy(alpha = 0.8f))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package eu.wewox.pagecurl.page
|
package eu.wewox.pagecurl.page
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -15,9 +16,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import eu.wewox.pagecurl.R
|
import eu.wewox.pagecurl.R
|
||||||
import eu.wewox.pagecurl.utils.Data
|
import eu.wewox.pagecurl.utils.Data
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ fun Page() {
|
|||||||
var posA by remember { mutableStateOf<Offset?>(null) }
|
var posA by remember { mutableStateOf<Offset?>(null) }
|
||||||
var posB by remember { mutableStateOf<Offset?>(null) }
|
var posB by remember { mutableStateOf<Offset?>(null) }
|
||||||
|
|
||||||
Box(
|
SoftwareLayerComposable(
|
||||||
Modifier
|
Modifier
|
||||||
.curlGesture(
|
.curlGesture(
|
||||||
onCurl = { a, b ->
|
onCurl = { a, b ->
|
||||||
@@ -47,22 +50,40 @@ fun Page() {
|
|||||||
posB = null
|
posB = null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.drawCurl(posA, posB)
|
.drawCurl(CurlConfig(), posA, posB)
|
||||||
) {
|
) {
|
||||||
// Text(
|
Text(
|
||||||
// text = Data.Lorem1,
|
text = Data.Lorem1,
|
||||||
// fontSize = 22.sp,
|
fontSize = 22.sp,
|
||||||
// modifier = Modifier
|
modifier = Modifier
|
||||||
// .fillMaxSize()
|
.fillMaxSize()
|
||||||
// .background(Color.White)
|
.background(Color.White)
|
||||||
// .padding(16.dp)
|
.padding(16.dp)
|
||||||
// )
|
)
|
||||||
Image(
|
// Image(
|
||||||
painter = painterResource(R.drawable.img_sleep),
|
// painter = painterResource(R.drawable.img_sleep),
|
||||||
contentDescription = null,
|
// contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
// contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.fillMaxSize()
|
// .fillMaxSize()
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SoftwareLayerComposable(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { context ->
|
||||||
|
ComposeView(context).apply {
|
||||||
|
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { composeView ->
|
||||||
|
composeView.setContent(content)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
63
app/src/main/java/eu/wewox/pagecurl/utils/MathUtils.kt
Normal file
63
app/src/main/java/eu/wewox/pagecurl/utils/MathUtils.kt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package eu.wewox.pagecurl.utils
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
|
||||||
|
data class Polygon(val vertices: List<Offset>) {
|
||||||
|
|
||||||
|
private val size: Int = vertices.size
|
||||||
|
|
||||||
|
fun translate(offset: Offset): Polygon =
|
||||||
|
Polygon(vertices.map { it + offset })
|
||||||
|
|
||||||
|
fun offset(value: Float): Polygon {
|
||||||
|
val edgeNormals = List(size) {
|
||||||
|
val edge = vertices[index(it + 1)] - vertices[index(it)]
|
||||||
|
Offset(edge.y, -edge.x).normalized()
|
||||||
|
}
|
||||||
|
|
||||||
|
val vertexNormals = List(size) {
|
||||||
|
(edgeNormals[index(it - 1)] + edgeNormals[index(it)]).normalized()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Polygon(
|
||||||
|
vertices.mapIndexed { index, vertex ->
|
||||||
|
vertex + vertexNormals[index] * value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toPath(): Path =
|
||||||
|
Path().apply {
|
||||||
|
vertices.forEachIndexed { index, vertex ->
|
||||||
|
if (index == 0) {
|
||||||
|
moveTo(vertex.x, vertex.y)
|
||||||
|
} else {
|
||||||
|
lineTo(vertex.x, vertex.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun index(i: Int) = ((i % size) + size) % size
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Offset.normalized(): Offset {
|
||||||
|
val distance = getDistance()
|
||||||
|
return if (distance != 0f) this / distance else this
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun lineLineIntersection(
|
||||||
|
line1a: Offset,
|
||||||
|
line1b: Offset,
|
||||||
|
line2a: Offset,
|
||||||
|
line2b: Offset,
|
||||||
|
): Offset? {
|
||||||
|
val denominator = (line1a.x - line1b.x) * (line2a.y - line2b.y) - (line1a.y - line1b.y) * (line2a.x - line2b.x)
|
||||||
|
if (denominator == 0f) return null
|
||||||
|
|
||||||
|
val x = ((line1a.x * line1b.y - line1a.y * line1b.x) * (line2a.x - line2b.x) -
|
||||||
|
(line1a.x - line1b.x) * (line2a.x * line2b.y - line2a.y * line2b.x)) / denominator
|
||||||
|
val y = ((line1a.x * line1b.y - line1a.y * line1b.x) * (line2a.y - line2b.y) -
|
||||||
|
(line1a.y - line1b.y) * (line2a.x * line2b.y - line2a.y * line2b.x)) / denominator
|
||||||
|
return Offset(x, y)
|
||||||
|
}
|
Reference in New Issue
Block a user