提示框组件的实现给我带来的思考和探索

发布于 大漠

Tooltips大家很多时候也将其称为提示框,另外还有一个视觉上长得和Tooltips的组件,常常称为Popovers(又名弹窗)。很多时候他们在视觉上长得非常类似,但是共交互上却有明显性的差异,特别是在PC端上:Tooltips一般是通过鼠标的悬浮来触发;Popovers一般是通过点击来触,但在移动端上两者都是通过点击来触发,因为移动端上悬浮的交互行为基本是不存在的。不过我们今天要聊的不是他们的交互行为,而是来看看如何以最好的方式来还原他们的视觉效果,并且能适用各种不同的UI风格。

使用场景

为了能更好的阐述后面要聊的东西,我们先来看一张截图:

上图是从平时工作场景碰到的UI效果截图过来的。上图中展示的Tooltips框基本上覆盖了常见的UI风格。简单的来归纳一下:

  • 带边框的提示框
  • 纯色(或带透明度纯色)的提示框
  • 带内阴影(或外阴影)的提示框
  • 带边框+渐变的提示框
  • 带边框+透明度背景的提示框
  • 提示框三角带圆角和阴影的提示框

可能还有我未碰到的提示框UI风格。面对这么多的UI风格,对于前端实现上来说是具有一定的挑战性,特别是多种效果组合在一起的。比如说,带有边框+内外阴影+渐变(或透明度)+圆角三角等。基本上组合了上图所提到的各种UI风格。

这次在项目中,实现带有边框和透明颜色背景,着实让我尝试了多种不同的实现方案。实为痛苦!

接下来看看,我是怎么来处理这种UI上的还原。

大家熟悉的技术方案

对于纯色(或带有透明度的纯色)提示框,实现起来是最为简单,借助伪元素::before(或::after)来实现即可。三角实现手段:

  • 使用border来绘制三角
  • 伪元素是一个正方形(长方形),然后做相应的旋转
  • 使用CSS渐变绘制三角形
  • 使用CSS的clip-path绘制三角形

先来看CSS的border绘制三角:

width: 0;
height: 0;
border: 10px solid;
border-color: yellow blue red green;

可以看到四个不同颜色的三角形:

这个时候,只需要留一条边的颜色,就可以得到对应方向的三角形。虽然其他边看不见,但还是占有一定的空间。如果你希望其不占空间,可以考虑将其border-width重置为0。比如:

width: 0;
height: 0;
border: 10px solid;
border-color: transparent transparent red transparent;
border-width: 0 10px 10px 10px;

可以得到一个向上的red三角形,并且顶部不占任何空间:

其他方向的三角形可以采用类似的方式获取。另外,在border-leftborder-right的宽度稍作调整之后还可以很容易让你获得直三角形(有的Tooltips会有这样的需求),具体代码不展示了,给大家示意一张图:

对于正方形放置绘制三角形这里不做额外的阐述。这里给大家展示一个长方形绘制三角形:

使用上图这种方法绘制三角形有几个关键点,其中最主要的是width = 1.41 x height(三角形需要额外的一个标签来制作)。

注意:如果三角形朝左或朝右时,height = 1.41 x width

使用border和矩形制作三角最大的不同之处,矩形制作三角形具有以下几个优势:

  • 易于加阴影
  • 易于加边框
  • 易于加圆角

另外还可以使用CSS的渐变来绘制三角形,而且可以绘制不同的三角形,比如矩形三角形等腰三角形等边三角形随机三角形

使用渐变绘制等边三角形和随机三角形时需要做一些简单的数学计算。其中等边三角形需要让高度足够大:

如上图所示,使用两个渐变g1g2,蓝线是div的宽度w,每个渐变是宽度的一半(w/2),而且三角形的每条边都相等(等于w)。绿线是渐变的高度hg,那么可以通过下面公式得到hg的值:

(w/2)² + hg² = w² ─➤ hg = (sqrt(3)/2) * w ─➤ hg = 0.866 * w

借助CSS自定义属性和CSS的calc()函数,就可以很好的控制每个渐变的大小(background-size):

