【前端】仿 Android TextView 实现完整的文本溢出截断省略效果
# 前言
在 Android 的文本组件中,有个内容过长显示省略号的属性 - ellipsize ,有以下选项
- end: 省略号在末尾
- start: 省略号在开头
- middle: 省略号在中间
- marquee: 跑马灯效果
并通过 singleline/lines 等属性进行行数的约束
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleline="true"
/>
2
3
4
5
6
7
这些功能在前端中又该如何实现呢?本文将逐一进行介绍,并封装成一个 TextView 组件
项目已开源,详见 TextView (opens new window)
实现过程中还需要注意以下几点:
- 双击文本进行复制的时候,默认应该是拿到全部文本,且复制的内容中不含省略号(同时该实现对搜索引擎良好,因为所有文本保留)
- 自适应,宽度足够的话不显示省略号
- 增加省略号在两边的功能,适用于某些场景(后面会提到)
# 单行
针对块级元素溢出内容,CSS 有一个 text-overflow (opens new window) 属性,用于处理溢出内容的处理
目前浏览器支持的,通过提案的值有:
clip
: 直接截断(默认值)ellipsis
: 采用省略号表示被截断的文本
以上为 Basic support 功能
在未通过的草案中还将支持 <string>
类型的值, fade 以及 fade() 方法,并允许配置两个值用于控制前后溢出内容的行为。但是浏览器基本都未支持,仅火狐支持了 String value 和 双值。详见浏览器兼容性 (opens new window)
[ clip | ellipsis | <string> ]{1,2}
基于兼容性问题,本节的实现仅基于 Basic support
# 省略号在末尾
最常见的需求
简单使用 text-overflow:ellipsis
即可
为了满足需求,还需要进行其他设置:
white-space: nowrap;
// 不对文本进行换行overflow: hidden;
// 隐藏溢出的文本
由于是隐藏溢出,所以双击复制文本的时候,拿到的是全部的文本(不含省略号)
<div style="width: 100px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
我和五个优秀员工,分别是xxxx
</div>
2
3
# 省略号在开头
先上例子,如果看懂的话可以直接跳过最后看结论
<div dir="rtl">0:这是测试文本-ss!</div>
<div dir="rtl"><span dir="ltr">0:这是测试文本-ss!</span></div>
2
对应在浏览器上的显示效果是什么呢?
!这是测试文本-ss:0
0:这是测试文本-ss!
2
(自右向左的书写方向)
# 背景知识
dir (opens new window) HTML 属性用于决定文本总体的书写方向。默认值为 ltr 表示从左到右, rtl 表示从右到左(一般用于阿拉伯语)
效果与 CSS 的 direction 属性相同
在浏览器中,字符按 html 结构中的顺序被存放到内存中,其与页面显示的顺序是不一样的。页面显示的方向由 unicode-bidi (opens new window) 双向书写算法决定。注意:双击选中复制文本的时候,拿的是内存中的值,不受 unicode-bidi 影响
根据方向属性, Unicode 字符分为以下几种类型:
类型 | 方向 | 字符 | 效果 |
---|---|---|---|
强字符 | LTR/RTL | 英文、汉字、阿拉伯文字等 | 方向性确定,LTR 或 RTL,和上下文无关.且可能影响其前后字符的方向性 |
弱字符 | LTR/RTL | 数字、数字相关符号 | 方向性确定,但是不会影响前后字符的方向性 |
中性字符 | Neutral | 大部分标点符号和空格 | 方向性不确定,由上下文环境决定其方向 |
上下文环境由强字符方向及全局书写方向决定,具体规则后面再写一篇文章 bidi 算法的讲解
一段文本中具有相同方向性的连续字符,称为方向串
因此 <div dir="rtl">0:这是测试文本-ss!</div>
包含了如下的方向串
0 ->
: <- // dir="rtl"
这是测试文本 ->
- -> // 受强字符影响
ss ->
! <- // dir="rtl"
2
3
4
5
6
这是测试文本
、-
、ss
为相同方向,继续合并为一个方向串
最后就剩
0 ->
: <-
这是测试文本-ss ->
! <-
2
3
4
根据 rtl 的总体书写方向,最后在页面中显示为 !这是测试文本-ss:0
,通过光标选中也能够测试其内部方向
如果文本应用了内联元素,其文本中中性字符的方向不受外层影响
对于 <div dir="rtl"><span dir="ltr">0:这是测试文本-ss!</span></div>
这个例子
span 中所有字符都是采用 ltr 的方向,对于 div 来说,其内容又是自右向左的。
于是就能实现自右向左水平溢出,内部字符顺序保持不变的效果
# 应用
根据以上特性,我们再应用上 text-overflow:ellipsis
试试
<!DOCTYPE HTML>
<html>
<head>
<style type="text/css">
div {
text-align: left;
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
</head>
<body>
<div dir="rtl"><span dir="ltr">0:这是测试文本-ss!</span></div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
考虑到父元素宽度足够大,而文本较少时,文本会靠右显示,故在 div 上设置 text-align: left;
实验中发现,如果 span 采用的是 direction: ltr;
,还需要加上 unicode-bidi: embed;
在边界加入一些控制字符。具体原理本文不再分析。
unicode-bidi 的相关资料可以查看以下链接
https://developer.mozilla.org/zh-CN/docs/Web/CSS/unicode-bidi
https://www.w3.org/TR/CSS2/visuren.html#propdef-unicode-bidi
<div dir="rtl" style="text-align: left;width: 100px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
<span dir="ltr">0:这是测试文本-ss!</span>
</div>
2
3
需要注意的是,在火狐中,得到的效果是呈现的字符相对父容器靠右
# 省略号在两边
一般使用的场景是,我们搜索到某个关键字,然后要居中展示该关键字,左右两边超过边界的字符都显示省略号
实现思路为切分为两个字符串,分为应用 省略在开头 和 省略在末尾 的解决方案
如何切分,又有以下两种思路
一般采用第二种
# 前半字符串含关键字
切分为两个字符串,前半字符串包含关键字
最后用一个div 包住两个字符串,该 div 宽度为前串 div 宽度+16,并应用 省略在末尾 的解决方案
注意,为了自适应,前串的宽度定义为 max-width: calc(100% - 14px);
当父 div 足够大时,前面的字串会先展示,然后再展示后面的字串
<!DOCTYPE HTML>
<html>
<head>
<style type="text/css">
.wrapper {
width: 200px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.left {
display: inline-block;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100% - 14px);
vertical-align: text-bottom;
}
</style>
</head>
<body>
<div class="wrapper">
<span dir="rtl" class="left"><span dir="ltr">12-这是一段测试文本一段测试文本!@</span></span><span class="right">右侧文本。</span>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
在控制台中修改 wrapper 的宽度,查看效果
注意以下几点:
- 行内元素之间会带一个空格;代码里写成一行就不会
- 在火狐中复制,需要
ctrl+a
全选才行 - 由于 IFC 的原因,后面的内联块会垂直偏下,通过设置
vertical-align
解决
# 切分时两个字符串各占50%
与上种思路的效果不同,当父元素变大的时候,左右两边的字符会一起不断展示
前串通过设置 max-width:50%;
样式,当父元素足够大的时候,文字始终居左
若要实现文字始终居中,则改为 width:50%;
<!DOCTYPE HTML>
<html>
<head>
<style type="text/css">
.wrapper {
width: 200px;
}
.left {
display: inline-block;
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.right {
width: 50%;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="left" dir="rtl"><span dir="ltr">12-这是一段测试文本!@1</span></div><span class="right">吱吱吱吱看的到我吗?</span>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 省略号在中间(或某个位置)
依旧需要对字符串进行切分
前后分别应用 省略在末尾 和 省略在开头
<!DOCTYPE HTML>
<html>
<head>
<style type="text/css">
.wrapper {
width: 200px;
}
.left {
max-width: 50%;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
}
.right {
display: inline-block;
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="left">12-这是一段测试文本!@1</div><span class="right" dir="rtl"><span dir="ltr">吱吱吱吱看的到我吗?</span></span>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
在 chrome 上显示良好,但是火狐,IE 两个省略号中会存在一个间隔,并且当 wrapper 宽度足够的时候,省略号将变为一个,样式上不统一
目前暂未找到其他解决方案
# 跑马灯效果
采用 marquee (opens new window) HTML 元素实现。该元素兼容性高,并提供多种属性配置。
loop 控制滚动次数,默认值 -1 表示连续滚动
<marquee width="100" loop="-1">This text will scroll from right to left</marquee>
<marquee width="100" loop="3">This text will scroll from right to left</marquee>
2
direction 控制滚动方向,可选值有 left【默认】, right, up and down
<marquee width="100" direction="left">This text will scroll from right to left</marquee>
<marquee width="100" direction="up">This text will scroll from right to left</marquee>
2
利用 scrollamount/scrolldelay/truespeed
控制滚动速度
<marquee width="100" scrollamount="6" scrolldelay="30" truespeed>This text will scroll from right to left</marquee>
<marquee width="100" scrollamount="10" scrolldelay="30" truespeed>This text will scroll from right to left</marquee>
<marquee width="100" scrollamount="6" scrolldelay="80">This text will scroll from right to left</marquee>
2
3
根据需求进行配置
# 多行
网上也有很多解决方案,详见 可能是最全的 “文本溢出截断省略” 方案合集 (opens new window),本文不做原理讲解
以及一个较为流行的开源库 Clamp.js (opens new window)
各有优缺点,没有完美方案
这里以伪元素 + 定位实现多行省略的解决方案实现多行溢出省略的效果
通过 line-height
和 max-height
控制行数。至于行高应该设置多少,这个应该对外提供一个属性让用户进行配置。总之,max-height = N * line-height (em)
<!DOCTYPE HTML>
<html>
<head>
<style type="text/css">
.multi-line-ellipsis {
width: 200px;
max-height: 2em;
line-height: 1;
overflow: hidden;
position: relative;
text-align: justify;
margin-right: -1em;
padding-right: 1em;
}
.multi-line-ellipsis::before {
content: '...';
position: absolute;
right: 0;
bottom: 0;
}
.multi-line-ellipsis::after {
content: '';
position: absolute;
right: 0;
width: 1em;
height: 1em;
margin-top: 0.2em;
background: white;
}
.block-with-text:before {
content: '...';
position: absolute;
right: 0;
bottom: 0;
}
</style>
</head>
<body>
<div class="multi-line-ellipsis">这是一串短字符串</div><br/>
<div class="multi-line-ellipsis">这是一串长长长长长长长长长长长字符串</div><br/>
<div class="multi-line-ellipsis">这是一串长长长长长长长长长长长字符串,后面的内容应该会被截掉了</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
效果如下
# 文本溢出模式
还剩下一个问题,如何判断当前文本处于文本溢出模式。
用于外部监听,当处于文本溢出模式,鼠标指向该组件时应该出现一个完整文本的 Tooltip 组件
那如何判断呢?
// target 为文本元素
let containerWidth = target.getBoundingClientRect().width //当前容器的宽度
let textWidth = target.scrollWidth; //当前文字(包括省略部分)的宽度
let isEllipsis = textWidth > containerWidth
2
3
4
这里我们可以用 title 属性来模拟 Tooltip
if(isEllipsis){
target.setAttribute("title","完整文本")
} else {
el.removeAttribute("title")
}
2
3
4
5
6
# 总结
基本上我们已经能够在前端模拟 Android TextView 的溢出文本效果
目前提出的解决方案能够兼容大多数主流浏览器,若存在不兼容的情况,欢迎提出
# 展望
若本身包含阿拉伯字符,以上操作是否还有效,还未验证
后续将采用 React Hooks 技术对组件进行封装,并进行开源