Browse Source

fix(read): linePaint 改由 upStyle 重建;清掉 ChapterProvider 671 行注释死代码与两个死 import

三笔挂账一起清。

## linePaint 的 by lazy 定型(真 bug)

`val linePaint: Paint by lazy { Paint(contentPaint).apply { strokeWidth = ... } }`
只在首次取用时算一次:之后改下划线粗细、正文字体、字号,朗读高亮行与搜索命中行下面
那条线都不会跟着变。而 `upThemeColors()` 又会**就地**改它的 `color`——于是它「颜色跟得上、
其余永远是首帧那份」,看起来像生效了,最难归因。

改成与 `titlePaint`/`contentPaint` 同样由 `upStyle()` 重建,粗细取 `renderStyle.underlineHeight`
(`underlineHeight` 的 action 集带 `UpdateChapterStyle`,正好落在 `upStyle()` 上)。
`ChapterProvider.init { upStyle() }` 已存在,没有未初始化窗口。

新增护栏「绘制画笔不得用 by lazy 定型」,改回去会红,反证过。

顺带撞上 R1.4 的存量护栏 `ChapterProviderMetricsInvariantTest`「排版度量不以可变静态量
形式存在」:`linePaint` 从 `val by lazy` 变 `var` 就落进了它的扫描面。它的允许名单里本来
就有 `dashEffect`/`reviewPaint`——绘制期读取的 Paint 类对象,放进不可变快照没有意义。
`linePaint` 正是同类,加进名单并在注释里记下这段来历:**它以前正是靠 `by lazy` 躲过这条
断言的,代价就是这个 bug**。

## 671 行注释死代码

`ChapterProvider` 里两大块块注释——旧的 `getTextChapter` 与
`setTypeImage`/`setTypeText`/`addCharsToLine*` 家族,活逻辑早已搬进 `TextChapterLayout`。
两块中间夹着**活代码** `getTextChapterAsync`,所以分两段删。
ChapterProvider 1263 → 592 行。

删后逐个核过 import:没有任何一个是只被死代码引用的,全部保留。

## 两个死 import

`ReadView.kt` 与 `SimulationPageDelegate.kt` 的 `import ...ReadBookConfig`,都早于本次
改动就已无引用。

全量单测 449 全绿。

回归面很小,只有 linePaint 一处行为变化:朗读高亮行、搜索命中行下面那条线,在改下划线
粗细或更换正文字体后应当跟着变(此前不变)。删注释与清 import 是零行为变化。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5151561 3 months ago
parent
commit
a1bb4b2137

+ 0 - 1
app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt

@@ -16,7 +16,6 @@ import android.widget.FrameLayout
 import io.legado.app.BuildConfig
 import io.legado.app.BuildConfig
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.PageAnim
 import io.legado.app.constant.PageAnim
-import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.model.ReadSessionState
 import io.legado.app.model.ReadSessionState
 
 
 import io.legado.app.ui.book.read.page.api.DataSource
 import io.legado.app.ui.book.read.page.api.DataSource

+ 0 - 1
app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt

@@ -12,7 +12,6 @@ import android.graphics.Region
 import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.GradientDrawable
 import android.os.Build
 import android.os.Build
 import android.view.MotionEvent
 import android.view.MotionEvent
-import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.model.ReadSessionState
 import io.legado.app.model.ReadSessionState
 import io.legado.app.ui.book.read.page.ReadView
 import io.legado.app.ui.book.read.page.ReadView
 import io.legado.app.ui.book.read.page.entities.PageDirection
 import io.legado.app.ui.book.read.page.entities.PageDirection

+ 16 - 679
app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt

@@ -49,14 +49,16 @@ object ChapterProvider {
 
 
     const val indentChar = " "
     const val indentChar = " "
 
 
-    val linePaint: Paint by lazy {
-        Paint(contentPaint).apply {
-            clearShadowLayer()
-            isAntiAlias = true
-            strokeWidth = ReadBookConfig.underlineHeight.toFloat()
-            style = Paint.Style.STROKE
-        }
-    }
+    /**
+     * 朗读高亮 / 搜索命中那条线的画笔。
+     *
+     * 曾经是 `by lazy`:首次取用时按当时的 [contentPaint] 与 `underlineHeight` 定型,之后
+     * 再改下划线粗细、字体、字号都不会反映到这条线上——`upThemeColors` 只就地改了它的
+     * `color`,粗细和字形一直是首帧那份。现在与其它画笔一样由 [upStyle] 重建。
+     */
+    @Volatile
+    var linePaint: Paint = Paint()
+        private set
 
 
     var dashEffect = DashPathEffect(floatArrayOf(dottedBase, dottedRatio), 0f)
     var dashEffect = DashPathEffect(floatArrayOf(dottedBase, dottedRatio), 0f)
 
 
@@ -246,167 +248,6 @@ object ChapterProvider {
         upStyle()
         upStyle()
     }
     }
 
 