.triangle {
    --w:100px;
    width:var(--w);
    height:100px;
    background-image:
        linear-gradient(to bottom right, transparent 49.5%,red 50%),
        linear-gradient(to bottom left,  transparent 49.5%,red 50%);
    background-size:calc(var(--w)/2 + 0.5px)  calc(0.866 * var(--w));
    background-position: left bottom,right bottom;
    background-repeat:no-repeat;
}

随机三角形相对来说更为简易于一些,也可以借助一些公式,这样更易于控制随机三角形的造形:

两个渐变的高度分别是hg1hg2,而且都相等(上图中的红线),然后把wg1wg2定义为渐变的宽度(wg1 + wg2 = a),他们之间的关系如下:

wg2 = (a²+c²-b²)/(2a)
wg1 = a - wg2
hg1 = hg2 = sqrt(b² - wg1²) = sqrt(c² - wg2²)

如此一来,可以很好的控制随机三角形:

.triangle {
    --wg1: 20px; 
    --wg2: 60px;
    --hg:30px; 
    width:calc(var(--wg1) + var(--wg2));
    height:100px;
    background-image:
        linear-gradient(to bottom right, transparent 49.5%,red 50%),
        linear-gradient(to bottom left,  transparent 49.5%,red 50%);

    background-size:var(--wg1) var(--hg),var(--wg2) var(--hg);
    background-position:left bottom,right bottom;
    background-repeat:no-repeat;
}

除了上面提到的方法之外,还可以借助HTML的实体符来生成三角形(▲ ─➤ ▲, ▼ ─➤ ▼,◄─➤ ◄,►─➤ ►) 。还有就是CSS的clip-path(后面会介绍)。

花了一定的篇幅来介绍如何使用CSS制作三角形,因为知道三角形的制作方式,对我们Tooltips的技术选型有着关键的作用。

如果你对三角形如何制作更详细的介绍,可以点击这里进行了解

回到提示框的制作上来。先来看border制作三角形,从而实现提示框的效果:

点击案例中的“PLAY”按钮,你可以看到提示框整个实现的过程:

采用这种方案会有几个缺陷:

  • 带边框的提示框,三角形部分需要另外一个伪元素叠加模拟边框,难控制
  • 带圆角的提示框,三角形添加圆角时无法和容器接壤
  • 带透明度的提示框+边框的提示框,透明三角形会露底(能看到容器的边框,无法遮盖)
  • 带阴影的提示框,三角形无法添加阴影(添加阴影时会是一个矩形阴影)

如下图放大部分所示:

小结:如果使用border来绘制提示框的三角形,只适合纯色或带透明的纯色提示框的制作

接下来看第二种方案,将提示框的三角形换成矩形来绘制。同样来看下面这个示例,点击**“PLAY”**按钮,可以看到整个制作过程:

使用::before(或::after)绘制了一个20px x 20px的矩形,然后以矩形中心点旋转45deg,将三角定位到对应的位置:

注意,这里的三角未采用上面提到矩形制作三角的方法,直接使用一个旋转的正方形来绘制。这种方式不足之处,正方形的另一半会说遮盖容器上面(或下面)。

该方案也会存在一些缺陷:

  • 带有透明度+边框时会楼底
  • 带有渐变时三角形的颜色难于和容器接壤

相比而言,使用矩形旋转来制作三角形要比border的方式适用的场景更多一些。但面对带有阴影和渐变的场景时,这两种方案都存有一定的缺陷。当然,对于带有阴影的提示框,我们可以借助::before::after的伪元素来模拟box-shadow,三角通过一个矩形旋转来处理:

按同样的原理,来实现一个带有渐变的提示框:

此处有一个调试小细节,就是三角的颜色和主体渐变色的边接(接壤处),如果从设计稿上不好获取的话,可以借助浏览器调试工具中的颜色取色器获取,像下面这样操作:

具体代码可以查看下面的Demo:

使用clip-path制作提示框

CSS的clip-path属性是一个很有意思的属性,接下来的内容有一个前提,假设性你已经对该属性有了一定的了解。如果你是初次接触该属性,建议您先花一点时间阅读下面相关的教程:

对于使用clip-path制作提示框,它可能需要七个坐标点

根据上图,我们使用clip-path:polygon()calc()配合CSS自定义属性来绘制一个提示框的效果:

来看其中一个示例代码:

