Explorar o código

perf: 减少默认封面与渐变边缘的绘制期分配

5151561 hai 3 meses
pai
achega
fccc518040

+ 1 - 1
app/src/main/java/io/legado/app/ui/main/homepage/modules/WaterfallModule.kt

@@ -148,7 +148,7 @@ fun WaterfallItem(
                     )
                 }
 
-                val kinds = book.getKindList()
+                val kinds = remember(book.wordCount, book.kind) { book.getKindList() }
                 if (kinds.isNotEmpty()) {
                     Spacer(modifier = Modifier.height(4.dp))
                     FlowRow(

+ 29 - 23
app/src/main/java/io/legado/app/ui/theme/FadingEdge.kt

@@ -6,9 +6,9 @@ import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.pager.PagerState
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.draw.drawWithCache
 import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
@@ -20,34 +20,40 @@ import androidx.compose.ui.unit.dp
 /**
  * Applies a horizontal fading edge effect to a scrollable container.
  *
+ * The alphas are passed as [State] and read inside the draw cache block, so a running
+ * fade animation invalidates the gradient without recomposing the caller.
+ *
  * @param leftAlpha  0f = fully faded, 1f = fully visible
  * @param rightAlpha 0f = fully faded, 1f = fully visible
  * @param gradientWidth width of the fade gradient on each side
  */
 fun Modifier.fadingEdge(
-    leftAlpha: Float,
-    rightAlpha: Float,
+    leftAlpha: State<Float>,
+    rightAlpha: State<Float>,
     gradientWidth: Dp = 24.dp
 ): Modifier = graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
-    .drawWithContent {
-        drawContent()
+    .drawWithCache {
         val width = size.width
+        if (width <= 0f) {
+            return@drawWithCache onDrawWithContent { drawContent() }
+        }
         val gradientWidthPx = gradientWidth.toPx()
         val leftStop = gradientWidthPx / width
         val rightStop = 1f - (gradientWidthPx / width)
-        drawRect(
-            brush = Brush.horizontalGradient(
-                colorStops = arrayOf(
-                    0f to Color.Black.copy(alpha = 1f - leftAlpha),
-                    leftStop to Color.Black,
-                    rightStop to Color.Black,
-                    1f to Color.Black.copy(alpha = 1f - rightAlpha)
-                ),
-                startX = 0f,
-                endX = width
+        val brush = Brush.horizontalGradient(
+            colorStops = arrayOf(
+                0f to Color.Black.copy(alpha = 1f - leftAlpha.value),
+                leftStop to Color.Black,
+                rightStop to Color.Black,
+                1f to Color.Black.copy(alpha = 1f - rightAlpha.value)
             ),
-            blendMode = BlendMode.DstIn
+            startX = 0f,
+            endX = width
         )
+        onDrawWithContent {
+            drawContent()
+            drawRect(brush = brush, blendMode = BlendMode.DstIn)
+        }
     }
 
 /**
@@ -58,12 +64,12 @@ fun Modifier.fadingEdge(
     listState: LazyListState,
     gradientWidth: Dp = 24.dp
 ): Modifier {
-    val leftAlpha by animateFloatAsState(
+    val leftAlpha = animateFloatAsState(
         targetValue = if (listState.canScrollBackward) 1f else 0f,
         animationSpec = tween(300),
         label = "LeftFadeAlpha"
     )
-    val rightAlpha by animateFloatAsState(
+    val rightAlpha = animateFloatAsState(
         targetValue = if (listState.canScrollForward) 1f else 0f,
         animationSpec = tween(300),
         label = "RightFadeAlpha"
@@ -79,12 +85,12 @@ fun Modifier.fadingEdge(
     pagerState: PagerState,
     gradientWidth: Dp = 24.dp
 ): Modifier {
-    val leftAlpha by animateFloatAsState(
+    val leftAlpha = animateFloatAsState(
         targetValue = if (pagerState.canScrollBackward) 1f else 0f,
         animationSpec = tween(300),
         label = "LeftFadeAlpha"
     )
-    val rightAlpha by animateFloatAsState(
+    val rightAlpha = animateFloatAsState(
         targetValue = if (pagerState.canScrollForward) 1f else 0f,
         animationSpec = tween(300),
         label = "RightFadeAlpha"
@@ -100,12 +106,12 @@ fun Modifier.fadingEdge(
     scrollState: ScrollState,
     gradientWidth: Dp = 24.dp
 ): Modifier {
-    val leftAlpha by animateFloatAsState(
+    val leftAlpha = animateFloatAsState(
         targetValue = if (scrollState.canScrollBackward) 1f else 0f,
         animationSpec = tween(300),
         label = "LeftFadeAlpha"
     )
-    val rightAlpha by animateFloatAsState(
+    val rightAlpha = animateFloatAsState(
         targetValue = if (scrollState.canScrollForward) 1f else 0f,
         animationSpec = tween(300),
         label = "RightFadeAlpha"

+ 8 - 9
app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt

@@ -5,6 +5,7 @@ import androidx.compose.animation.ExperimentalSharedTransitionApi
 import androidx.compose.animation.SharedTransitionScope
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.IntrinsicSize
@@ -16,9 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Check
@@ -171,17 +170,17 @@ fun SearchBookListItem(
                 )
             }
 
-            val kinds = book.getKindList()
+            val kinds = remember(book.wordCount, book.kind) { book.getKindList() }
             if (kinds.isNotEmpty()) {
                 Spacer(modifier = Modifier.height(4.dp))
-                val lazyListState = rememberLazyListState()
-                LazyRow(
-                    state = lazyListState,
+                val scrollState = rememberScrollState()
+                Row(
                     modifier = Modifier
                         .fillMaxWidth()
-                        .fadingEdge(lazyListState, gradientWidth = 8.dp)
+                        .fadingEdge(scrollState, gradientWidth = 8.dp)
+                        .horizontalScroll(scrollState)
                 ) {
-                    items(kinds) { kind ->
+                    kinds.forEach { kind ->
                         SearchBookTagChip(text = kind)
                         Spacer(modifier = Modifier.width(6.dp))
                     }

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookPreviewSheet.kt

@@ -138,7 +138,7 @@ fun SearchBookPreviewSheet(
                 }
             }
 
-            val kinds = book.getKindList()
+            val kinds = remember(book.wordCount, book.kind) { book.getKindList() }
             if (kinds.isNotEmpty()) {
                 Spacer(modifier = Modifier.height(12.dp))
                 val lazyListState = rememberLazyListState()

+ 139 - 85
app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt

@@ -11,9 +11,9 @@ import androidx.compose.animation.EnterExitState
 import androidx.compose.animation.ExperimentalSharedTransitionApi
 import androidx.compose.animation.SharedTransitionScope
 import androidx.compose.animation.core.animateFloat
-import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.width
@@ -32,6 +32,7 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithCache
 import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
@@ -386,113 +387,166 @@ private fun CoverTextOverlay(
     // If text contains Latin letters, force horizontal layout
     val isHorizontal = configIsHorizontal || isLatinBasedText(name)
 
-    Canvas(modifier = Modifier.fillMaxSize()) {
-        val viewWidth = size.width
-        val viewHeight = size.height
-
-        drawIntoCanvas { canvas ->
-            val nativeCanvas = canvas.nativeCanvas
-
-            if (showName && !name.isNullOrBlank()) {
-                val paint = Paint().apply {
-                    isAntiAlias = true
-                    textAlign = Paint.Align.CENTER
-                    typeface = Typeface.DEFAULT_BOLD
-                    textSize = viewWidth / 8f
-                    color = textColor
-                    if (coverSettings.showShadow) {
-                        setShadowLayer(4f, 2f, 2f, shadowColor)
-                    }
+    // Paints, StaticLayout and per-character positions are built in the cache block so they are
+    // rebuilt only when the size or the settings above change, not on every draw pass.
+    Spacer(
+        modifier = Modifier
+            .fillMaxSize()
+            .drawWithCache {
+                val viewWidth = size.width
+                val viewHeight = size.height
+                if (viewWidth <= 0f || viewHeight <= 0f) {
+                    return@drawWithCache onDrawBehind { }
                 }
 
-                if (isHorizontal) {
-                    val maxWidth = (viewWidth * 0.8f).toInt()
-                    val textPaint = TextPaint(paint).apply { textAlign = Paint.Align.LEFT }
-                    val layout = StaticLayout.Builder
-                        .obtain(name, 0, name.length, textPaint, maxWidth)
+                val namePaint = if (showName && !name.isNullOrBlank()) {
+                    Paint().apply {
+                        isAntiAlias = true
+                        textAlign = Paint.Align.CENTER
+                        typeface = Typeface.DEFAULT_BOLD
+                        textSize = viewWidth / 8f
+                        color = textColor
+                        if (coverSettings.showShadow) {
+                            setShadowLayer(4f, 2f, 2f, shadowColor)
+                        }
+                    }
+                } else null
+
+                val nameMaxWidth = (viewWidth * 0.8f).toInt().coerceAtLeast(1)
+                val nameTextPaint = if (namePaint != null && isHorizontal) {
+                    TextPaint(namePaint).apply { textAlign = Paint.Align.LEFT }
+                } else null
+                val nameLayout = if (nameTextPaint != null && name != null) {
+                    StaticLayout.Builder
+                        .obtain(name, 0, name.length, nameTextPaint, nameMaxWidth)
                         .setAlignment(Layout.Alignment.ALIGN_CENTER)
                         .setMaxLines(3)
                         .setEllipsize(TextUtils.TruncateAt.END)
                         .build()
+                } else null
+                val nameLayoutX = (viewWidth - nameMaxWidth) / 2f
+                val nameLayoutY = viewHeight * 0.08f
 
-                    nativeCanvas.withSave {
-                        val textX = (viewWidth - maxWidth) / 2f
-                        val textY = viewHeight * 0.08f
-                        translate(textX, textY)
-                        if (coverSettings.showStroke) {
-                            textPaint.style = Paint.Style.STROKE
-                            textPaint.strokeWidth = textPaint.textSize / 12
-                            val originalColor = textPaint.color
-                            textPaint.color = Color.White.toArgb()
-                            textPaint.clearShadowLayer()
-                            layout.draw(this)
-                            textPaint.style = Paint.Style.FILL
-                            textPaint.color = originalColor
-                            if (coverSettings.showShadow) {
-                                textPaint.setShadowLayer(4f, 2f, 2f, shadowColor)
-                            }
+                val nameStrokePaint =
+                    if (namePaint != null && !isHorizontal && coverSettings.showStroke) {
+                        Paint(namePaint).apply {
+                            color = Color.White.toArgb()
+                            style = Paint.Style.STROKE
+                            strokeWidth = namePaint.textSize / 10
+                            clearShadowLayer()
                         }
-                        layout.draw(this)
-                    }
-                } else {
+                    } else null
+                val nameCharDraws = if (namePaint != null && name != null && !isHorizontal) {
+                    val charHeight = namePaint.fontMetrics.let { it.bottom - it.top }
                     var startX = viewWidth * 0.16f
                     var startY = viewHeight * 0.16f
-                    val fm = paint.fontMetrics
-                    val charHeight = fm.bottom - fm.top
-                    name.forEach { char ->
-                        if (coverSettings.showStroke) {
-                            val strokePaint = Paint(paint).apply {
-                                color = Color.White.toArgb()
-                                style = Paint.Style.STROKE
-                                strokeWidth = paint.textSize / 10
-                                clearShadowLayer()
-                            }
-                            nativeCanvas.drawText(char.toString(), startX, startY, strokePaint)
-                        }
-                        nativeCanvas.drawText(char.toString(), startX, startY, paint)
+                    name.map { char ->
+                        val draw = Triple(char.toString(), startX, startY)
                         startY += charHeight
                         if (startY > viewHeight * 0.8f) {
-                            startX += paint.textSize * 1.2f
+                            startX += namePaint.textSize * 1.2f
                             startY = viewHeight * 0.2f
                         }
+                        draw
                     }
-                }
-            }
-
-            if (showAuthor && !author.isNullOrBlank()) {
-                val paint = Paint().apply {
-                    isAntiAlias = true
-                    textAlign = Paint.Align.CENTER
-                    textSize = viewWidth / 12f
-                    color = textColor
-                    if (coverSettings.showShadow) {
-                        setShadowLayer(4f, 1f, 1f, shadowColor)
+                } else emptyList()
+
+                val authorPaint = if (showAuthor && !author.isNullOrBlank()) {
+                    Paint().apply {
+                        isAntiAlias = true
+                        textAlign = Paint.Align.CENTER
+                        textSize = viewWidth / 12f
+                        color = textColor
+                        if (coverSettings.showShadow) {
+                            setShadowLayer(4f, 1f, 1f, shadowColor)
+                        }
                     }
-                }
-                if (isHorizontal) {
-                    val authorText = TextUtils.ellipsize(author, TextPaint(paint), viewWidth * 0.9f, TextUtils.TruncateAt.END)
-                    if (coverSettings.showStroke) {
-                        val strokePaint = Paint(paint).apply {
+                } else null
+
+                val authorText = if (authorPaint != null && author != null && isHorizontal) {
+                    TextUtils.ellipsize(
+                        author,
+                        TextPaint(authorPaint),
+                        viewWidth * 0.9f,
+                        TextUtils.TruncateAt.END
+                    ).toString()
+                } else null
+                val authorStrokePaint =
+                    if (authorPaint != null && isHorizontal && coverSettings.showStroke) {
+                        Paint(authorPaint).apply {
                             color = Color.White.toArgb()
                             style = Paint.Style.STROKE
-                            strokeWidth = paint.textSize / 10
+                            strokeWidth = authorPaint.textSize / 10
                             clearShadowLayer()
                         }
-                        nativeCanvas.drawText(authorText.toString(), viewWidth / 2, viewHeight * 0.75f, strokePaint)
-                    }
-                    nativeCanvas.drawText(authorText.toString(), viewWidth / 2, viewHeight * 0.75f, paint)
-                } else {
+                    } else null
+                val authorCharDraws = if (authorPaint != null && author != null && !isHorizontal) {
+                    val charHeight = authorPaint.fontMetrics.let { it.bottom - it.top }
                     val startX = viewWidth * 0.84f
-                    val fm = paint.fontMetrics
-                    val charHeight = fm.bottom - fm.top
-                    var startY = viewHeight * 0.16f - (author.length * charHeight)
-                    startY = startY.coerceAtLeast(viewHeight * 0.2f)
-                    author.forEach { char ->
-                        nativeCanvas.drawText(char.toString(), startX, startY, paint)
+                    var startY = (viewHeight * 0.16f - (author.length * charHeight))
+                        .coerceAtLeast(viewHeight * 0.2f)
+                    author.map { char ->
+                        val draw = Triple(char.toString(), startX, startY)
                         startY += charHeight
+                        draw
+                    }
+                } else emptyList()
+
+                onDrawBehind {
+                    drawIntoCanvas { canvas ->
+                        val nativeCanvas = canvas.nativeCanvas
+
+                        if (nameLayout != null && nameTextPaint != null) {
+                            nativeCanvas.withSave {
+                                translate(nameLayoutX, nameLayoutY)
+                                if (coverSettings.showStroke) {
+                                    nameTextPaint.style = Paint.Style.STROKE
+                                    nameTextPaint.strokeWidth = nameTextPaint.textSize / 12
+                                    val originalColor = nameTextPaint.color
+                                    nameTextPaint.color = Color.White.toArgb()
+                                    nameTextPaint.clearShadowLayer()
+                                    nameLayout.draw(this)
+                                    nameTextPaint.style = Paint.Style.FILL
+                                    nameTextPaint.color = originalColor
+                                    if (coverSettings.showShadow) {
+                                        nameTextPaint.setShadowLayer(4f, 2f, 2f, shadowColor)
+                                    }
+                                }
+                                nameLayout.draw(this)
+                            }
+                        } else if (namePaint != null) {
+                            nameCharDraws.forEach { (text, x, y) ->
+                                if (nameStrokePaint != null) {
+                                    nativeCanvas.drawText(text, x, y, nameStrokePaint)
+                                }
+                                nativeCanvas.drawText(text, x, y, namePaint)
+                            }
+                        }
+
+                        if (authorPaint != null) {
+                            if (authorText != null) {
+                                if (authorStrokePaint != null) {
+                                    nativeCanvas.drawText(
+                                        authorText,
+                                        viewWidth / 2,
+                                        viewHeight * 0.75f,
+                                        authorStrokePaint
+                                    )
+                                }
+                                nativeCanvas.drawText(
+                                    authorText,
+                                    viewWidth / 2,
+                                    viewHeight * 0.75f,
+                                    authorPaint
+                                )
+                            } else {
+                                authorCharDraws.forEach { (text, x, y) ->
+                                    nativeCanvas.drawText(text, x, y, authorPaint)
+                                }
+                            }
+                        }
                     }
                 }
             }
-        }
-    }
+    )
 }