「展开全部」应该在左边还是右边

前几个月一直在做知乎「想法」,项目上线以后开始逐渐扣一些细节上的体验。之前想法首页的 Feed 卡片简单限制了文本内容的行数为 6 行,但是上线以后百字以上的内容并不少见,用户也普遍反馈希望有一个类似微信朋友圈的「展开全部」功能。于是我们就做了这么一个功能,但是在实践过程中存在争执,我认为有必要记录下这一过程。

最开始的时候,我们最直接想到的,是当文本超出 6 行时,「展开全部」四个字自动出现在第 6 行的的最右,缩略时确保存在省略号,如下所示:

情况一,文本缩略后,「展开全部」恰好跟在文本末尾:

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Ex... 展开全部

情况二,文本缩略后,距离屏幕右边还有一定距离:

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu...                           展开全部

之所以想到这样的实现,于技术上,是因为很久之前知乎 Android App 的问题详情描述就倾向于这样,由于当时技术有限没有做到,现在这个问题解决了,于个人私心是算耿耿于怀吧;产品上认为位于最右有利于用户单手操作,同时比另起一行更节省空间。大家都很认可,于是就这样做了。

但是在 Demo 实现后,我的思路开始发生了转变,认为朋友圈的实现是更好的,实际上即刻也是如此。朋友圈的策略是,缩略的末尾不需要省略号,同时「展开全部」另起一行置于最左

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
展开全部

为了表明自己的观点,在具体讨论之前,我先来说说两种类似「展开全部」的文本末尾处理的实现方案。

第一种是直接在原文本被缩略的基础上,进行二次处理,在其末尾拼接上「展开全部」字样,并设置相应的 ClickableSpan ,所有内容均在一个 TextView 中展示。对于上述置于最右时的情况二,我们可以使用 ReplacementSpan 或者空格占位,关键在于如何准确计算出需要占位的空间长度。我们只需要计算最后一行所需的占位,也就是单行文本所需的占位空间,可以使用如下代码得到:

// 使用 TextView 自己持有的 Paint 对象,
// 而不是 new 出来的 Paint 对象测量文本长度。
// 原因在于 TextView 会对自己持有的 Paint 对象做一些设置,
// 直接 new 出来的 Paint 对象缺少这些设置,会导致测量结果出现偏差
@FloatRange(from = 0.0F)
public static float measureText(
@NonNull TextView tv, @NonNull CharSequence text) {
return tv.getPaint().measureText(text, 0, text.length());
}

接着直接用 TextView 本身的宽度减去单行文本的宽度即可。为了尽可能地减少二次 layout ,我们重写了 TextView 的 onLayout(而不是 onMeasure ,因为 onMeasure 并不能保证 setText 之后 getLineCount 马上就能拿到准确的数值),在文本拼接处理完毕后再次调用 setText 即可。但是这种方法存在瑕疵,你会发现实际的 TextView 高度总比你期望的高度稍微高那么一点点,由此导致点击「展开全部」时 TextView 会先往回缩一下才展开,有肉眼可见的跳动,所以还是得二次 layout 才能保证 TextView 的高度正确;另外,这种方法并不能保证「展开全部」一定摆放在最右,如果是稍有偏差是可以接受的,但熟悉 TextView 的同学都知道,TextView 本身的排版过程是很迷的,同一段文本在有缩略和没有缩略的情况下,文字右边的折行规则可能不一样,与预期不符的提前折行或延后折行,都会导致「展开全部」的位置摆放错误,出现一两个字的距离偏差是很容易发生的事。总的来说就是,我们干涉到了 TextView 本身的排版过程,出现问题是不可避免的。

第二种方法就比较简单了,「展开全部」和缩略的文本实际上是两个 TextView ,各自负责各自的排版,互不干涉,只需要解决两个 TextView 之间的排放位置关系即可;且不会出现文字展开时的抖动现象。这里不可避免地还会二次 layout ,但两种方法相比,技术角度更推荐第二种。实际上这也是用户体验的一部分,我们知道了第一种方法的体验是不好的;第二种则是常见的解决方案了。

在此基础上,我们再回头看看最左和最右的问题。对于最右,推荐单个 TextView 内部拼接文本的实现,不得已而为之;对于最左,则推荐使用两个 TextView 排列组合的实现,简单高效。最右虽然相对于最左有方便右手点击的优势,但是其有个致命性的缺点,就是视觉重心问题,大多数人习惯的阅读顺序是从左到右从上往下的,大多数 App 的卡片设计也是 F 型的视觉重心,此时你在右下角放一个可点击的类按钮控件,会吸引用户注意力,打破了这种视觉习惯,造成观感上的不适。放右边确实挺蠢的,哈哈。

返回