Icon和文本对齐方式的探索

发布于 大漠

在Web中很多场景中都会使用到Icon,那么就会面临Icon和文本对齐的处理。而这个对齐效果的处理又不是一件轻易的事情,特别是面又众多不同移动终端的情形之下。那么今天这篇文章就来和大家一起探讨一下这方面的话题。

Web中的图标

这里所说的Web中的图标是指Web中的Icon图标。从《Web中的图标》一文中,我们可以得知,到目前为止,在Web中使用图标的方式主要有:

  • 通过img标签加载Icon图标
  • 使用svg内联标签,加载矢量Icon图标
  • 通过Icon Fonts来加载Icon图标

不管是哪一种方式,都有各自的利弊,至于怎么选择,可以阅读《Web中的图标》一文。在这里,我们主要来探讨的是图标和文本的对齐姿势

Web中引用图标的姿势

在Web应用程序或者Web页面中,引用Icon图标,常见的方式主要有:

<elem>
    <img src="...">文本
</elem>

<elem>
    文本<img src="...">
</elem>

<elem>
    <svg></svg>文本
</elem>

<elem>
    文本<svg></svg>
</elem>

<elem>
    <iconfont></iconfont>文本
</elem>

<elem>
    文本<iconfont></iconfont>
</elem>

但在很多情况之下,为了更好的让Icon图标和文本能对齐(垂直对齐),大部分同学喜欢用一个行内元素,比如<span>把文本包裹起来,就像下面这样:

<elem>
    <img src="..."><span>文本</span>
</elem>

而实际中,有些场景是无法让我们人肉的给文本内容添加类似<span>这样的标签,特别是在CMS系统中的操作,或者说通过JavaScript动态插入内容。

对于如何使用CSS让Icon图标和文本能够垂直对齐,我想大家脑海中第一浮现的属性是 vertical-align 。那么,是否使用该属性就真的能让Icon图标和文本能完美的实现垂直居中呢?希望大家带着这样的一个问题继续往下阅读。

浏览器的默认行为

首先来看看浏览器的默认行为。先来看两种情形:

<elem>
    <img />case
</elem>

<elem>
    <img /><span>case</span> 
</elem>

为了让样子好看一点,添加一点基本样式:

body {
    padding: 2vw;
    font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue",
    "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC",
    "WenQuanYi Micro Hei", sans-serif;
    font-size: 14px;
    line-height: 1.7;
    -webkit-font-smoothing: antialiased;
}
svg,img {
    width: .85em;
}

.fas {
    font-size: 170px;
}

在你的浏览器下看到的效果如下:

接下来稍稍改变一下,让浏览器自行让Icon和文本垂直对齐。到目前为止,主流的有两种方式:

  • inline-flexalign-items
  • inline-blockvertical-align

前者采用的是CSS Flexbox的模式,后者是较为传统的垂直对齐模式。一般情况之下,inline-flexalign-items运用于:

<elem>
    <img />case
</elem>

即: 文本没有额外的HTML标签包裹 !而inline-blockvertical-align一般用于:

<elem>
    <img /><span>case</span>
</elem>

事实上,inline-flex也可以运用于后者的结构中。

特别声明,示例代码中的<img />标签可以是<svg>或者<iconfont>,其中<span>标签也可以是其他任何你希望的HTML标签或者HTML自定义标签。

为了能更为形象的看出它们之间的差异性。来做几个测试用例,看看浏览器对其渲染效果的差异性。

先看inline-flex(不管带不管span标签):

.demo {
    display: inline-flex;
    align-items: center;
}

看上去似乎对齐:

如果按照inline-flexalign-items的属性特性描述来判定,Icon和文本是应该对齐(注意是应该对齐)。那么我们把页面的截图放到设计软件中来描绘制一下:

为了能更好的展示测试效果,我把文本换成了三种情形。仅从页面上的显示效果(肉眼看上去),似乎对齐,但事实并非如此。

接着再来看第二种对齐方式:

.demo img,
.demo svg,
.demo .fas,
.demo span{
    display: inline-block;
    vertical-align: middle;
}

浏览器渲染出来的效果如下:

左侧和右侧的效果有明显的差异性。主要是左侧的文本内容没有使用额外的HTML标签包裹。如果你观察足够仔细的话,使用inline-blockvertical-align:middle会造成文本离开baseline的基线,这种现象会直接影响上下间距和相邻元素的对齐。

特别声明:上面测试效果截图来源于 Chrome v67.0.3396.99。MacOS v10.12.6。其他平台,特别是Android系统下测试结果略有偏差!

简单分析

