From 9ed51e90818d0fd12760fa2334dbf7e0173ac036 Mon Sep 17 00:00:00 2001 From: Oleksandr Balan Date: Thu, 7 Apr 2022 18:11:29 +0200 Subject: [PATCH] Add shadow and config --- .../java/eu/wewox/pagecurl/page/CurlDraw.kt | 181 ++++++++++++++---- .../main/java/eu/wewox/pagecurl/page/Page.kt | 55 ++++-- .../java/eu/wewox/pagecurl/utils/MathUtils.kt | 63 ++++++ 3 files changed, 245 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/eu/wewox/pagecurl/utils/MathUtils.kt diff --git a/app/src/main/java/eu/wewox/pagecurl/page/CurlDraw.kt b/app/src/main/java/eu/wewox/pagecurl/page/CurlDraw.kt index 1385369..6479584 100644 --- a/app/src/main/java/eu/wewox/pagecurl/page/CurlDraw.kt +++ b/app/src/main/java/eu/wewox/pagecurl/page/CurlDraw.kt @@ -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)) + } + } } diff --git a/app/src/main/java/eu/wewox/pagecurl/page/Page.kt b/app/src/main/java/eu/wewox/pagecurl/page/Page.kt index de637fd..d40d985 100644 --- a/app/src/main/java/eu/wewox/pagecurl/page/Page.kt +++ b/app/src/main/java/eu/wewox/pagecurl/page/Page.kt @@ -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(null) } var posB by remember { mutableStateOf(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, + ) +} diff --git a/app/src/main/java/eu/wewox/pagecurl/utils/MathUtils.kt b/app/src/main/java/eu/wewox/pagecurl/utils/MathUtils.kt new file mode 100644 index 0000000..46304e4 --- /dev/null +++ b/app/src/main/java/eu/wewox/pagecurl/utils/MathUtils.kt @@ -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) { + + 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) +}