:root {
    --w: 50vh;  // 提示框宽度
    --h: 30vh;  // 提示框高度
    --h1: 4vh;  // 提示框三角形距容器高度
    --w1: 10vh; // 提示框底点1距容器边缘宽度
    --w2: 13vh; // 提示框顶点距容器边缘宽度
    --w3: 16vh; // 提示框底点2距容器边缘宽度
}

div {
    margin: 2vh;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    color: #fff;
    width: var(--w);
    height: var(--h);
    background: #f36;
    clip-path: polygon(
        0 0, 
        100% 0, 
        100% calc(100% - var(--h1)),
        calc(100% - var(--w1)) calc(100% - var(--h1)),
        calc(100% - var(--w2)) 100%,
        calc(100% - var(--w3)) calc(100% - var(--h1)),
        0 calc(100% - var(--h1))
    );
}

上面的代码根据上图套入公式计算得来。提示框的三角方向不同时,clip-pathpolygon()函数中的坐标值需要做相应的调整。具体的代码可以看上面的示例。

对于上面示例中的提示框,如果你不想花时间去做计算的话,可以借助**clippy**来帮助我们实现:

就在元素自身上使用clip-path来绘制提示框也有利弊:

  • 可以灵活的绘制三角形
  • 可以灵活的接壤容器

不足之处是:

  • border-radius缺失
  • box-shadow缺失
  • border缺失

也就是说,在元素自身上使用clip-path无法绘制带有圆角边框阴影相关的提示框。不过我们可以来个迂回战术,和前面的方法一样,借助::before::after来配合,实现一个更为灵活的提示框。

首先拿一个较为复杂的案例来举例:

提示框带有边框、带有透明(或渐变)、带圆角和阴影

在具体介绍如何使用clip-path绘制这个复杂的提示框效果之前,先来看看如何使用clip-path绘制一个三角形。因为这个三角形也很重要。使用clip-path绘制一个三角形有多种方法,比如说,对一个正方法对角做裁剪,然后旋转不同的方向,得到相应的三角形:

使用clip-path对一个正方形按下图的方式进行裁剪,得到一个直角三角形:

代码很简单:

div {
    width: 30vh;
    height: 30vh;
    background: #f36;
    clip-path: polygon(
        0 0,
        100% 100%,
        0 100%
    );
}

除了上述方法之外,还可以使用clip-path绘制任何你想要的三角形。同样拿**clippy**来做辅助:

上面我们看到的是一个矩形中绘制三角形,其实我们还可以脱离矩形(比如在一个正方形)绘制三角形:

其实使用clip-path绘制三角形没那么复杂。

就上图,我们借助CSS自定义属性和calc()来绘制三角形:

接下来把提示框分成两个部分,一个是四方形,一个是三角形,然后两个拼接在一起组合成一个提示框。这样整个坐标示意图如下:

假设提示框的尺寸是w x h,边框厚度是h1,那么绘制带有缺口的时需要以下几个坐标点:

  • d1坐标(0, 0)
  • d2坐标((50% - b), 0)((w / 2 - b), 0) 其中b是三角形对角边长度的一半,后面会介绍到
  • d3坐标((50% - b - h1), h1)((w / 2 - b - h1), h1)
  • d4坐标((50% + b + h1), h1)((w / 2 + b + h1), h1)
  • d5坐标((50% + b), 0)((w / 2 + b), 0)
  • d6坐标(100%, 0)(w, 0)
  • d7坐标(100%, 100%)(w, h)
  • d8坐标(0, 100%)(0, h)

如果将上面的坐标点放置到clip-pathpolygon()函数中,最终剪切之后的图形看上去像下图:

clip-path: polygon(
    0 0, 
    calc(50% - 4px) 0, calc(50% - 7px) 2px, calc(50% + 7px) 2px, calc(50% + 4px) 0, 100% 0, 100% 100%, 0 100%, 0 0);

另外就是三角形的部分,如果我们的三角形是一个10px x 10px旋转45deg得到。根据一些三角函数的公式和已知的正方形边长就可以计算出正方形斜对角的长度:

这个时候你就得到了提示框的两个部分:

这样提示框效果就出来了:

