TextView 在测量文字宽度时,用的是字体里每个 glyph 的“advance”(水平步进)。
ľ(U+013E,小写 L + 抑扬符)在很多字体里为了美观,会把抑扬符的“尾巴”做成负 left‐bearing 的 overhang——也就是实际位图比 advance 再往左探出去几个像素。
Android 的 Layout/StaticLayout 在 measure 阶段 只累加 advance,不会为 overhang 追加额外宽度;到了 draw 阶段 却会把整个 bitmap 画出来,于是尾部 overhang 被父控件边缘或 TextView 自己的 padding 裁剪掉,看起来“点被吃掉”。
宽度“足够”只是 advance 足够,但 overhang 没算进去,所以仍然显示不全。
二、根治方案(4 选 1)
| 方案 | 改动点 | 优点 | 缺点 |
|---|---|---|---|
| 1. 字体改造 | 把 ľ 的 left‐bearing 调成 ≥ 0 | 彻底,无运行时损耗 | 需换字体/重签名 |
| 2. 字符串层面 | 末尾加 U+00A0(NBSP)或 U+200A(thin space) | 0 行 Java,立即生效 | 多一个空白字符,复制粘贴会带空格 |
| 3. View 层面 | 给 TextView 加 2-3 dp 右 padding | 0 行 Java,设计师可控 | 所有行尾部都多留白 |
| 4. 代码层面 | 自定义 ReplacementSpan 把 ľ 的 advance 手动加宽 | 精准,只影响 ľ | 需要写 30 行 Java,维护成本 |
线上最快落地:方案 2 或 3。
长期彻底:方案 1。
三、方案 2 代码示例(Kotlin)
val raw = "Reštaurácia Hiltonľ."
tv.text = raw.replace(Regex("ľ\b"), "ľ\u00A0") // 只在词尾加 NBSP
四、方案 4 代码示例(Java)
public class SlovakLCaronSpan extends ReplacementSpan {
private final float extra; // 额外宽度,px
public SlovakLCaronSpan(float extraPx) { this.extra = extraPx; }
@Override
public int getSize(@NonNull Paint paint, CharSequence text,
int start, int end, @Nullable FontMetricsInt fm) {
return (int) (paint.measureText(text, start, end) + extra);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text,
int start, int end, float x, int top, int y,
int bottom, @NonNull Paint paint) {
canvas.drawText(text, start, end, x, y, paint);
}
}
// 使用
SpannableString sp = new SpannableString("Reštaurácia Hiltonľ.");
int i = sp.toString().indexOf('ľ');
if (i >= 0) {
sp.setSpan(new SlovakLCaronSpan(dp2px(3)), i, i + 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
tv.setText(sp);
五、完整调用时序图
六、一句话总结
ľ 的尾巴是字体 overhang,Android measure 只算 advance;
要么让字体把 overhang 收回来,要么人为在尾部加空白/右 padding/ReplacementSpan 把 advance 撑大,就能把“点”完整露出来。