Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

    • 编程语言

      • CSS

        • CSS 学习笔记
        • BFC
        • flex布局笔记
        • inline-block空内容最小高度的问题
        • 单行文本两端对齐
        • 响应式卡片分页
        • 实现两个直角梯形按钮斜切的效果
        • 最多2行文本&单行文本居底
        • 段落文本嵌入图标
        • 监听 stick 状态
        • 【前端】仿 Android TextView 实现完整的文本溢出截断省略效果
          • 前言
          • 单行
            • 省略号在末尾
            • 省略号在开头
            • 背景知识
            • 应用
            • 省略号在两边
            • 前半字符串含关键字
            • 切分时两个字符串各占50%
            • 省略号在中间(或某个位置)
            • 跑马灯效果
          • 多行
          • 文本溢出模式
          • 总结
          • 展望
          • 拓展阅读
        • 前端元素默认宽高
        • CSS居中布局
      • HTML

      • JavaScript

      • Rust

      • TypeScript

      • WebAssembly

    • 开发工具

    • 前端调试

    • 浏览器原理

    • 浏览器生态

  • 应用框架

  • 工程能力

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 前端基础
  • 编程语言
  • CSS
gahing
2019/04/20
目录

【前端】仿 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"
    />
1
2
3
4
5
6
7

这些功能在前端中又该如何实现呢?本文将逐一进行介绍,并封装成一个 TextView 组件

项目已开源,详见 TextView (opens new window)

实现过程中还需要注意以下几点:

  1. 双击文本进行复制的时候,默认应该是拿到全部文本,且复制的内容中不含省略号(同时该实现对搜索引擎良好,因为所有文本保留)
  2. 自适应,宽度足够的话不显示省略号
  3. 增加省略号在两边的功能,适用于某些场景(后面会提到)

# 单行

针对块级元素溢出内容,CSS 有一个 text-overflow (opens new window) 属性,用于处理溢出内容的处理

目前浏览器支持的,通过提案的值有:

  • clip : 直接截断(默认值)
  • ellipsis: 采用省略号表示被截断的文本

以上为 Basic support 功能

在未通过的草案中还将支持 <string> 类型的值, fade 以及 fade() 方法,并允许配置两个值用于控制前后溢出内容的行为。但是浏览器基本都未支持,仅火狐支持了 String value 和 双值。详见浏览器兼容性 (opens new window)

[ clip | ellipsis | <string> ]{1,2}
1

基于兼容性问题,本节的实现仅基于 Basic support

# 省略号在末尾

最常见的需求

简单使用 text-overflow:ellipsis 即可

为了满足需求,还需要进行其他设置:

  1. white-space: nowrap; // 不对文本进行换行
  2. overflow: hidden; // 隐藏溢出的文本

由于是隐藏溢出,所以双击复制文本的时候,拿到的是全部的文本(不含省略号)

<div style="width: 100px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
  我和五个优秀员工,分别是xxxx
</div>
1
2
3
我和五个优秀员工,分别是xxxx

# 省略号在开头

先上例子,如果看懂的话可以直接跳过最后看结论

<div dir="rtl">0:这是测试文本-ss!</div>
<div dir="rtl"><span dir="ltr">0:这是测试文本-ss!</span></div>
1
2

对应在浏览器上的显示效果是什么呢?

                !这是测试文本-ss:0
                0:这是测试文本-ss!
1
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"
1
2
3
4
5
6

这是测试文本、-、ss 为相同方向,继续合并为一个方向串

最后就剩

0 ->
: <- 
这是测试文本-ss ->
! <-
1
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>
1
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>
1
2
3
0:这是测试文本-ss!

需要注意的是,在火狐中,得到的效果是呈现的字符相对父容器靠右

# 省略号在两边

一般使用的场景是,我们搜索到某个关键字,然后要居中展示该关键字,左右两边超过边界的字符都显示省略号

实现思路为切分为两个字符串,分为应用 省略在开头 和 省略在末尾 的解决方案

如何切分,又有以下两种思路

一般采用第二种

# 前半字符串含关键字

切分为两个字符串,前半字符串包含关键字

最后用一个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>
1
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
12-这是一段测试文本一段测试文本!@右侧文本。

在控制台中修改 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>
1
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
12-这是一段测试文本!@1
吱吱吱吱看的到我吗?

# 省略号在中间(或某个位置)

依旧需要对字符串进行切分

前后分别应用 省略在末尾 和 省略在开头

<!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>
1
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 宽度足够的时候,省略号将变为一个,样式上不统一

12-这是一段测试文本!@1
吱吱吱吱看的到我吗?

目前暂未找到其他解决方案

# 跑马灯效果

采用 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>
1
2

This text will scroll from right to left This text will scroll from right to left

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>
1
2

This text will scroll from right to left This text will scroll from right to left

利用 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>
1
2
3

This text will scroll from right to left This text will scroll from right to left This text will scroll from right to left

根据需求进行配置

# 多行

网上也有很多解决方案,详见 可能是最全的 “文本溢出截断省略” 方案合集 (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>
1
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

效果如下

lines

# 文本溢出模式

还剩下一个问题,如何判断当前文本处于文本溢出模式。

用于外部监听,当处于文本溢出模式,鼠标指向该组件时应该出现一个完整文本的 Tooltip 组件

那如何判断呢?

// target 为文本元素
let containerWidth = target.getBoundingClientRect().width  //当前容器的宽度
let textWidth = target.scrollWidth; //当前文字(包括省略部分)的宽度
let isEllipsis = textWidth > containerWidth
1
2
3
4

这里我们可以用 title 属性来模拟 Tooltip

if(isEllipsis){
  target.setAttribute("title","完整文本")
} else {
  el.removeAttribute("title")
}

1
2
3
4
5
6

# 总结

基本上我们已经能够在前端模拟 Android TextView 的溢出文本效果

目前提出的解决方案能够兼容大多数主流浏览器,若存在不兼容的情况,欢迎提出

# 展望

若本身包含阿拉伯字符,以上操作是否还有效,还未验证

后续将采用 React Hooks 技术对组件进行封装,并进行开源

# 拓展阅读

  1. CSS深度学习 - 文本方向 direction 和 dir (opens new window)
  2. bidi(双向文字)与RTL布局总结 (opens new window)
  3. Unicode 控制字符及其有关的双向算法 (opens new window)
  4. 多行文本省略近完美方案 (opens new window)
编辑 (opens new window)
#HTML#CSS
上次更新: 2023/08/26, 10:18:07
监听 stick 状态
前端元素默认宽高

← 监听 stick 状态 前端元素默认宽高→

最近更新
01
我的 2024 总结
12-31
02
浅谈代码质量与量化指标
08-27
03
快速理解 JS 装饰器
08-26
更多文章>
Theme by Vdoing | Copyright © 2016-2025 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式