Add shadow and config

This commit is contained in:
Oleksandr Balan
2022-04-07 18:11:29 +02:00
parent 47d4603dd5
commit 9ed51e9081
3 changed files with 245 additions and 54 deletions

View File

@@ -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))
}
}
}

View File

@@ -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,
)
}

View 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)
}