-    /*
-    /**
-     * 获取拆分完的章节数据
-     */
-    suspend fun getTextChapter(
-        book: Book,
-        bookChapter: BookChapter,
-        displayTitle: String,
-        bookContent: BookContent,
-        chapterSize: Int,
-    ): TextChapter {
-        val contents = bookContent.textList
-        val textPages = arrayListOf<TextPage>()
-        val stringBuilder = StringBuilder()
-        var absStartX = paddingLeft
-        var durY = 0f
-        textPages.add(TextPage())
-        if (ReadBookConfig.titleMode != 2 || bookChapter.isVolume) {
-            //标题非隐藏
-            displayTitle.splitNotBlank("\n").forEach { text ->
-                setTypeText(
-                    book, absStartX, durY,
-                    if (ReadConfig.enableReview) text + reviewChar else text,
-                    textPages,
-                    stringBuilder,
-                    titlePaint,
-                    titlePaintTextHeight,
-                    titlePaintFontMetrics,
-                    isTitle = true,
-                    emptyContent = contents.isEmpty(),
-                    isVolumeTitle = bookChapter.isVolume
-                ).let {
-                    absStartX = it.first
-                    durY = it.second
-                }
-            }
-            textPages.last().lines.last().isParagraphEnd = true
-            stringBuilder.append("\n")
-            durY += titleBottomSpacing
-        }
-        contents.forEach { content ->
-            if (book.getImageStyle().equals(Book.imgStyleText, true)) {
-                //图片样式为文字嵌入类型
-                var text = content.replace(srcReplaceChar, "▣")
-                val srcList = LinkedList<String>()
-                val sb = StringBuffer()
-                val matcher = AppPattern.imgPattern.matcher(text)
-                while (matcher.find()) {
-                    matcher.group(1)?.let { src ->
-                        srcList.add(src)
-                        matcher.appendReplacement(sb, srcReplaceChar)
-                    }
-                }
-                matcher.appendTail(sb)
-                text = sb.toString()
-                setTypeText(
-                    book,
-                    absStartX,
-                    durY,
-                    text,
-                    textPages,
-                    stringBuilder,
-                    contentPaint,
-                    contentPaintTextHeight,
-                    contentPaintFontMetrics,
-                    srcList = srcList
-                ).let {
-                    absStartX = it.first
-                    durY = it.second
-                }
-            } else {
-                val matcher = AppPattern.imgPattern.matcher(content)
-                var start = 0
-                while (matcher.find()) {
-                    val text = content.substring(start, matcher.start())
-                    if (text.isNotBlank()) {
-                        setTypeText(
-                            book,
-                            absStartX,
-                            durY,
-                            text,
-                            textPages,
-                            stringBuilder,
-                            contentPaint,
-                            contentPaintTextHeight,
-                            contentPaintFontMetrics
-                        ).let {
-                            absStartX = it.first
-                            durY = it.second
-                        }
-                    }
-                    setTypeImage(
-                        book,
-                        matcher.group(1)!!,
-                        absStartX,
-                        durY,
-                        textPages,
-                        contentPaintTextHeight,
-                        stringBuilder,
-                        book.getImageStyle()
-                    ).let {
-                        absStartX = it.first
-                        durY = it.second
-                    }
-                    start = matcher.end()
-                }
-                if (start < content.length) {
-                    val text = content.substring(start, content.length)
-                    if (text.isNotBlank()) {
-                        setTypeText(
-                            book, absStartX, durY,
-                            if (ReadConfig.enableReview) text + reviewChar else text,
-                            textPages,
-                            stringBuilder,
-                            contentPaint,
-                            contentPaintTextHeight,
-                            contentPaintFontMetrics
-                        ).let {
-                            absStartX = it.first
-                            durY = it.second
-                        }
-                    }
-                }
-            }
-            textPages.last().lines.last().isParagraphEnd = true
-            stringBuilder.append("\n")
-        }
-        val textPage = textPages.last()
-        val endPadding = 20.dpToPx()
-        val durYPadding = durY + endPadding
-        if (textPage.height < durYPadding) {
-            textPage.height = durYPadding
-        } else {
-            textPage.height += endPadding
-        }
-        textPage.text = stringBuilder.toString()
-        textPages.forEachIndexed { index, item ->
-            item.index = index
-            //item.pageSize = textPages.size
-            item.chapterIndex = bookChapter.index
-            item.chapterSize = chapterSize
-            item.title = displayTitle
-            item.doublePage = doublePage
-            item.paddingTop = paddingTop
-            item.upLinesPosition()
-        }
-
-        return TextChapter(
-            bookChapter,
-            bookChapter.index, displayTitle,
-            //textPages,
-            chapterSize,
-            bookContent.sameTitleRemoved,
-            bookChapter.isVip,
-            bookChapter.isPay,
-            bookContent.effectiveReplaceRules,
-            bookContent.effectiveContentProcesses,
-        )
-    }
-    */
-
     fun getTextChapterAsync(
     fun getTextChapterAsync(
         scope: CoroutineScope,
         scope: CoroutineScope,
         book: Book,
         book: Book,
@@ -432,516 +273,6 @@ object ChapterProvider {
         return textChapter
         return textChapter
     }
     }
 
 
-    /*
-    /**
-     * 排版图片
-     */
-    private suspend fun setTypeImage(
-        book: Book,
-        src: String,
-        x: Int,
-        y: Float,
-        textPages: ArrayList<TextPage>,
-        textHeight: Float,
-        stringBuilder: StringBuilder,
-        imageStyle: String?,
-    ): Pair<Int, Float> {
-        var absStartX = x
-        var durY = y
-        val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource)
-        if (size.width > 0 && size.height > 0) {
-            if (durY > visibleHeight) {
-                val textPage = textPages.last()
-                if (textPage.height < durY) {
-                    textPage.height = durY
-                }
-                textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
-                stringBuilder.clear()
-                textPages.add(TextPage())
-                durY = 0f
-            }
-            var height = size.height
-            var width = size.width
-            when (imageStyle?.uppercase(Locale.ROOT)) {
-                Book.imgStyleFull -> {
-                    width = visibleWidth
-                    height = size.height * visibleWidth / size.width
-                }
-
-                Book.imgStyleSingle -> {
-                    width = visibleWidth
-                    height = size.height * visibleWidth / size.width
-                    if (height > visibleHeight) {
-                        width = width * visibleHeight / height
-                        height = visibleHeight
-                    }
-                    if (durY > 0f) {
-                        val textPage = textPages.last()
-                        if (doublePage && absStartX < viewWidth / 2) {
-                            //当前页面左列结束
-                            textPage.leftLineSize = textPage.lineSize
-                            absStartX = viewWidth / 2 + paddingLeft
-                        } else {
-                            //当前页面结束
-                            if (textPage.leftLineSize == 0) {
-                                textPage.leftLineSize = textPage.lineSize
-                            }
-                            textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
-                            stringBuilder.clear()
-                            textPages.add(TextPage())
-                        }
-                        // 双页的 durY 不正确,可能会小于实际高度
-                        if (textPage.height < durY) {
-                            textPage.height = durY
-                        }
-                        durY = 0f
-                    }
-
-                    // 图片竖直方向居中:调整 Y 坐标
-                    if (height < visibleHeight) {
-                        val adjustHeight = (visibleHeight - height) / 2f
-                        durY = adjustHeight // 将 Y 坐标设置为居中位置
-                    }
-                }
-
-                else -> {
-                    if (size.width > visibleWidth) {
-                        height = size.height * visibleWidth / size.width
-                        width = visibleWidth
-                    }
-                    if (height > visibleHeight) {
-                        width = width * visibleHeight / height
-                        height = visibleHeight
-                    }
-                    if (durY + height > visibleHeight) {
-                        val textPage = textPages.last()
-                        if (doublePage && absStartX < viewWidth / 2) {
-                            //当前页面左列结束
-                            textPage.leftLineSize = textPage.lineSize
-                            absStartX = viewWidth / 2 + paddingLeft
-                        } else {
-                            //当前页面结束
-                            if (textPage.leftLineSize == 0) {
-                                textPage.leftLineSize = textPage.lineSize
-                            }
-                            textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
-                            stringBuilder.clear()
-                            textPages.add(TextPage())
-                        }
-                        // 双页的 durY 不正确,可能会小于实际高度
-                        if (textPage.height < durY) {
-                            textPage.height = durY
-                        }
-                        durY = 0f
-                    }
-                }
-            }
-            val textLine = TextLine(isImage = true)
-            textLine.lineTop = durY + paddingTop
-            durY += height
-            textLine.lineBottom = durY + paddingTop
-            val (start, end) = if (visibleWidth > width) {
-                val adjustWidth = (visibleWidth - width) / 2f
-                Pair(adjustWidth, adjustWidth + width)
-            } else {
-                Pair(0f, width.toFloat())
-            }
-            textLine.addColumn(
-                ImageColumn(start = x + start, end = x + end, src = src)
-            )
-            calcTextLinePosition(textPages, textLine, stringBuilder.length)
-            stringBuilder.append(" ") // 确保翻页时索引计算正确
-            textPages.last().addLine(textLine)
-        }
-        return absStartX to durY + textHeight * paragraphSpacing / 10f
-    }
-
-    /**
-     * 排版文字
-     */
-    private suspend fun setTypeText(
-        book: Book,
-        x: Int,
-        y: Float,
-        text: String,
-        textPages: ArrayList<TextPage>,
-        stringBuilder: StringBuilder,
-        textPaint: TextPaint,
-        textHeight: Float,
-        fontMetrics: FontMetrics,
-        isTitle: Boolean = false,
-        emptyContent: Boolean = false,
-        isVolumeTitle: Boolean = false,
-        srcList: LinkedList<String>? = null
-    ): Pair<Int, Float> {
-        var absStartX = x
-        val layout = if (ReadBookConfig.useZhLayout) {
-            ZhLayout(
-                text, textPaint, visibleWidth, emptyList(), emptyList(),
-                ReadBookConfig.paragraphIndent.length
-            )
-        } else {
-            StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
-        }
-        var durY = when {
-            //标题y轴居中
-            emptyContent && textPages.size == 1 -> {
-                val textPage = textPages.last()
-                if (textPage.lineSize == 0) {
-                    val ty = (visibleHeight - layout.lineCount * textHeight) / 2
-                    if (ty > titleTopSpacing) ty else titleTopSpacing.toFloat()
-                } else {
-                    var textLayoutHeight = layout.lineCount * textHeight
-                    val fistLine = textPage.getLine(0)
-                    if (fistLine.lineTop < textLayoutHeight + titleTopSpacing) {
-                        textLayoutHeight = fistLine.lineTop - titleTopSpacing
-                    }
-                    textPage.lines.forEach {
-                        it.lineTop -= textLayoutHeight
-                        it.lineBase -= textLayoutHeight
-                        it.lineBottom -= textLayoutHeight
-                    }
-                    y - textLayoutHeight
-                }
-            }
-
-            isTitle && textPages.size == 1 && textPages.last().lines.isEmpty() ->
-                y + titleTopSpacing
-
-            else -> y
-        }
-        for (lineIndex in 0 until layout.lineCount) {
-            val textLine = TextLine(isTitle = isTitle)
-            if (durY + textHeight > visibleHeight) {
-                val textPage = textPages.last()
-                if (doublePage && absStartX < viewWidth / 2) {
-                    //当前页面左列结束
-                    textPage.leftLineSize = textPage.lineSize
-                    absStartX = viewWidth / 2 + paddingLeft
-                } else {
-                    //当前页面结束,设置各种值
-                    if (textPage.leftLineSize == 0) {
-                        textPage.leftLineSize = textPage.lineSize
-                    }
-                    textPage.text = stringBuilder.toString()
-                    //新建页面
-                    textPages.add(TextPage())
-                    stringBuilder.clear()
-                    absStartX = paddingLeft
-                }
-                if (textPage.height < durY) {
-                    textPage.height = durY
-                }
-                durY = 0f
-            }
-            val lineStart = layout.getLineStart(lineIndex)
-            val lineEnd = layout.getLineEnd(lineIndex)
-            val lineText = text.substring(lineStart, lineEnd)
-            val (words, widths) = measureTextSplit(lineText, textPaint)
-            val desiredWidth = widths.fastSum()
-            when {
-                lineIndex == 0 && layout.lineCount > 1 && !isTitle -> {
-                    //第一行 非标题
-                    textLine.text = lineText
-                    addCharsToLineFirst(
-                        book, absStartX, textLine, words,
-                        desiredWidth, widths, srcList
-                    )
-                }
-
-                lineIndex == layout.lineCount - 1 -> {
-                    //最后一行
-                    textLine.text = lineText
-                    //标题x轴居中
-                    val startX = if (
-                        isTitle &&
-                        (ReadBookConfig.isMiddleTitle || emptyContent || isVolumeTitle)
-                    ) {
-                        (visibleWidth - desiredWidth) / 2
-                    } else {
-                        0f
-                    }
-                    addCharsToLineNatural(
-                        book, absStartX, textLine, words,
-                        startX, !isTitle && lineIndex == 0, widths, srcList
-                    )
-                }
-
-                else -> {
-                    if (
-                        isTitle &&
-                        (ReadBookConfig.isMiddleTitle || emptyContent || isVolumeTitle)
-                    ) {
-                        //标题居中
-                        val startX = (visibleWidth - desiredWidth) / 2
-                        addCharsToLineNatural(
-                            book, absStartX, textLine, words,
-                            startX, false, widths, srcList
-                        )
-                    } else {
-                        //中间行
-                        textLine.text = lineText
-                        addCharsToLineMiddle(
-                            book, absStartX, textLine, words,
-                            desiredWidth, 0f, widths, srcList
-                        )
-                    }
-                }
-            }
-            if (doublePage) {
-                textLine.isLeftLine = absStartX < viewWidth / 2
-            }
-            calcTextLinePosition(textPages, textLine, stringBuilder.length)
-            stringBuilder.append(lineText)
-            textLine.upTopBottom(durY, textHeight, fontMetrics)
-            val textPage = textPages.last()
-            textPage.addLine(textLine)
-            durY += textHeight * lineSpacingExtra
-            if (textPage.height < durY) {
-                textPage.height = durY
-            }
-        }
-        durY += textHeight * paragraphSpacing / 10f
-        return Pair(absStartX, durY)
-    }
-
-    private fun calcTextLinePosition(
-        textPages: ArrayList<TextPage>,
-        textLine: TextLine,
-        sbLength: Int
-    ) {
-        val lastLine = textPages.last().lines.lastOrNull { it.paragraphNum > 0 }
-            ?: textPages.getOrNull(
-                textPages.lastIndex - 1
-            )?.lines?.lastOrNull { it.paragraphNum > 0 }
-        val paragraphNum = when {
-            lastLine == null -> 1
-            lastLine.isParagraphEnd -> lastLine.paragraphNum + 1
-            else -> lastLine.paragraphNum
-        }
-        textLine.paragraphNum = paragraphNum
-        textLine.chapterPosition =
-            (textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull()?.run {
-                chapterPosition + charSize + if (isParagraphEnd) 1 else 0
-            } ?: 0) + sbLength
-        textLine.pagePosition = sbLength
-    }
-
-    /**
-     * 有缩进,两端对齐
-     */
-    private suspend fun addCharsToLineFirst(
-        book: Book,
-        absStartX: Int,
-        textLine: TextLine,
-        words: List<String>,
-        /**自然排版长度**/
-        desiredWidth: Float,
-        textWidths: List<Float>,
-        srcList: LinkedList<String>?
-    ) {
-        var x = 0f
-        if (!ReadBookConfig.textFullJustify) {
-            addCharsToLineNatural(
-                book, absStartX, textLine, words,
-                x, true, textWidths, srcList
-            )
-            return
-        }
-        val bodyIndent = ReadBookConfig.paragraphIndent
-        for (i in bodyIndent.indices) {
-            val x1 = x + indentCharWidth
-            textLine.addColumn(
-                TextColumn(
-                    charData = indentChar,
-                    start = absStartX + x,
-                    end = absStartX + x1
-                )
-            )
-            x = x1
-            textLine.indentWidth = x
-        }
-        if (words.size > bodyIndent.length) {
-            val text1 = words.subList(bodyIndent.length, words.size)
-            val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
-            addCharsToLineMiddle(
-                book, absStartX, textLine, text1,
-                desiredWidth, x, textWidths1, srcList
-            )
-        }
-    }
-
-    /**
-     * 无缩进,两端对齐
-     */
-    private suspend fun addCharsToLineMiddle(
-        book: Book,
-        absStartX: Int,
-        textLine: TextLine,
-        words: List<String>,
-        /**自然排版长度**/
-        desiredWidth: Float,
-        /**起始x坐标**/
-        startX: Float,
-        textWidths: List<Float>,
-        srcList: LinkedList<String>?
-    ) {
-        if (!ReadBookConfig.textFullJustify) {
-            addCharsToLineNatural(
-                book, absStartX, textLine, words,
-                startX, false, textWidths, srcList
-            )
-            return
-        }
-        val residualWidth = visibleWidth - desiredWidth
-        val spaceSize = words.count { it == " " }
-        if (spaceSize > 1) {
-            val d = residualWidth / spaceSize
-            var x = startX
-            for (index in words.indices) {
-                val char = words[index]
-                val cw = textWidths[index]
-                val x1 = if (char == " ") {
-                    if (index != words.lastIndex) (x + cw + d) else (x + cw)
-                } else {
-                    (x + cw)
-                }
-                addCharToLine(
-                    book, absStartX, textLine, char,
-                    x, x1, index + 1 == words.size, srcList
-                )
-                x = x1
-            }
-        } else {
-            val gapCount: Int = words.lastIndex
-            val d = residualWidth / gapCount
-            var x = startX
-            for (index in words.indices) {
-                val char = words[index]
-                val cw = textWidths[index]
-                val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
-                addCharToLine(
-                    book, absStartX, textLine, char,
-                    x, x1, index + 1 == words.size, srcList
-                )
-                x = x1
-            }
-        }
-        exceed(absStartX, textLine, words)
-    }
-
-    /**
-     * 自然排列
-     */
-    private suspend fun addCharsToLineNatural(
-        book: Book,
-        absStartX: Int,
-        textLine: TextLine,
-        words: List<String>,
-        startX: Float,
-        hasIndent: Boolean,
-        textWidths: List<Float>,
-        srcList: LinkedList<String>?
-    ) {
-        val indentLength = ReadBookConfig.paragraphIndent.length
-        var x = startX
-        for (index in words.indices) {
-            val char = words[index]
-            val cw = textWidths[index]
-            val x1 = x + cw
-            addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
-            x = x1
-            if (hasIndent && index == indentLength - 1) {
-                textLine.indentWidth = x
-            }
-        }
-        exceed(absStartX, textLine, words)
-    }
-
-    /**
-     * 添加字符
-     */
-    private suspend fun addCharToLine(
-        book: Book,
-        absStartX: Int,
-        textLine: TextLine,
-        char: String,
-        xStart: Float,
-        xEnd: Float,
-        isLineEnd: Boolean,
-        srcList: LinkedList<String>?
-    ) {
-        val column = when {
-            srcList != null && char == srcReplaceChar -> {
-                val src = srcList.removeFirst()
-                ImageProvider.cacheImage(book, src, ReadBook.bookSource)
-                ImageColumn(
-                    start = absStartX + xStart,
-                    end = absStartX + xEnd,
-                    src = src
-                )
-            }
-
-            isLineEnd && char == reviewChar -> {
-                ReviewColumn(
-                    start = absStartX + xStart,
-                    end = absStartX + xEnd,
-                    count = 100
-                )
-            }
-
-            else -> {
-                TextColumn(
-                    start = absStartX + xStart,
-                    end = absStartX + xEnd,
-                    charData = char
-                )
-            }
-        }
-        textLine.addColumn(column)
-    }
-
-    /**
-     * 超出边界处理
-     */
-    private fun exceed(absStartX: Int, textLine: TextLine, words: List<String>) {
-        val visibleEnd = absStartX + visibleWidth
-        val endX = textLine.columns.lastOrNull()?.end ?: return
-        if (endX > visibleEnd) {
-            val cc = (endX - visibleEnd) / words.size
-            for (i in 0..words.lastIndex) {
-                textLine.getColumnReverseAt(i).let {
-                    val py = cc * (words.size - i)
-                    it.start -= py
-                    it.end -= py
-                }
-            }
-        }
-    }
-
-    fun measureTextSplit(
-        text: String,
-        paint: TextPaint
-    ): Pair<ArrayList<String>, ArrayList<Float>> {
-        val length = text.length
-        val widthsArray = FloatArray(length)
-        paint.getTextWidths(text, widthsArray)
-        val clusterCount = widthsArray.count { it > 0f }
-        val widths = ArrayList<Float>(clusterCount)
-        val stringList = ArrayList<String>(clusterCount)
-        var i = 0
-        while (i < length) {
-            val clusterBaseIndex = i++
-            widths.add(widthsArray[clusterBaseIndex])
-            while (i < length && widthsArray[i] == 0f) {
-                i++
-            }
-            stringList.add(text.substring(clusterBaseIndex, i))
-        }
-        return stringList to widths
-    }
-    */
-
     /**
     /**
      * 更新样式
      * 更新样式
      */
      */
