mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-05 18:25:20 +08:00
Add shadow and config
This commit is contained in:
@@ -1,23 +1,53 @@
|
||||
package eu.wewox.pagecurl.page
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
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.drawIntoCanvas
|
||||
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.dp
|
||||
import eu.wewox.pagecurl.utils.Polygon
|
||||
import eu.wewox.pagecurl.utils.lineLineIntersection
|
||||
import java.lang.Float.max
|
||||
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(
|
||||
config: CurlConfig = CurlConfig(),
|
||||
posA: Offset?,
|
||||
posB: Offset?,
|
||||
): Modifier = drawWithContent {
|
||||
): Modifier = drawWithCache {
|
||||
fun drawOnlyContent() =
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
}
|
||||
|
||||
if (posA == null || posB == null) {
|
||||
drawContent()
|
||||
return@drawWithContent
|
||||
return@drawWithCache drawOnlyContent()
|
||||
}
|
||||
|
||||
val topIntersection = lineLineIntersection(
|
||||
@@ -29,51 +59,128 @@ fun Modifier.drawCurl(
|
||||
posA, posB
|
||||
)
|
||||
if (topIntersection == null || bottomIntersection == null) {
|
||||
drawContent()
|
||||
return@drawWithContent
|
||||
return@drawWithCache drawOnlyContent()
|
||||
}
|
||||
|
||||
val topCurlOffset = Offset(max(0f, topIntersection.x), topIntersection.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()
|
||||
path.lineTo(topCurlOffset.x, topCurlOffset.y)
|
||||
path.lineTo(bottomCurlOffset.x, bottomCurlOffset.y)
|
||||
path.lineTo(0f, size.height)
|
||||
clipPath(path) { this@drawWithContent.drawContent() }
|
||||
|
||||
withTransform({
|
||||
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))
|
||||
return result@{
|
||||
clipPath(path) {
|
||||
this@result.drawContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private 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
|
||||
private fun CacheDrawScope.prepareShadowBelowCurl(
|
||||
shadow: CurlConfig.ShadowConfig,
|
||||
topCurlOffset: Offset,
|
||||
bottomCurlOffset: Offset,
|
||||
): ContentDrawScope.() -> Unit {
|
||||
val shadowColor = shadow.color.copy(alpha = shadow.alpha).toArgb()
|
||||
val transparent = shadow.color.copy(alpha = 0f).toArgb()
|
||||
|
||||
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)
|
||||
val paint = Paint()
|
||||
val frameworkPaint = paint.asFrameworkPaint()
|
||||
frameworkPaint.color = transparent
|
||||
|
||||
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
|
||||
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import eu.wewox.pagecurl.R
|
||||
import eu.wewox.pagecurl.utils.Data
|
||||
|
||||
@@ -35,7 +38,7 @@ fun Page() {
|
||||
var posA by remember { mutableStateOf<Offset?>(null) }
|
||||
var posB by remember { mutableStateOf<Offset?>(null) }
|
||||
|
||||
Box(
|
||||
SoftwareLayerComposable(
|
||||
Modifier
|
||||
.curlGesture(
|
||||
onCurl = { a, b ->
|
||||
@@ -47,22 +50,40 @@ fun Page() {
|
||||
posB = null
|
||||
}
|
||||
)
|
||||
.drawCurl(posA, posB)
|
||||
.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()
|
||||
)
|
||||
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()
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
@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