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

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

- TextLine.addColumn(): 把 fontPath.isNotEmpty() 纳入 onlyTextColumn 判断,
  防止仅改字体的规则被 fastDrawTextLine 跳过
- TextColumn: 新增 internal getTypeface() 让排版阶段复用 typeface 缓存
- TextChapterLayout: 在 applyHighlightRules 后用 remeasureWithHighlightFonts()
  按字符/cluster 使用对应 fontPath 的 TextPaint 重新测宽,
  确保 ZhLayout 断行、desiredWidth、TextColumn.start/end 全部基于正确字体度量
This commit is contained in:
HapeLee
2026-06-11 01:36:05 +08:00
parent 8af29b969b
commit ce22b9f20e
3 changed files with 42 additions and 5 deletions
@@ -82,7 +82,7 @@ data class TextLine(
fun addColumn(column: BaseColumn) {
if (column !is TextColumn) {
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
}
column.textLine = this
@@ -110,15 +110,19 @@ data class TextColumn(
}
private fun getCustomTypeface(): Typeface? {
if (fontPath.isEmpty()) return null
return typefaceCache.getOrPut(fontPath) {
loadTypeface(fontPath)
}
return getTypeface(fontPath)
}
companion object {
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? {
return runCatching {
when {
@@ -924,6 +924,10 @@ class TextChapterLayout(
val charStyles = applyHighlightRules(text, isTitle)
val widthsArray = allocateFloatArray(text.length)
textPaint.getTextWidthsCompat(text, widthsArray)
// 用高亮规则字体重新测量字符宽度,确保排版和绘制使用同一套字体
if (charStyles != null) {
remeasureWithHighlightFonts(text, charStyles, textPaint, widthsArray)
}
val layout = if (useZhLayout) {
val (words, widths) = measureTextSplit(text, widthsArray)
val indentSize = if (isFirstLine) paragraphIndent.length else 0
@@ -1359,6 +1363,35 @@ class TextChapterLayout(
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。
*/