【前端】仿 Android TextView 实现完整的文本溢出截断省略效果

2,326 阅读11分钟

前言

在 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"
    />

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

项目已开源,详见 TextView

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

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

单行

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

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

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

以上为 Basic support 功能

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

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

基于兼容性问题,本节的实现仅基于 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>
我和五个优秀员工,分别是xxxx

省略号在开头

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

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

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

                !这是测试文本-ss:0
                0:这是测试文本-ss!

(自右向左的书写方向)

黑人问号

背景知识

dir HTML 属性用于决定文本总体的书写方向。默认值为 ltr 表示从左到右, rtl 表示从右到左(一般用于阿拉伯语)

效果与 CSS 的 direction 属性相同

在浏览器中,字符按 html 结构中的顺序被存放到内存中,其与页面显示的顺序是不一样的。页面显示的方向由 unicode-bidi 双向书写算法决定。注意:双击选中复制文本的时候,拿的是内存中的值,不受 unicode-bidi 影响

根据方向属性, Unicode 字符分为以下几种类型:

类型 方向 字符 效果
强字符 LTR/RTL 英文、汉字、阿拉伯文字等 方向性确定,LTR 或 RTL,和上下文无关.且可能影响其前后字符的方向性
弱字符 LTR/RTL 数字、数字相关符号 方向性确定,但是不会影响前后字符的方向性
中性字符 Neutral 大部分标点符号和空格 方向性不确定,由上下文环境决定其方向

上下文环境由强字符方向及全局书写方向决定,具体规则后面再写一篇文章 bidi 算法的讲解

一段文本中具有相同方向性的连续字符,称为方向串

因此 <div dir="rtl">0:这是测试文本-ss!</div> 包含了如下的方向串

0 ->
: <- // dir="rtl"
这是测试文本 ->
- -> // 受强字符影响
ss ->
! <- // dir="rtl"

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

最后就剩

0 ->
: <- 
这是测试文本-ss ->
! <-

根据 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>

考虑到父元素宽度足够大,而文本较少时,文本会靠右显示,故在 div 上设置 text-align: left;

实验中发现,如果 span 采用的是 direction: ltr; ,还需要加上 unicode-bidi: embed; 在边界加入一些控制字符。具体原理本文不再分析。

unicode-bidi 的相关资料可以查看以下链接

developer.mozilla.org/zh-CN/docs/…

www.w3.org/TR/CSS2/vis…

<div dir="rtl" style="text-align: left;width: 100px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
  <span dir="ltr">0:这是测试文本-ss!</span>
</div>
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>
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>
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>

在 chrome 上显示良好,但是火狐,IE 两个省略号中会存在一个间隔,并且当 wrapper 宽度足够的时候,省略号将变为一个,样式上不统一

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

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

跑马灯效果

采用 <marquee> 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>

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>

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>

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

根据需求进行配置

多行

网上也有很多解决方案,详见 可能是最全的 “文本溢出截断省略” 方案合集,本文不做原理讲解

各有优缺点,没有完美方案

这里以伪元素 + 定位实现多行省略的解决方案实现多行溢出省略的效果

通过 line-heightmax-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>

效果如下

lines

文本溢出模式

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

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

那如何判断呢?

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

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

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

总结

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

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

展望

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

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

拓展阅读

  1. CSS深度学习 - 文本方向 direction 和 dir
  2. bidi(双向文字)与RTL布局总结
  3. Unicode 控制字符及其有关的双向算法
  4. 多行文本省略近完美方案