开发移动端项目时,特别是活动页面,免不了会涉及动画。而且好的动画会给产品加分。本文总结了 web 端常见动画实现的方式,希望读者在阅读完本文后,掌握动画开发技巧,更加自信地解决交互设计师提出的需求。
实现 web 端动画有多种方式,常见的有 GIF/APNG,CSS3,JavaScript,Lottie,SVG,Canvas 等。具体在业务中使用何种方式,需要综合考虑实现成本与运行效率。需要对动画进行较多控制的,使用 JavaScript 或 Lottie,其他使用 CSS3 与 GIF/APNG。
在上述的动画方式中,GIF/APNG 无疑最省力。相较于 GIF,APNG 更有优势(参考这篇文章)。简单的场景下,使用 APNG 与 setTimeout 也可以实现流程的控制。不过于在实际使用的时候,注意以下问题:
通常 APNG 图片尺寸较大,最好提前加载。
已缓存的 APNG 图片再次显示时,动画不播放。你可以在链接中通过 query 参数来重新加载图片。Codepen 示例
大家应该已经在项目中广泛应用 CSS3 动画,transition/animation 是动画利器。常见的过渡效果通过 CSS 都可以实现。如果没有思路的话,不妨去 Animate.css 或者 Animista.net 看看,也许答案就在上面。
该属性设置在动画结束后,保持终止状态还是恢复初始状态。经常使用 animation-fill-mode: forwards;
来保持终止状态。
一般情况下,动画的过渡是连续的。通过 steps
函数可以让动画「断断续续」(每次切换一帧),实现帧动画的效果。借助该特性,实现一个简单的倒计时。Codepen 示例
clip-path
属性用于控制元素的显示区域。虽然使用 overflow: hidden;
也可以实现部分效果,但是代码量会增多,且对于多边形的裁剪就无能为力。Codepen 示例
尽可能多地使用 transform
,有需要时使用 will-change
属性。如果同时要对大量的 DOM 元素做动效,或许你应该尝试使用 Canvas 而非 CSS。
Vue 对于动画进行了封装,提供 transition
与 transition-group
组件,过渡/动画用一套 API。Vue 中的过渡两个特性值得关注:
设置 mode 可以同时对离开与出现的元素添加过渡效果。Codepen 示例
当你要给多个元素做动画时,可以使用 transition-group 组件。移动端场景中常见的跑马灯使用该组件实现。Codepen 示例
对于复杂的动画,推荐使用 Lottie,并且动画制作软件 AE 支持导出 Lottie 文件。Lottie 使用方式极其简单,导入 JSON 文件完成动画的创建。
import lottie from 'Lottie-web';
import animData from './animData.json';
const anim = lottie.loadAnimation(animData);
使用 lottie-api ,你甚至可以编辑原有的动画。Codepen 示例
Lottie 相当于使用 JS 来播放动画,你可以对动画的播放速度,次数,帧数,顺序进行精准的控制。事件或者方法参考官方文档,这里不再赘述。
如果 Lottie JSON 文件引入图片资源,要去调整图片的路径,避免出现 404 问题。当然更好的方法是使用自动化工具比如 lottie-loader 来处理 JSON 文件,调整图片路径。在使用了 lottie-loader 的前提下,你可以直接把 JSON 文件当做组件来用。
// 将 JSON 当做 Vue 组件来使用
import MyAnimate from './data.json';
export default {
components: { MyAnimate },
};
遗憾的是, Lottie 官方文档并没有介绍 JSON 中各字段的含义,只能在代码仓库中找到一些 JSON schema 描述文件。下面对部分字段做简要描述,当你有定制化需求时,或许需要:
{
"fr": 20, // 每秒播放的帧数
"ip": 0, // 动画开始的开始帧数
"op": 40, // 动画开始的结束帧数
"w": 700, // 动画内容区域宽度
"h": 500, // 动画内容区域高度
"assets": [{ // 图片资源,为避免图片 404 问题,你可能需要编辑这里
"w": 120, // 图片宽度
"h": 120, // 图片高度
"u": "images/", // 图片路径
"p": "img_0.png"// 图片名称
}]
}
由 fr
, ip
与 op
可知动画播放一个周期需要 2s。通过调用 setSeed(2)
可以将播放时间降为 1s;通过 play(frame)
或者 goToAndPlay(frame)
类似的方法设置动画从某一帧开始播放。
lottie-loader 的原理就是处理了 assets 中的图片路径
借助上文中提到了多种工具,处理简单的动画不在话下。对于稍微复杂的动画,要用 JS 去编写动画逻辑。以下面的动画效果为例,来讲解实现思路。
效果图尺寸较大,可以点击这里预览效果。
通过分析发现,主要的逻辑集中在红包上。红包有停止,暂停两种状态;按照某个频率从顶部还会落下新的红包;按照某个频率移除可视区范围外的红包;移动的过程中旋转红包。编写代码时,为了提高灵活性,最常用的策略是数据抽象与过程抽象。先进行数据抽象,把红包雨整个区域用数据来描述。
// 动画区域
class Stage {
// ...
children: Packet[];
duration: number;
destroyed: boolean;
}
class Packet {
x: number;
y: number;
degree: number;
rotation: 'clockwise' | 'anticlockwise';
status: 'idle' | 'moving' | 'removed';
}
接下来,进行过程抽象,添加方法,让数据动起来。由于存在多种频率不同的动画,把一种动画想象成一个 task,用 async 函数描述过程。
class Stage {
init() {}
// 按照一定的频率添加新的红包
async add() {
while (true) {
if (this.destroyed) return;
await wait(200);
// 执行动画逻辑 ...
}
}
// 与 add 类似,让红包移动,同时做清理工作
async animateAndClear() {}
}
class Packet {
// 设置停止
stop() {}
}
通过前面两步,完成了对整个动效的抽象。此时,动画已经是「运动」的了。接下来都是 View 层的工作。借助我们已经掌握的技能,View 层不难实现。在项目开发过程中,使用的框架是 Vue。如果你想用 Canvas 去实现,数据层可以复用,只需要针对 View 层做适配工作即可。
React 与 Vue3 都提供了对 Hook 的支持,数据或者处理数据的逻辑(Hook)是可以复用的。在基础组件库日益丰富的当下,View 层的工作以组合基础组件为主,相当多的业务逻辑是处理数据。回到刚才的例子,我们将先考虑状态的设计,再剥离状态,类似于对 Lottie/Hook 的简单模仿。毕竟调整数据的配置比编写业务代码成本低得多。
Vue 的封装/mode 小节中的示例如果用 Hook(ReactSpring) 来实现的话,核心代码如下:
import { useTransition, animated } from 'react-spring';
export default function App() {
// ...
const transitions = useTransition(show, null, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
});
return <div>...</div>;
}
完整的代码
从上面的例子可以看出,我们只需要设置好初始与终止状态,然后再与 View 层做绑定,整个动画就完成了,便捷程度堪比 CSS3。在先前 Vue 的实现中,依赖 transition 组件的功能,而 ReactSpring 则是整个过渡的状态提供给你,你决定如何去渲染。
如果后续需要对该场景进行封装,那么大概会这样拆分:
如果对组件不满意,是完全可以基于 Hook 实现新的组件,代码量并不大。
随着 Hook 概念的推广,我们也要逐渐学习掌握它。开发项目过程中,有意识地去思考如何对数据进行抽象,如何更好的管理数据。
本文主要讨论的是 web 端简单的动画实现方式。对于更为复杂的场景,如 HTML 5 游戏,为了性能与开发效率的考量,建议使用 pixi.js 或者 phaser 之类的游戏框架。
货拉拉大前端正在招聘,坐标上海与深圳。如果你对我们所做的事情感兴趣,欢迎加入!
简历可投至:cony.yan@huolala.cn