@@ -949,6 +280,12 @@ object ChapterProvider {
         upRenderStyle()
         upRenderStyle()
         val typeface = getTypeface(ReadBookConfig.textFont)
         val typeface = getTypeface(ReadBookConfig.textFont)
         val (titlePaint, contentPaint) = getPaints(typeface)
         val (titlePaint, contentPaint) = getPaints(typeface)
+        linePaint = Paint(contentPaint).apply {
+            clearShadowLayer()
+            isAntiAlias = true
+            strokeWidth = renderStyle.underlineHeight.toFloat()
+            style = Paint.Style.STROKE
+        }
         dashEffect = DashPathEffect(
         dashEffect = DashPathEffect(
             floatArrayOf(ReadBookConfig.durConfig.dottedBase, ReadBookConfig.durConfig.dottedRatio),
             floatArrayOf(ReadBookConfig.durConfig.dottedBase, ReadBookConfig.durConfig.dottedRatio),
             0f
             0f

+ 4 - 2
app/src/test/java/io/legado/app/ui/book/read/page/provider/ChapterProviderMetricsInvariantTest.kt

@@ -84,10 +84,12 @@ class ChapterProviderMetricsInvariantTest {
 
 
         /**
         /**
          * 允许保留可变的成员:
          * 允许保留可变的成员:
-         * - `dashEffect` / `reviewPaint`:绘制期读取的 Paint 类对象,本身可变,
+         * - `dashEffect` / `reviewPaint` / `linePaint`:绘制期读取的 Paint 类对象,本身可变,
          *   放进不可变快照没有意义(根治要让排版任务持有自己的副本,属 Track D2)。
          *   放进不可变快照没有意义(根治要让排版任务持有自己的副本,属 Track D2)。
+         *   `linePaint` 原本是 `by lazy`,因此躲过了本断言,代价是改下划线粗细/字体后
+         *   永远沿用首帧那份;改成 `upStyle()` 重建后才落进这一类。
          * - `upViewSizeRunnable`:主线程去抖用的 Runnable 句柄,不跨线程。
          * - `upViewSizeRunnable`:主线程去抖用的 Runnable 句柄,不跨线程。
          */
          */
-        val ALLOWED_MUTABLE = setOf("dashEffect", "reviewPaint", "upViewSizeRunnable")
+        val ALLOWED_MUTABLE = setOf("dashEffect", "reviewPaint", "linePaint", "upViewSizeRunnable")
     }
     }
 }
 }

