今年谷歌发布了 Android 12(此文写于2021年10月),同时带来了 Material You 设计语言。2014 年夏天谷歌发布了 Android 5.0, 并将 Material Design 作为 Android 的设计语言,Material You 是其第三次重大更新。自从 7 年前那个夏天,我成为了 Material Design 粉丝。在前端领域有非常多实现 Material Design 的框架,比如 mui。本人也使用这个框架做了个浏览器新标签页扩展。OK,不扯远了,待会人都跑光了。说回主题,此次新系统除了带来新的设计语言也带来了全新设计的小组件。我非常喜欢效果图上名为 Scallop 的扇贝时钟小组件(见下图),如果能实现一下,将来还可以放进我的浏览器扩展里。不过相关效果图一直比较少,没法知道具体 UI 信息,就一直鸽着。直到最近正式版发布,网上的 Android 12 截图渐渐多起来,我才开始新建文件夹。
Pixel 6 Pro
Scallop Clock Widget
Code & Demo
配合 Demo 观看效果更佳。
实现
首先做点需求分析:
- 整体看过去最显眼的是表盘,一个边缘带锯齿的圆。再细看可以发现表盘其实是一个正 12 角星,刚好每个顶角对应普通表盘上的数字 1-12。顶角加了圆角效果,整体看起来像一个扇贝,形如其名。
- 表盘中间有两根指针,深色较短的是时针,浅色较长的是分针。指针都偏粗,两端是半圆的圆角。
- 表盘的边缘分别有一个圆点表示的秒,以及和该点关于表盘中心对称的日期,日期带有圆弧效果。
看到这里,可能有小伙伴觉得这也没啥特别的,分分钟写出来,一眼看穿水文的本质,还极其啰嗦。但本着水文不犯法的想法,我决定还是水一水罢。
言归正传,这个小组件看起来非常圆润、抽象且新奇,小别致还挺东西的。下面说说实现步骤:
-
首先选择技术方案。目前前端画钟表的技术很多,像 canvas、div + css 和 svg 等。 canvas,灵活性高,容易实现;缺点是基于像素,需要考虑高分屏的缩放问题,另外如果动态调整画布大小需要实时绘制。div + css,轻松应对调整大小和高分屏;缺点是非常规图形可能有点呛。 svg,能搞定非常规图形和缩放问题;缺点是上手有点门槛。因为小组件风格够扁平也有点非常规,所以我选择了 div + css + svg 的方案,无懈可击;
-
确定方案后,第一个问题就是怎么画那个正 12 角星的扇贝表盘。表盘只能选择使用 svg,考虑到自己算这个形状的路径将耗费大量时间,绝对不是懒得算,我想到了借用设计软件。刚好在 Figma 上有个五角星的基本形状,添加到画布后修改角的数量为 12,设置形状宽高各为100px,再调整圆角大小和半径直到和效果图一致。在左侧资源列表里右键复制 svg,粘贴到代码里,设置填充色,当当,一个扇贝壳就做好了。详细参数如下:
-
然后用两个 div 绘制时针和分针。设置为绝对定位,高 8px,宽分别为 27px 和 36px,圆角 8px,左和上定位 50%。此外位移(-4px, -50%) 让圆角中心和表盘中心处于一点。此时两指针都处于 3 的位置。此时还要设置
transform-origin: 4px center,也就是以表盘中心做旋转。为什么初始指针不设置成指向 12 点方向呢?因为横向的指针的定位和 transform-origin 的值是一致的,旋转指针相对简单些; -
再用一个 div 绘制秒,宽和高各为 9px,定位与上面相同,圆角 50%,位移
(35px, -50%)让它处于表盘边缘,此时位于 3 的位置,设置transform-origin: -35px, center以表盘中心做旋转; -
设置时针、分针和秒的背景色。此时这个组件已经初具雏形:
-
现在组件上的元素只剩下日期了,与秒关于表盘中心对称。日期是星期和日组成的文字,而且是弧形的效果,与表盘的外接圆是同一个圆心。弧形文字目前有几种方式:一种是使用
arctext.js,原理是将每个字符做一个位移和旋转达到弧形效果,但它依赖 jQuery,所以,下一个;第二种是用 canvas,可是 canvas 在最初就被咔嚓了;第三种,svg 也可以绘制弧形文字,就它了。使用textPath指定xlink:href的值为某个path的id,浏览器就会按照这个path绘制文字。因为秒的位置距离圆心 35px,所以我们需要一个半径 radius 为 35px 的圆,让文字和秒处于相同的位置。这里直接用 path 画一个非闭合的圆,起始点在 9 点位置,代码如下。具体原理参考这篇文章 svg - 用 path 画半圆 。path 还需增加一个 id 属性,和 textPath 关联。text 也需要设置transform-origin: 50px 50px,绕表盘中心旋转。此处之所以使用圆路径是为了放下足够多的文字。具体代码见以下:<svg width="100" height="100" view-box="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <!-->表盘<--> <path d="M46.6112 ..." fill="#FFECE7"/> <!-->日期路径<--> <path id="circle" d="M 15 50 a 35 35 0 1 1 0 0.1"></path> <!-->日期文字<--> <text fill="#fff" font-size="9px" style="transform-origin: 50px 50px;"> <textPath xlink:href="#circle">Tue 18</textPath> </text> </svg>
-
这个时候可以看到文字有了弧形效果,但起始点是在表盘上 9 的位置。如何与秒关于表盘中心对称呢。此时秒处于 3 的位置,两者相差 180 度,所以只需要日期文字逆时针旋转所占圆弧的一半就能对称了。那么问题来了,怎么计算呢?经过了解,浏览器并没有提供获取弧形文本长度的方法,唯一的尺寸信息通过 getBoundingClientRect 获取的 layout box 信息,但无法准确计算文字长度。其实可以换个思路,普通文本的绘制从左到右,等同于按照从左到右的直线路径绘制,曲线同样如此,所以文本长度是不变的,变的只是文字的方向。所以通过普通文本的 getBoundingClientRect 得到的 width 就是弧形文本的长度了。接下来计算就很简单了,文字所占角度等于
(width / (Math.PI * radius)) * 180; -
最后就是让大家动起来。在初始化阶段调用 updateTime 函数,在这个函数里获取当前时间的时分秒并算出时分秒指针的角度。时针分为12格,所以一格为 30 度,因为初始位置在 3 所以需要减去 3 校正,需要注意的是,时针的位置也是实时变化的所以需要加上分和秒换算成小时的角度。分针和秒的角度以此类推。最后日期文字的角度为秒的角度减去其所占角度的一半即可。初始化阶段设置一秒一次的定时器,小组件的基本功能就实现完成了。
优化
做到这里,有个不完美的地方,秒的变化是一秒动一次的,比较生硬,不够丝滑。最简单的办法就是给时分秒和日期加上 transition: transform 1s linear;。但是有个 Bug,在秒变为 0 的时候角度突然由大变小,会发生指针逆时针旋转一圈的效果,不符合实际预期。可以通过计数器的方式避免,也就是指针走完一圈就加一,再算角度的时候加上 360 乘以这个数就不会发生角度由大变小的问题,但是不够优雅。而且定时器在浏览器进入后台运行后可能会停止,然后回到前台更新时间后会突然大幅度旋转指针,也不符合预期。
其实可以换一种思路,因为动画是连续的,我们可以在浏览器刷新每一帧的时间里去计算和设置时分秒的角度,这样就得到了动画效果。使用 window.requestAnimationFrame 最合适。这样就解决了 transition 和定时器可能不准的问题,时间误差在毫秒级别也可以忽略不计。
One more thing
如果想要实现表盘毛玻璃效果该怎么做呢?
CSS 提供了一个方便的属性,backdrop-filter。当我们把表盘的 path 的样式设置这个属性时发现并没有效果。也就是说 backdrop-filter 不适用于 svg 元素。难道无法实现了吗?其实 CSS 还提供了一个 clip-path 属性,元素的边缘会按照指定的 svg 路径绘制,我们首先创建一个div,把之前 svg 里的表盘代码复制到对应的样式里,加上宽高和 backdrop-filter,完美解决毛玻璃效果。至此整个小组件就完成了💯。