想達到 iReader 級別的兩端對齊(分散對齊),Android 原生 TextView 不支援 `textAlign=justify`。兩條成熟路徑任君選擇。
WebView wv = findViewById(R.id.webView);
String html = "%s";
wv.loadData(String.format(html, content), "text/html", "utf-8");
| 優點 | 缺點 |
|---|---|
| 零排版代碼、支援複雜富文本、自動換行換頁 | 啟動較慢、記憶體佔用高、字體渲染受 WebView 版本影響、難以精確控制行間距/字間距 |
class JustifiedTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
override fun onDraw(canvas: Canvas) {
val text = this.text.toString()
if (text.isBlank()) { super.onDraw(canvas); return }
val paint = this.paint
val width = measuredWidth - paddingLeft - paddingRight
val lines = breakLines(text, paint, width)
val y = paddingTop + paint.textSize
lines.forEachIndexed { idx, line ->
if (idx == lines.lastIndex && line.length < width * 0.6) {
// 最後一行不拉伸
canvas.drawText(line, paddingLeft.toFloat(), y, paint)
} else {
drawJustifiedLine(canvas, line, width, y, paint)
}
y += paint.fontMetricsInt.descent - paint.fontMetricsInt.ascent + lineSpacingExtra
}
}
private fun drawJustifiedLine(canvas: Canvas, line: String, width: Int, y: Float, paint: TextPaint) {
val words = line.split(" ").filter { it.isNotBlank() }
if (words.size <= 1) { canvas.drawText(line, paddingLeft.toFloat(), y, paint); return }
val textWidth = words.sumOf { paint.measureText(it).toInt() }
val spaceCount = words.size - 1
val totalSpace = width - textWidth
val baseSpace = totalSpace / spaceCount
val extra = totalSpace % spaceCount
var x = paddingLeft.toFloat()
words.forEachIndexed { i, w ->
canvas.drawText(w, x, y, paint)
x += paint.measureText(w)
if (i < spaceCount) {
val sp = baseSpace + if (i < extra) 1 else 0
x += sp
}
}
}
private fun breakLines(text: String, paint: TextPaint, width: Int): List {
// 簡易斷行:按空格切詞,貪心填滿
val words = text.split("\\s+".toRegex()).filter { it.isNotBlank() }
val lines = mutableListOf()
var current = ""
words.forEach { w ->
if (paint.measureText(current + " " + w) <= width) {
current = if (current.isEmpty()) w else current + " " + w
} else {
lines.add(current)
current = w
}
}
if (current.isNotEmpty()) lines.add(current)
return lines
}
}
<com.example.JustifiedTextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.5"
android:textSize="16sp" />