+ 16 - 0
app/src/test/java/io/legado/app/ui/book/read/page/provider/RenderStyleSnapshotBoundaryTest.kt

@@ -45,6 +45,22 @@ class RenderStyleSnapshotBoundaryTest {
         )
         )
     }
     }
 
 
+    @Test
+    fun `绘制画笔不得用 by lazy 定型`() {
+        val lazyPaints = Regex("""\bval\s+(\w*[Pp]aint)\s*:\s*\w*Paint\s+by\s+lazy""")
+            .findAll(stripComments(mainSource(CHAPTER_PROVIDER)))
+            .map { it.groupValues[1] }
+            .toList()
+
+        assertTrue(
+            "以下画笔用 by lazy 定型:${lazyPaints.joinToString()}。\n" +
+                "`by lazy` 只在首次取用时算一次,此后改字体/字号/下划线粗细都不会反映到它上面," +
+                "而 upThemeColors() 又会就地改它的 color——于是它「颜色跟得上、其余永远是首帧那份」," +
+                "看起来像是生效了,最难归因。画笔请和 titlePaint/contentPaint 一样由 upStyle() 重建。",
+            lazyPaints.isEmpty(),
+        )
+    }
+
     @Test
     @Test
     fun `upRenderStyle 覆盖 RenderStyle 的每个字段`() {
     fun `upRenderStyle 覆盖 RenderStyle 的每个字段`() {
         assertAllFieldsAssigned(
         assertAllFieldsAssigned(