《〈今天吃什么〉的实现(上)》介绍了 SVG,以及怎么实现《今天吃什么》中的蓝色渐变背景和遮罩。这篇文章继续介绍如何实现候选项部分。
首先处理候选词本体。考虑到 iOS 和 Android 内置字体里没有合适的,我下载了「思源柔黑体」(注意版权,这是一款开源可商用的字体,商业用途一定要使用可商用字体)。
我们把候选词导出成一张长图,可以是 SVG 或者 PNG。公众号 HTML 中不支持元素有 id 属性,所以使用 SVG 会导致阴影效果丢失。这里我演示导出 PNG。
![](figma-candidates@2x.png)
为了适配高分屏,导出的 PNG 需要是 2x(两倍)尺寸。我使用的是 Figma,操作如图。
如果你用的是 Sketch,而且打算导出成 SVG,注意需要选中文字层,选择 Layer > Convert to Outlines,否则会生成 <text> 标签,渲染时依赖本地字体。
假设导出的图片文件名叫 [email protected]
。每个候选项的宽度是 300、高度是 150,所以定义一个 300×150 的 SVG,用 <image> 引入图片,放在 (0, 0)(画布左上角)。
<svg width="300" height="150">
<image x="0" y="0"
width="300" height="1200"
href="[email protected]">
</image>
</svg>
添加一个平移动画。from
和 to
中分别有两个数字,代表水平方向的平移量和垂直方向的平移量。
平移初态是零平移,终态是向上平移 1050(刚好图片底端与画布底端重合,1050+150=1200)。
<svg width="300" height="150">
<image x="0" y="0"
width="300" height="1200"
href="[email protected]">
+ <animateTransform
+ attributeName="transform"
+ type="translate"
+ from="0 0" to="0 -1050"
+ dur="1s" repeatCount="indefinite"
+ />
</image>
</svg>
鼠标点击时动画停止,并且冻结(freeze)在最后的状态:
<svg width="300" height="150">
<image x="0" y="0"
width="300" height="1200"
href="[email protected]">
<animateTransform
attributeName="transform"
type="translate"
from="0 0" to="0 -1050"
dur="1s" repeatCount="indefinite"
+ end="click" fill="freeze"
/>
</image>
</svg>
试着点击下图:
为了方便测试,点击后等待 2 秒会恢复滚动。多试几次,看看有什么问题?
动画会在点击的瞬间冻结,而我们期望动画停止的时候,候选项能够恰好停在画布中间。
唯一能够在「随时停止」时,候选项都恰好在画布中间的方法,是把连续的动画改成离散(discrete)的。用 values
代替 from
和 to
,指定所有的关键帧。
<svg width="300" height="150">
<image x="0" y="0"
width="300" height="1200"
href="[email protected]">
<animateTransform
attributeName="transform"
type="translate"
- from="0 0" to="0 -1050"
+ values="0 0; 0 -150;
+ 0 -300; 0 -450; 0 -600;
+ 0 -750; 0 -900"
+ calcMode="discrete"
dur="1s" repeatCount="indefinite"
end="click" fill="freeze"
/>
</image>
</svg>
虽然动画变成了幻灯片,但是至少在点击后,候选项总能出现在正确的位置了。试着点击下图:
这也太呆了,有没有办法兼得鱼和熊掌?因为公众号页面的限制,不得不使用一些黑科技。
考虑把画布分成 4 层,从后到前分别是:蓝色渐变背景、连续滚动的候选项、再一次蓝色渐变背景、离散滚动的候选项。
初始的时候,将前面两层不透明度设置为 0,只显示后面两层,即蓝色渐变背景和连续滚动的候选项。
<svg width="300" height="150">
<g>
<!-- 蓝色渐变背景 -->
<foreignObject x="0" y="0" width="300" height="150">
<span style="display: block; width: 300px; height: 150px; background: linear-gradient(180deg, #51B4F7 0%, #34A0EA 100%); border-radius: 10px"></span>
</foreignObject>
<!-- 连续滚动的候选项 -->
<image x="0" y="0" width="300" height="1200" href="[email protected]">
<animateTransform
attributeName="transform"
type="translate"
from="0 0" to="0 -1050"
dur="1s" repeatCount="indefinite"
/>
</image>
</g>
<!-- 初始不透明度为 0 -->
<g opacity="0">
<!-- 蓝色渐变背景 -->
<foreignObject x="0" y="0" width="300" height="150">
<span style="display: block; width: 300px; height: 150px; background: linear-gradient(180deg, #51B4F7 0%, #34A0EA 100%); border-radius: 10px"></span>
</foreignObject>
<!-- 离散滚动的候选项 -->
<image x="0" y="0" width="300" height="1200" href="[email protected]">
<animateTransform
attributeName="transform"
type="translate"
values="0 0; 0 -150;
0 -300; 0 -450; 0 -600;
0 -750; 0 -900"
calcMode="discrete"
dur="1s" repeatCount="indefinite"
/>
</image>
</g>
</svg>
在初始的时候,可以看到连续滚动的候选项:
由于最前面两层只是调低了透明度,虽然看不见,但还是能够接收点击事件的。我们可以在点击的时候将不透明度调回 1,显示出最前面两层,同时冻结离散滚动的动画:
<!-- 离散滚动的候选项 -->
<image x="0" y="0" width="300" height="1200" href="[email protected]">
<animateTransform
attributeName="transform"
type="translate"
values="0 0; 0 -150;
0 -300; 0 -450; 0 -600;
0 -750; 0 -900"
calcMode="discrete"
dur="1s" repeatCount="indefinite"
+ end="click" fill="freeze"
+ begin="0;click+3600s"
/>
</image>
+ <set
+ attributeName="opacity"
+ to="1"
+ begin="click"
+ />
</g>
</svg>
begin="0;click+3600s"
是指 0s 和点击后 3600s 之后开始动画,是对安卓 Chrome「串串」BUG 的 workaround(可在《今天吃什么》评论区查看详情)。连续滚动的候选项其实还在滚动,但是由于被前面两层挡住了,视觉上就消失了。
试着点击下图:
最后加上蒙版:
+ <!-- 蒙版 -->
+ <foreignObject x="0" y="0"
+ width="300" height="150"
+ style="pointer-events: none"
+ >
+ <span style="
+ display: block;
+ width: 300px;
+ height: 150px;
+ border-radius: 10px;
+ background: linear-gradient(
+ to bottom,
+ #51b4f7, #47adf200 34%,
+ #3da7ee00 67%, #34a0ea
+ )">
+ </span>
+ </foreignObject>
</svg>
蒙版跟蓝色渐变背景的做法相同。唯一需要注意的点在于,蒙版在最顶层,需要设置 pointer-events: none
,避免影响下层的点击事件。
至此,《今天吃什么》 中的效果就被完全实现了。
不能使用 id 属性直接扼杀了 SVG 的众多能力,否则可以实现得更加简洁优雅,而不是像这样充满黑科技。