前面我们看到了浏览器对于Icon和文本垂直对齐的渲染效果。不管使用的是哪一种方式(浏览器对齐方式),最终看到的效果都只是近似对齐。为什么会这样呢?这里简单的来分析一下。

IFC概念

IFC是“Inline Formatting Context”的缩写,类似于BFC的一个概念,其主要用来定义行内盒子(Inline Box)组成上下文中的表现特性。有关于IFC的详细描述,可以查阅W3C规范中的相关说明

根据W3C规范的描述我们可以得知:

由IFC描述可以了解到,行内盒子(Inline Box)是由行内级元素(Inline Level Elements)文本(Contents)所组成,而行内盒子水平排列所构成的一行矩形区域,被称作行盒子(Line Box)

从IFC的场景中再回到我们的实际场景中,Icon图标是一个行内级元素,而文本是另外一个行内级元素,把这两个元素形成的两个行内盒子放在一起,构成了一个行盒子。除了inline-blockinline-flex元素和文本是行内级元素外,display属性为inlineinline-table的元素以及像imginputvideo等**替换元素(Replaced Elements)**都是行内级元素,但它们在行盒子中的设计计算方式不太相同。

替换元素的高度等于它们本身的高度(包含margin,而非替换元素的高度,其计算非常复杂,其中字体度量(Font Metrics)对其影响就较大。

Font Metrics

Font Metrics常常被称之为字体度量,指的就是字体中的一系列参数,这些参数对于CSS而言是不可见的,但我们可以借助类似FontForgeOpentype.js等工具来获取。

如果你对字体度量较为感兴趣,强烈建议你花一些时间这篇文章进行了解。这里用一张经典的图来展示我们更为关注的一些信息:

而我们关注的是,怎么得到元素的实际高度。假设我们的示例中,设置了font-size100px,它的高度并不一定等于100px,比如这里,其实际高度就是100 / 2048 * (2167 + 536) ≈ 132px

其他概念

这里所说的其他概念,指的就是CSS中有关于字体相关的一些属性:

  • vertical-align
  • baseline
  • line-height
  • font-size

那么在这里我并不想花太多时间来解释这些概念或者说这些属性的具体使用。但对于Icon和文本的对齐方式的的确确会受到这些属性的值的影响。或者简单的说,将会受到字体度量相关的参数影响,而这几个又是其重要的一些参数。

假设你对这些属性和相应的概念有了深入的了解。如果你对它们还不够了解,建议你花时间先阅读下面这些文章,因为这些文章可以更好的帮助你理解后面的内容,和找到解决方案的依据:

解决方案

特别声明,整个方案的解决思路来源于@长天之云的《图标如何对齐文本》一文。

虽然使用inline-flexalign-itemsinline-blockvertical-align可以达到对齐效果,但这些效果都是近似对齐。因此很多同学会质疑,为什么没有真正对齐,特别是在Android系统上的一些APP中。造成这种现象主要是我们没有搞清楚浏览器是如何决定图标放置,它们对齐又是参照什么方式。

如果要彻底解决,那就需要搞清楚它们之间的关系。

假设你对字体的特征和CSS的vertical-align有了一定的了解。因此我们直接进入主题。先来看一张图:

从这张图中,可以获取很多有价值的信息:

  • 最左侧是一个已对齐的Icon图标,它的尺寸是1.2 x 1.2 em
  • 最右侧是文字特征的一些横向参考线,除了cap-line之外都是vertical-align的可选值,每种值得到的不同效果,可以阅读vertical-align,你应该知道的一切一文
  • vertical-align作用于文本和非文本(比如示例中的imgsvg等元素)效果将不同,比如vertical-align取值为baseline时:“同一行内不同字体类型、字体大小或不同行高的文本对齐在相同的baseline上;同一行内不同尺寸的图片底边对齐在baseline上”
  • xHeight为小写字母xheight,可以用ex单位来表示(1ex大约等于0.5em),如果vertical-align取值为middle,那么middle的值就是xHeight的一半(xHeight / 2),因此,如果仅对图片应用于vertical-align: middle时,图片看上去会偏下
  • capHeight为大写字母高度(CSS和JavaScript都无法获取),大多数字体约为0.7em

了解这些信息之后,我们可以考虑Icon图标和大定字母中间对齐,即:baseline往上先移动capHeight / 2 。 这样一来可以考虑:

从baseline基线开始偏移

默认情况之下,Icon图标底边贴在baseline上,可以先移动Icon图标自身50%(向下移动,比如translateY(50%)),使其中间对齐baseline,然后再上移capHeight / 2

.demo img {
    vertical-align: baseline;
    transform: translateY(calc(50% - 0.35em))
}

从middle基线开始偏移

Icon图标正中间正好在middle基线上,所以可以先将图片下移xHeight / 2,然后再上移capHeight / 2

.demo img {
    vertical-align: middle;
    position: relative;
    top: -.1em; // xHeight / 2 - cpaHeight / 2 = (.5 - .7) / 2 = -.1em
}

也可以使用ex

.demo img {
    vertical-align: middle;
    position: relative;
    top: calc(.5ex - .35em);
}

其实这种方式也是我们常用的一种方式,但最终的效果也只能得到近似对齐的效果,因为示例中的偏移值会根据当前字体不同而有细微差异。比如下图所展示的数据,就是根据Font Metrics的参数,针对不同字体计算出来的值:

感觉花这么多时间,并不解决我们想要解决的问题。那么有没有办法实现精确的对齐呢?先来看内容,再来找答案吧。同样先上一张图:

内联元素在不同条件下产生了不同的边界(颜色填充区域),先来解释一下这张图展示的相关信息:

j表示基础的line-height,也就是1em高,等同于当前的font-size:100px。当inline-blockinline-flex元素设置为line-height: 1时,就可以得到它,图中橙色区域。值得注意的是,j的边界不在任何参考线上(vertical-align的值对应的参考线)。也就是说,使用这个值的时候要特别小心,特别是设置了overflow: hidden时,有可能会造成内容被截断。这也就是为什么很多同学在一些运用中会发现,文本之类的显示不全。

x表示安全的line-height,是inline元素默认的高度(也是text-toptext-bottom的距离)。也对应(ascender + descender) / unitsPerEm计算得到的值(Font Metrics的一些参数),在我们的示例中,采用的字体对应的值是1.17777

S表示实际的line-height,是inline-blockinline-flex元素默认的高度(也是topbottom的距离)。当父容器设置line-height: 1.7时,元素对应的实际line-height100px * 1.7 = 170px

有了这些信息只是我们实现精确对齐的基础,关键点我们还需要借助一个参考特。这里可以借鉴CSS规范中的Strut概念

On a block container element whose content is composed of inline-level elements, 'line-height' specifies the minimal height of line boxes within the element. The minimum height consists of a minimum height above the baseline and a minimum depth below it, exactly as if each line box starts with a zero-width inline box with the element's font and line height properties. We call that imaginary box a "strut." (The name is inspired by TeX.).

也就是说,创建一个局部的容器,生成不可见文本(零空格,模拟Strut),让不可见文本对齐Line Box中其他文本,让图标对齐这个不可见文本。

局部容器的高度可以是上图中S的高度(inline-flex居中),也可以跟随图标的高度(图标绝对定位,保持与容器位置相同),也可以固定(图标绝对定位居中,但缺陷是不能超过行高太多)。

这样一来,我们的结构可以修改为:

<elem>
    <span>&#8203;<img /></span>文本
</elem>

同样可以使用inline-blockinline-flex来完成。先来看inline-block的CSS代码:

span {
    position: relative;
    display: inline-block;
    line-height: 1; // 使文本高度为图标高度
    width: 1em; //图标大小,用来占位
}
span img {
    position: absolute;
    top: 0;
    left: 0;
    width: 1em;
    height: 1em;
}

对于inline-flex要比inline-block简单的多:

span {
    display: inline-flex;
    align-items: center;
}

原来方案就是这么的简单。

特别声明,整个方案的解决思路来源于@长天之云的《图标如何对齐文本》一文。

除了手动在HTML结构中添加&#8203;来模拟Strut之外,还可以借助CSS的伪元素::before来处理:

<elem>
    <span>
        <img />
    </span>
    文本
</elem>

在上面的代码中添加:

span::before {
    content: '\00a0'
}

为了一劳永逸,也可以考虑将其封装成一个组件,比如@长天之云将其封装成了一个React组件。如果你感兴趣的话,也可以将其封装成Vue组件等。

小技巧:如果你项目中使用的是svg标签制作Icon图标。为了能让图标像Icon Font那样缩放,建议你在svg中设置widthheight的时候采用em单位,比如width: 1em;height:1em,就相当于SVG图标会根据font-size进行缩放。有关于这方面更详细的介绍可以阅读《Align SVG Icons to Text and Say Goodbye to Font Icons》一文。译文可以点击这里阅读

总结

Icon图标和文本对齐是日常开发中常碰到的细节问题,在不同的环境和场景中都会花费一定的时间来处理这样的细节。这篇文章主要深纠了一下Icon图标和文本对齐的一些原理,通过这些原理找出相应的解决方案。文章提供了两种解决思路,一种是大家时常用的近似对齐方式,另一种是精确的对齐方式解决方案。

如果您有更好的解决方案,欢迎在下面的评论中一起交流和探讨!

扩展阅读