编程加

《今天吃什么》的实现(下)

2021年3月15日

《〈今天吃什么〉的实现(上)》介绍了 SVG,以及怎么实现《今天吃什么》中的蓝色渐变背景和遮罩。这篇文章继续介绍如何实现候选项部分。

首先处理候选词本体。考虑到 iOS 和 Android 内置字体里没有合适的,我下载了「思源柔黑体」(注意版权,这是一款开源可商用的字体,商业用途一定要使用可商用字体)。

我们把候选词导出成一张长图,可以是 SVG 或者 PNG。公众号 HTML 中不支持元素有 id 属性,所以使用 SVG 会导致阴影效果丢失。这里我演示导出 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>

添加一个平移动画。fromto 中分别有两个数字,代表水平方向的平移量和垂直方向的平移量。

平移初态是零平移,终态是向上平移 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 代替 fromto,指定所有的关键帧。

 <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 的众多能力,否则可以实现得更加简洁优雅,而不是像这样充满黑科技。

编程加公众号