mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-06 10:45:43 +08:00
Optimization for API 28, add back-page config
This commit is contained in:
@@ -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 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 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)
|
||||||
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@{
|
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)
|
|
||||||
|
|
||||||
clipPath(path)
|
|
||||||
}) {
|
}) {
|
||||||
this@result.drawContent()
|
this@result.drawShadow()
|
||||||
drawRect(Color.White.copy(alpha = 0.8f))
|
|
||||||
|
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 {
|
||||||
|
it.nativeCanvas.drawBitmap(bitmap, -2 * radius, -2 * radius, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 ->
|
||||||
@@ -52,38 +45,20 @@ fun Page() {
|
|||||||
)
|
)
|
||||||
.drawCurl(CurlConfig(), 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user