这样就可以较好的适合较多场景的提示框。不过较为麻烦的是坐标点的确定和计算。不过使用clip-path还可以更帮助我们更好的控制一些动效。比如@Travis Almand的《Animating with Clip-Path》一文中详细介绍了clip-path给动效过场带来的动效效果。

未来的技术方案

虽然clip-path能帮助我们更好的实现提示框的效果,但灵活度还是有一定的缺陷。不过在未来, 随着CSS Houdini技术的成熟度更高时,那么对于提示框的制作,甚至更为复杂的UI场景我们也可以得到更好的封装,而暴露给使用的开发者而言,就仅仅几个相关的API。

比如:

<div class="tooltip-1">This is a tip</div> 
<script>CSS.paintWorklet.addModule('my-tooltip-worklet.js')</script> 
<style> 
.tooltip-1 { 
    background-image: paint(tooltip); 
    padding: calc(var(--triangle-size) * 1px + .5em) 1em 1em; 
    --round-radius: 0; 
    --background-color: #4d7990; 
    --triangle-size: 20; 
    --position: 20; 
    --direction: top; 
    --border-color: #333; 
    --border-width: 2; 
    color: #fff; 
} 
</style>

上面的示例来自于@Yangguang写的一个示例。有关于my-tooltip-worklet.js的代码可以点击这里查阅

如果你觉得上面的案例有点复杂,可以先从简单的看起。在CSS Houdini Rocks上,@iamvdo展示了一个简单的提示框案例

.el {
    background: #00c6ff;
    border-image-source: paint(tooltip);
    border-image-slice: 0 0 100% 0;
    border-image-width: var(--border-width);
    border-image-outset: var(--border-width);
    --border-width: 25px;
    --tooltip-position: 48%;
    --tooltip-size: 30px;
}

// paint.js

registerPaint('tooltip', class Bubble {

    static get inputProperties() {
        return [
            'background-color',
            '--tooltip-position',
            '--tooltip-size'
        ];
    }

    paint(ctx, geom, props, args) {

        const color = props.get('background-color').toString()
        const positionPercent = props.get('--tooltip-position').toString().replace('%', '') * 1
        const position = geom.width * positionPercent / 100
        const size = props.get('--tooltip-size').toString().replace('px', '') * 1

        ctx.beginPath();
        ctx.moveTo(position - size, 0);
        ctx.lineTo(position + size, 0);
        ctx.lineTo(position, geom.height);
        ctx.closePath();

        // fill
        ctx.fillStyle = color;
        ctx.fill();

    }

})

效果如下:

如果你对CSS Houdini 相关的技术感兴趣的话,可以点击这里进行了解

组件化构建

提示框是常见的一种业务形态或者说UI组件。在CSS自定义属性的出现和CSS Houdini的即将到来,我们在构建组件的时候,应该将这些技巧的思想或技巧带进来。比如说,我们使用Vue或React构建提示框组件的时候,应该更像CSS Houdini的实现思路,在底层将相关的细节封装完成,暴露给使用者只是仅仅几个API。

在接下来,我会尝试着在Vue,React体系下,借助CSS Houdini和CSS的自定义属性等特殊来封装一个提示框,看看最终能否达到类似CSS Houdini实现的提示框效果。

另外,尝试了一下imgcook这样的生成工具

从Sketch中导出数据,然后在生成工具中来生成:

从图中可以看得出来,最终依赖的还是图片。刚开始我以为是效果太过复杂,然后我尝试着使用一个更简单的:

终究还是没有逃脱使用图片的宿命。

不管是人工还原还是智能机器人还原,我想我们都应该追求一个原则,能用代码实现的应该尽量使用代码实现。就该示例而言,智能化转化如果要达到这一点,还是有很多事情需要做的。期待有一天这个领域会更为成熟。

待续...

在这一章中,主要探索了各种实现提示框UI的技术方案。虽然每一种方案都有自己的利弊,但可以说没有好坏之分,只有是否合适的区别。但在构建一个常见或者说简易(任何东西的真正实现都来得不那么地轻易),我们更应该考虑怎么将新技术,新特性带进来。让我们的事情能变得更为简易。在下一节中,我主要会从CSS的自定义属性、CSS Houdini的角度出发,看看如何在Vue或React这样的框架体系下怎么构建一个复用性,灵活性,可扩展性更强的提示框组件。