最近的需求中有一个tab切换的场景,设计师提出了自己期望的效果,核心关注点在蓝色边框上,本文围绕如何实现这样的边框效果展开讨论。
背景
设计师期望的效果如下,核心关注点在蓝色边框上。
实现这样的边框,核心问题有几个:
- 如何将两个元素的边框相连
- 内凹的圆角如何实现
- tab元素滚动离屏,边框如何过渡
CSS
我决定先用CSS试试,border + border-radius,应该轻松搞定。
▐ 问题一:CSS 如何实现边框相连
这倒不难,我们需要:
- 给 tab元素 设置
border-right: none
,同时border-top-right-radius: 0
、border-bottom-right-radius: 0
- 再给 tab元素 一个向右的偏移,偏移量 = 边框宽度
- 最后让 tab元素 z-Index 高于内容区,并给 tab元素 加上背景色(背景色需要和页面背景色一致)
- 缺陷
这时候缺点已经来了,我们通过加背景色遮盖边框实现边框相连,不可避免地遮盖了页面内容,如果页面背景比较复杂,我们会很难处理。这个方案并不足够通用,但好在我们的场景页面背景纯白,先忍了。
▐ 问题二:CSS 如何实现内凹圆角
也还行,我们需要:
- 新增两个元素,宽高和
border-radius
值相等 - 元素1 设置背景色,先覆盖在边框相连处
- 元素2 设置
border-bottom-right-radius: 50%
,同时border
和tab元素保持一致
- 缺陷
其实和问题一一样,我们又使用了背景色对边框进行遮盖,但先忍了,实现要紧。
▐ 问题三:CSS 如何实现滚动离屏过渡
这个问题用css就比较难实现了,它可以被拆解成两个子问题:
- tab元素的顶边在离屏过程中需要固定,border 框选区域高度不断变小
- 圆角如何平滑过渡到直线
如果世界上已经没有其他方式能实现这样的边框,我想硬着头皮写一堆恶心逻辑也是能实现效果的,但我觉得这样的实现比较丑陋,不太优雅,因此 CSS 的尝试到这里就结束了,我决定换个方案。
SVG
其实使用SVG来实现一些CSS不好处理的场景在社区中已经有很多实践了。比如用于新人引导的开源库 driver.js。
driver.js地址:https://driverjs.com/
【新人引导】指的是这样的场景:
这个场景下,【蒙层内区域高亮】是技术核心,driver.js 在几个月前刚进行了一次重构,将蒙层改用SVG实现,支持了高亮区的圆角。这给了我启发,哥们也用 SVG 画个边框吧。
▐ 问题一:SVG 如何实现边框相连
svg嘛,用起来就是更麻烦,先从简单的开始吧:
- 怎么用 SVG 画一条线
这个容易,使用 <line />
标签,提供两个点坐标(x1, y1)、(x2, y2)
,在描述一下边框的样式就可以了。
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <line x1="0" y1="80" x2="100" y2="20" stroke="black" /> </svg>
使用 line 标签的方式固然可以,但为了方便后续代码逻辑,我们还有更好的方式:<path />
标签,我们可以通过命令式的方式,完成 SVG 各种型状的绘制,比如一条直线:
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <path d="M 0 80 L 100 20" stroke="black" fill="none" /> </svg>
<path />
文档地址:https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths
其中核心字段位 d="M 0 80 L 100 20"
,这一段命令中有两个指令 M
、L
:
M
全称“Move to”,可以理解为将SVG画笔挪到某个点作为路径起点,因此该命令后边跟两个数字,分别对应起点的x、y
L
全称“Line”,可以理解为从当前画笔位置为起点,绘制一条直线到另一个点(终点),并且绘制后,画笔位置也会挪到终点,(path 中大多数指令都是指定终点即可,起点就是当前画笔位置),因此该命令后边跟两个数字,分别对应终点的x、y
。
关于 path 的其他指令不再赘述,总的来说,想使用 path 绘制边框,我们首先要获取到边框上各个结点坐标,之后再用命令将他们链接起来。
- 获取点坐标来画线
我们首先获取 tab元素 和 内容区 的四个节点,我们通过getBoundingClientRect
方法获取 top
、left
、right
、bottom
四个值来构造这些点坐标。
但我们不能直接给他两点相连起来,那就成这样了:
我们需要做做个调整,需要将(right1, top1)
、(right1, bottom1)
两个点的 x 坐标做偏移,让这两点的 x 和元素2的 left 一致,得到(left2, top1)
、(left2, bottom1)
我们再给这些点加上编号,按照 ABCDEFGH 的顺序,将这些点通过直线相连,path的命令就会如下:
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <path d=" M left1 top1 L left2 top1 L left2 top2 L right2 top2 L right2 bottom2 L left2 bottom2 L left2 bottom1 L left1 bottom1 Z " stroke="black" fill="none" /> <!-- Z 命令为 path 结束指令--> </svg>
这样实现的边框,不会有 CSS 背景色遮挡的问题。
▐ 问题二:SVG 如何实现内凹圆角
问题又变得复杂起来了,同样,我们还是先从简单的开始吧:
- 怎么用 SVG 画一个圆弧
path 中有一个弧形指令A
,这个指令能绘制椭圆,正圆自然也不在话下,他的参数有很多:
A rx ry x-axis-rotation large-arc-flag sweep-flag x y
rx ry
:为 X 轴和 Y 轴的半径,对于正圆来说,rx = ry,在我们的场景里,他的值和 border-radius 是等效的x-axis-rotation
:用于控制这个弧线沿 X 轴旋转的角度,对于正圆来说,怎么转都一样,所以这个值我们使用时始终为 0 即可large-arc-flag
:决定弧线是大于还是小于 180 度,0 表示小角度弧,1 表示大角度弧,由于border-radius 其实都是 90 度角,因此我们使用时始终为 0 即可sweep-flag
:表示弧线的方向,0 表示从起点到终点沿逆时针画弧,1 表示从起点到终点沿顺时针画弧x y
:弧线终点坐标
下边是一些示例:
<svg width="325" height="325" xmlns="http://www.w3.org/2000/svg"> <path d="M 80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z" fill="green" /> <path d="M 230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z" fill="red" /> <path d="M 80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z" fill="purple" /> <path d="M 230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z" fill="blue" /> </svg>
效果如下(有颜色区域是最终形状,其他线条是辅助线):
- 给边框加上圆角
上文中,我们已经拿到了 ABCDEFGH 8个点,每一个点其实都会有一个对应的圆弧,因此在绘制边框的时候,我是这样管理 圆弧 和 直线 的,下边是一个点的数据结构:
const A = { x: 100, y: 100, arc: 'A xxxxxx', // 经过该点的圆弧 line: 'L xxxxxx' // 圆弧的结束点到下一个圆弧起点的直线 }
根据这个结构,我再按 ABCDEFGH 的顺序,将每个点的 svg 指令拼接起来,先拼接 圆弧(arc) 再拼接 直线(line)
那么圆弧的指令如何生成呢,我们以一个点来分析:
- 圆弧的起点坐标为
(x, y-radius)
- 终点坐标为
(x+radius, y)
- 半径就是 border-radius 的值
- 弧线方向会有区别,两个内凹圆角是逆时针,其他圆角都是顺时针
有了这些信息,其实一个圆弧的指令就呼之欲出了,我们通过一段代码快速生成(两个为 0 的值上文介绍A指令时有提到,不赘述原因):
enum ESweepFlag { cw = 1, // 顺时针 ccw = 0, // 逆时针 } /** * 生成圆弧svg路径 * @param endX: 圆弧终点x坐标 * @param endY: 圆弧终点y坐标 * @param radius: 圆弧半径 * @param sweepFlag: 顺时针还是逆时针: 1 顺时针、0 逆时针 */ const generatorArc = (endX: number, endY: number, radius: number, sweepFlag: ESweepFlag = ESweepFlag.cw) => { return `A${radius} ${radius} 0 0 ${sweepFlag} ${endX} ${endY}`; }
到这里,我们将 圆弧 和 直线 指令,按 ABCDEFGH 点顺序,先圆弧后直线挨个拼接起来,边框也就画成了。
如何用SVG画一个特定边框(下):https://developer.aliyun.com/article/1480476