ソースを参照

fix: 高亮规则 fontPath 排版测量与绘制字体不一致导致行首行尾标点错位

排版阶段始终用默认字体测宽和断行,但绘制阶段才切换高亮规则字体,
导致不同字体的标点 advance 差异引起首尾符号错位。

- TextLine.addColumn(): 把 fontPath.isNotEmpty() 纳入 onlyTextColumn 判断,
  防止仅改字体的规则被 fastDrawTextLine 跳过
- TextColumn: 新增 internal getTypeface() 让排版阶段复用 typeface 缓存
- TextChapterLayout: 在 applyHighlightRules 后用 remeasureWithHighlightFonts()
  按字符/cluster 使用对应 fontPath 的 TextPaint 重新测宽,
  确保 ZhLayout 断行、desiredWidth、TextColumn.start/end 全部基于正确字体度量
HapeLee 5 ヶ月 前
親
コミット
ce22b9f20e

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

@@ -82,7 +82,7 @@ data class TextLine(
     fun addColumn(column: BaseColumn) {
     fun addColumn(column: BaseColumn) {
         if (column !is TextColumn) {
         if (column !is TextColumn) {
             onlyTextColumn = false
             onlyTextColumn = false
-        } else if (column.textColor != null || column.bgColor != null || column.underlineMode != 0 || column.bgImage.isNotEmpty()) {
+        } else if (column.textColor != null || column.bgColor != null || column.underlineMode != 0 || column.bgImage.isNotEmpty() || column.fontPath.isNotEmpty()) {
             onlyTextColumn = false
             onlyTextColumn = false
         }
         }
         column.textLine = this
         column.textLine = this

+ 8 - 4
app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt

@@ -110,15 +110,19 @@ data class TextColumn(
     }
     }
 
 
     private fun getCustomTypeface(): Typeface? {
     private fun getCustomTypeface(): Typeface? {
-        if (fontPath.isEmpty()) return null
-        return typefaceCache.getOrPut(fontPath) {
-            loadTypeface(fontPath)
-        }
+        return getTypeface(fontPath)
     }
     }
 
 
     companion object {
     companion object {
         private val typefaceCache = HashMap<String, Typeface?>()
         private val typefaceCache = HashMap<String, Typeface?>()
 
 
+        internal fun getTypeface(fontPath: String): Typeface? {
+            if (fontPath.isEmpty()) return null
+            return typefaceCache.getOrPut(fontPath) {
+                loadTypeface(fontPath)
+            }
+        }
+
         private fun loadTypeface(fontPath: String): Typeface? {
         private fun loadTypeface(fontPath: String): Typeface? {
             return runCatching {
             return runCatching {
                 when {
                 when {

+ 33 - 0
app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt

@@ -924,6 +924,10 @@ class TextChapterLayout(
         val charStyles = applyHighlightRules(text, isTitle)
         val charStyles = applyHighlightRules(text, isTitle)
         val widthsArray = allocateFloatArray(text.length)
         val widthsArray = allocateFloatArray(text.length)
         textPaint.getTextWidthsCompat(text, widthsArray)
         textPaint.getTextWidthsCompat(text, widthsArray)
+        // 用高亮规则字体重新测量字符宽度,确保排版和绘制使用同一套字体
+        if (charStyles != null) {
+            remeasureWithHighlightFonts(text, charStyles, textPaint, widthsArray)
+        }
         val layout = if (useZhLayout) {
         val layout = if (useZhLayout) {
             val (words, widths) = measureTextSplit(text, widthsArray)
             val (words, widths) = measureTextSplit(text, widthsArray)
             val indentSize = if (isFirstLine) paragraphIndent.length else 0
             val indentSize = if (isFirstLine) paragraphIndent.length else 0
@@ -1359,6 +1363,35 @@ class TextChapterLayout(
         return code == 8203 || code == 8204 || code == 8205 || code == 8288
         return code == 8203 || code == 8204 || code == 8205 || code == 8288
     }
     }
 
 
+    /**
+     * 对有自定义字体的高亮字符重新测量宽度,确保排版和绘制使用同一套字体度量。
+     */
+    private fun remeasureWithHighlightFonts(
+        text: String,
+        charStyles: Array<CharStyle?>,
+        textPaint: TextPaint,
+        widthsArray: FloatArray
+    ) {
+        val originalTypeface = textPaint.typeface
+        var i = 0
+        while (i < text.length) {
+            val fontPath = charStyles[i]?.fontPath.orEmpty()
+            if (fontPath.isEmpty()) { i++; continue }
+            // 找连续使用同一字体的区间
+            val segStart = i
+            i++
+            while (i < text.length && charStyles[i]?.fontPath.orEmpty() == fontPath) i++
+            val segEnd = i
+            val typeface = TextColumn.getTypeface(fontPath) ?: continue
+            textPaint.typeface = typeface
+            val segment = text.substring(segStart, segEnd)
+            val segWidths = FloatArray(segEnd - segStart)
+            textPaint.getTextWidthsCompat(segment, segWidths)
+            segWidths.copyInto(widthsArray, segStart)
+        }
+        textPaint.typeface = originalTypeface
+    }
+
     /**
     /**
      * 对文本应用高亮规则,返回每字符的样式数组。无匹配时返回 null。
      * 对文本应用高亮规则,返回每字符的样式数组。无匹配时返回 null。
      */
      */