Optimization for API 28, add back-page config

This commit is contained in:
Oleksandr Balan
2022-04-08 21:54:41 +02:00
parent 9ed51e9081
commit c0faeb5cfd
3 changed files with 141 additions and 119 deletions

View File

@@ -2,6 +2,7 @@ package eu.wewox.pagecurl.page
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.os.Build
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
@@ -18,21 +19,28 @@ import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.wewox.pagecurl.utils.Polygon import eu.wewox.pagecurl.utils.Polygon
import eu.wewox.pagecurl.utils.lineLineIntersection import eu.wewox.pagecurl.utils.lineLineIntersection
import eu.wewox.pagecurl.utils.rotate
import java.lang.Float.max import java.lang.Float.max
import kotlin.math.atan2 import kotlin.math.atan2
data class CurlConfig( data class CurlConfig(
val backPage: BackPageConfig = BackPageConfig(),
val shadow: ShadowConfig = ShadowConfig() val shadow: ShadowConfig = ShadowConfig()
) { ) {
data class BackPageConfig(
val color: Color = Color.White,
val contentAlpha: Float = 0.1f,
)
data class ShadowConfig( data class ShadowConfig(
val color: Color = Color.Black, val color: Color = Color.Black,
val alpha: Float = 0.2f, val alpha: Float = 0.2f,
val radius: Dp = 40.dp, val radius: Dp = 15.dp,
val offsetX: Dp = 0.dp, val offset: DpOffset = DpOffset((-5).dp, 0.dp),
val offsetY: Dp = 0.dp,
) )
} }
@@ -65,14 +73,12 @@ fun Modifier.drawCurl(
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 clippedContent = prepareClippedContent(topCurlOffset, bottomCurlOffset) val drawClippedContent = prepareClippedContent(topCurlOffset, bottomCurlOffset)
val shadowBelowCurl = prepareShadowBelowCurl(config.shadow, topCurlOffset, bottomCurlOffset) val drawCurl = prepareCurl(config, topCurlOffset, bottomCurlOffset)
val curl = prepareCurl(topCurlOffset, bottomCurlOffset)
onDrawWithContent { onDrawWithContent {
clippedContent() drawClippedContent()
shadowBelowCurl() drawCurl()
curl()
} }
} }
@@ -91,96 +97,129 @@ private fun CacheDrawScope.prepareClippedContent(
} }
} }
private fun CacheDrawScope.prepareShadowBelowCurl( private fun CacheDrawScope.prepareCurl(
shadow: CurlConfig.ShadowConfig, config: CurlConfig,
topCurlOffset: Offset, topCurlOffset: Offset,
bottomCurlOffset: Offset, bottomCurlOffset: Offset,
): ContentDrawScope.() -> Unit { ): ContentDrawScope.() -> Unit {
val shadowColor = shadow.color.copy(alpha = shadow.alpha).toArgb() val polygon = Polygon(
val transparent = shadow.color.copy(alpha = 0f).toArgb()
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 { sequence {
suspend fun SequenceScope<Offset>.yieldEndSideInterception() {
val offset = lineLineIntersection(
topCurlOffset, bottomCurlOffset,
Offset(size.width, 0f), Offset(size.width, size.height)
) ?: return
yield(offset)
yield(offset)
}
if (topCurlOffset.x < size.width) { if (topCurlOffset.x < size.width) {
yield(topCurlOffset) yield(topCurlOffset)
yield(Offset(size.width, topCurlOffset.y)) yield(Offset(size.width, topCurlOffset.y))
} else { } else {
val a = lineLineIntersection(topCurlOffset, bottomCurlOffset, Offset(size.width, 0f), Offset(size.width, size.height))!! yieldEndSideInterception()
yield(a)
yield(a)
} }
if (bottomCurlOffset.x < size.width) { if (bottomCurlOffset.x < size.width) {
yield(Offset(size.width, size.height)) yield(Offset(size.width, size.height))
yield(bottomCurlOffset) yield(bottomCurlOffset)
} else { } else {
val a = lineLineIntersection(topCurlOffset, bottomCurlOffset, Offset(size.width, 0f), Offset(size.width, size.height))!! yieldEndSideInterception()
yield(a)
yield(a)
} }
}.toList() }.toList()
).translate( )
Offset(2 * radius, 2 * radius)
).offset(
radius
).toPath()
canvas.drawPath(path.asAndroidPath(), frameworkPaint)
return {
val lineVector = topCurlOffset - bottomCurlOffset val lineVector = topCurlOffset - bottomCurlOffset
val angle = atan2(-lineVector.y, lineVector.x) val angle = Math.PI.toFloat() + atan2(-lineVector.y, lineVector.x) * 2
val drawShadow = prepareShadow(config, polygon, angle)
return result@{
withTransform({ withTransform({
scale(-1f, 1f, pivot = bottomCurlOffset) scale(-1f, 1f, pivot = bottomCurlOffset)
rotateRad(angle, pivot = bottomCurlOffset)
rotateRad(Math.PI.toFloat() + 2 * angle, pivot = bottomCurlOffset)
}) { }) {
this@result.drawShadow()
clipPath(polygon.toPath()) {
this@result.drawContent()
val overlayAlpha = 1f - config.backPage.contentAlpha
drawRect(config.backPage.color.copy(alpha = overlayAlpha))
}
}
}
}
private fun CacheDrawScope.prepareShadow(
config: CurlConfig,
polygon: Polygon,
angle: Float
): ContentDrawScope.() -> Unit {
val shadow = config.shadow
if (shadow.alpha == 0f || shadow.radius == 0.dp) {
return { /* No shadow is requested */ }
}
val radius = shadow.radius.toPx()
val shadowColor = shadow.color.copy(alpha = shadow.alpha).toArgb()
val transparent = shadow.color.copy(alpha = 0f).toArgb()
val shadowOffset = Offset(-shadow.offset.x.toPx(), shadow.offset.y.toPx())
.rotate(2 * Math.PI.toFloat() - angle)
val paint = Paint().apply {
val frameworkPaint = asFrameworkPaint()
frameworkPaint.color = transparent
frameworkPaint.setShadowLayer(
shadow.radius.toPx(),
shadowOffset.x,
shadowOffset.y,
shadowColor
)
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
prepareShadowApi28(radius, paint, polygon)
} else {
prepareShadowImage(radius, paint, polygon)
}
}
private fun prepareShadowApi28(
radius: Float,
paint: Paint,
polygon: Polygon,
): ContentDrawScope.() -> Unit = {
drawIntoCanvas {
it.nativeCanvas.drawPath(
polygon
.offset(radius).toPath()
.asAndroidPath(),
paint.asFrameworkPaint()
)
}
}
private fun CacheDrawScope.prepareShadowImage(
radius: Float,
paint: Paint,
polygon: Polygon,
): ContentDrawScope.() -> Unit {
val bitmap = Bitmap.createBitmap(
(size.width + radius * 4).toInt(),
(size.height + radius * 4).toInt(),
Bitmap.Config.ARGB_8888
)
Canvas(bitmap).apply {
drawPath(
polygon
.translate(Offset(2 * radius, 2 * radius))
.offset(radius).toPath()
.asAndroidPath(),
paint.asFrameworkPaint()
)
}
return {
drawIntoCanvas { drawIntoCanvas {
it.nativeCanvas.drawBitmap(bitmap, -2 * radius, -2 * radius, null) 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,7 +1,5 @@
package eu.wewox.pagecurl.page package eu.wewox.pagecurl.page
import android.view.View
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
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -15,13 +13,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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.platform.ComposeView
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.utils.Data import eu.wewox.pagecurl.utils.Data
@Composable @Composable
@@ -38,7 +31,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) }
SoftwareLayerComposable( Box(
Modifier Modifier
.curlGesture( .curlGesture(
onCurl = { a, b -> onCurl = { a, b ->
@@ -69,21 +62,3 @@ fun Page() {
// ) // )
} }
} }
@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

@@ -2,8 +2,10 @@ package eu.wewox.pagecurl.utils
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import kotlin.math.cos
import kotlin.math.sin
data class Polygon(val vertices: List<Offset>) { internal data class Polygon(val vertices: List<Offset>) {
private val size: Int = vertices.size private val size: Int = vertices.size
@@ -46,6 +48,12 @@ private fun Offset.normalized(): Offset {
return if (distance != 0f) this / distance else this return if (distance != 0f) this / distance else this
} }
internal fun Offset.rotate(angle: Float): Offset {
val sin = sin(angle)
val cos = cos(angle)
return Offset(x * cos - y * sin, x * sin + y * cos)
}
internal fun lineLineIntersection( internal fun lineLineIntersection(
line1a: Offset, line1a: Offset,
line1b: Offset, line1b: Offset,