阅读 788

Android UI 文本还原纪实

对于 Android 开发同学来说, "不管用户比例多高, 设计稿都按 iOS 给"绝对是最值得吐槽的事情之一.

在我刚开始接触 Android 开发的那个阶段, 每当有人问起这件事, 我都说 "Android 的做法就是看着差不多就行了..." 后来有些要求特别高的设计, Android 开发同学就只能很苦逼的一个 dp 一个 dp 的改到 UI 满意为止. 我看现在不少辅助开发工具的思路也是这样.

17 年底 ~ 18 年初搞 UI 大改版的时候, iOS 开发同学 传人 Joe 跟设计敲定了 iOS 的还原方式. 我觉得如果 Android 不搞的话之后开发就太烦了, 就决定 试一试. 最后的方案虽然不是特别通用, 但也能解决大部分问题.

然后就被设计小姐姐催着写原理, 然后一年就过去了...


从这里开始

图: 未对齐时的样子

上图中, 粉底的 "22 一行"和 "22 多行"是设计小姐姐给的参照图, 是用 Sketch 输出的, 使用 22 号字+默认行高情况下的设计稿上的样子. 绿底和黄底的 "22 一行"是 Nexus 5 上, 使用 22dp 的样子; 蓝底(叠加显示成紫色)的 "22 四行"也是 22dp, 文字用 "\n" 换行. 底色之间的差异就是"行高"的差异.

可见绿底一行的行高有一点细微的偏差, 黄底一行因为叠加了 4 次这个误差, 比较明显. 多行情况下的误差更大, 因为 Android 和 iOS 在多行文本排版的概念上差异很大. 参照图上单行和多行是能对上的, 现在我们要想办法让 Android 的单行和多行都能跟参照图对上.

单行对齐

观察发现单行差的是底部的一段空白, 我称之为 additionalPaddingBottom. 对比各种字号的情况, 发现并没有规律, 因此搞出来一组经验值.

这个经验值在 3 倍屏上还是比较准确的(最重要的是设计走查就用 3 倍屏...), 单行文字位置和行高都能对上. 在其他倍数的屏幕上基本 ok, 但也有一些异常. 比如在 1.5 倍屏上, 部分字号只能达成行高对的上但文字位置对不上的效果, 而且还受到 setSingleLine 的影响, 15dp + setSingleLine(true) 时偏差尤其大.

/**
 * density 为 3 时的经验值, 作为计算 additionalLineSpace 的基数
 */
static {
    paddingBottomMap.put(10, 1f / 3);
    paddingBottomMap.put(11, 4f / 3);
    paddingBottomMap.put(12, 2f / 3);
    paddingBottomMap.put(13, 1f / 3);
    paddingBottomMap.put(14, 3f / 3);
    paddingBottomMap.put(15, 2f / 3);
    paddingBottomMap.put(16, 1f / 3);
    paddingBottomMap.put(17, 4f / 3);
    paddingBottomMap.put(19, 1f / 3);
    paddingBottomMap.put(22, 2f / 3);
    paddingBottomMap.put(30, 5f / 3);
}
复制代码

多行对齐

根据 Android 文本排版概念, 我写了个简单的 MetricsTextView 来确定单行和多行的行高关系:

Matrics

观察发现: 两行文字的高度 = 单行文字的高度 + 单行文字设置 setIncludeFontPadding(false) 的高度

同时, 两行文字和两组单行的差别在于文字之间的空白, 因此需要增加 lineSpaceExtra = topSpace + bottomSpace + additionalPaddingBottom. 这样 Android 也实现了 n 行文字行高 = n x 单行文字行高, 多行也就对上了.

单行和多行对齐

行高

上面都是参考图使用默认行高的情况, 如果行高变了呢? 抱歉还是得基于经验值.

/**
 * sketch 中字号对应的默认行高 (dp)
 */
static {
    defaultLineHeightMap.put(10, 14);
    defaultLineHeightMap.put(11, 16);
    defaultLineHeightMap.put(12, 17);
    defaultLineHeightMap.put(13, 18);
    defaultLineHeightMap.put(14, 20);
    defaultLineHeightMap.put(15, 21);
    defaultLineHeightMap.put(16, 22);
    defaultLineHeightMap.put(17, 24);
    defaultLineHeightMap.put(19, 26);
    defaultLineHeightMap.put(30, 42);
}
复制代码

首先我们有默认行高的值, 然后把 deltaPaddingTop = deltaPaddingBottom = (lineHeight - defaultLineHeight) / 2 用 paddingTop 和 paddingBottom 加到 每一行 上 - 实验结果表明上下加的一样多, 可以除 2, 真是幸运.

带行高的对齐:

带行高的对齐

局限性

  • 只关注行高, 不关注文本宽度, 所以换行还是跟 iOS 不一样.
  • 并不是对所有的字号/字体都有效, 只处理了我们常用的字号(其它字号要加也不难), 默认字体.
  • 没有抽成库, 原因就是上面那条.
  • 18 年 Android 最新的 support 库好像为 AppCompatTextView 增加了行高支持, 但我还没有来得及试.

其它已知问题

  • 5.0 以下系统需要特殊处理 paddingBottom, 因为会增加额外的 lineSpaceExtra
    • 但是有些 5.0 及以上的手机 (vivo X9, 锤子) 居然也有这个问题, 管不了了...
  • 因为直接修改了 TextView 的 paddingTop 和 paddingBottom, 如果设计稿上有上下边距, 只能用 margin 或者再嵌套一层的方式解决.
    • 也许可以写得更完善些.
  • 在生产环境中使用时, 有同学发现该 TextView 中 ClickableSpan 的点击事件无法被触发.
    • 还木有解决...

@Uraka.Lee

关注下面的标签,发现更多相似文章
评论