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

View File

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

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