动画让小程序「活起来」,Canvas 则承担了海报生成、图表绘制等无法用 WXML/CSS 表达的重任。但两者都藏着大量性能陷阱:动画用错属性会掉帧,Canvas 用错 API 在真机上黑屏,大图绘制直接撑爆内存。本文系统梳理 CSS 动画与 wx.createAnimation 的适用边界、手势交互动效、Canvas 2D 与旧版 canvas 差异、海报生成流程、图表库选型与内存优化,帮助开发者写出流畅、稳定、可维护的动效与绘图代码。
一、CSS 动画基础
1.1 transition 与 animation
纯 UI 动效(位移、显隐、渐变)优先使用 CSS,渲染层无需逻辑层参与,性能最好:
/* transition:状态变化时的补间动画 */
.card {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.card.active {
transform: scale(1.05);
opacity: 0.8;
}
/* animation + keyframes:循环/自动播放动画 */
@keyframes pulse {
0% { opacity: 0.4; }
50% { opacity: 1; }
100% { opacity: 0.4; }
}
.live-dot {
animation: pulse 1.6s ease-in-out infinite;
}
1.2 动画属性性能选择
| 属性 | 是否合成层友好 | 说明 |
|---|---|---|
transform | 是 | 位移/缩放/旋转,GPU 合成 |
opacity | 是 | 透明度,合成层属性 |
filter | 部分 | 部分滤镜可合成,注意兼容 |
width/height/top/left | 否 | 触发布局重排,避免动画 |
一句话:动画只动
transform与opacity,避免动width/height/left/top,这是不掉帧的第一原则。
二、wx.createAnimation(旧式动画)
需要程序化控制动画(如旋转、序列化逐帧推进)时,用 wx.createAnimation,通过 setData 把动画数据注入视图:
// pages/card/card.js
Page({
data: { animData: {} },
onRotate() {
const anim = wx.createAnimation({
duration: 600,
timingFunction: 'ease-out',
delay: 0,
transformOrigin: '50% 50%'
});
anim.rotate(360).scale(1.1).step(); // 定义动画序列
this.setData({ animData: anim.export() }); // 导出动画数据
}
});
<!-- pages/card/card.wxml -->
<view class="card" animation="{{animData}}" bindtap="onRotate">卡片</view>
适用场景:旋转奖品转盘、缩放弹层、连续 step 链式动画。注意其本质仍是「逻辑层计算 → 注入数据 → 渲染层执行」,高频使用会带来 setData 开销,复杂长动画建议交给 CSS。
一句话:
wx.createAnimation适合「程序化、链式、需要 JS 参与计算」的动画;纯 UI 动效应优先用 CSS,两者是互补关系。
三、交互动效:手势与转场
3.1 手势驱动动画
触摸手势(滑动、捏合)要求动画参数跟随手指实时变化,常见做法是监听触摸事件计算位移/缩放:
// 图片缩放:双指捏合
Page({
data: { scale: 1, baseScale: 1, touches: [] },
onTouchStart(e) {
this.data.baseScale = this.data.scale;
this.data.touches = e.touches;
},
onTouchMove(e) {
if (e.touches.length === 2) {
// 计算两指距离变化 → 更新缩放值
const dist = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
const ratio = dist / this.data.initDist;
this.setData({ scale: this.data.baseScale * ratio });
}
}
});
3.2 页面转场与过渡
- 原生页面跳转的默认滑动转场不可深度定制;需要自定义转场(淡入、放大进入)时,可在目标页用 CSS 过渡实现进场动画。
- 组件显隐切换优先用
transition配合wx:if/hidden控制,避免生硬闪现。
<!-- 弹层淡入示例 -->
<view class="mask" wx:if="{{visible}}" bindtap="close">
<view class="dialog" style="transition: transform .3s, opacity .3s;">
内容
</view>
</view>
一句话:手势动效的关键是「参数由触摸数据实时驱动」,而转场动效的关键是「进场/退场分开设计」,二者都尽量走合成层属性。
四、Canvas 新老 API 对比
小程序 Canvas 有两代 API,务必分清:
| 维度 | 旧版 Canvas | Canvas 2D |
|---|---|---|
| 获取方式 | wx.createCanvasContext(id) | type="2d" + query.select().fields({node}) |
| 绘制模型 | 命令缓冲 + ctx.draw() 提交 | 标准 CanvasRenderingContext2D |
| 高清屏适配 | 需手动 scale | 需手动按 dpr 设置画布尺寸 |
| 性能 | 一般 | 更优,支持离屏 Canvas |
| 推荐 | 兼容旧基础库 | 新项目默认选择 |
<!-- 新版 Canvas 2D -->
<canvas type="2d" id="chartCanvas" style="width: 100%; height: 400px;"></canvas>
// 获取 Canvas 2D 节点并做高清屏适配
async function getCanvas(id) {
const query = wx.createSelectorQuery();
const res = await new Promise((resolve) => {
query.select('#' + id).fields({ node: true, size: true }).exec((data) => {
resolve(data[0] || null);
});
});
if (!res) return null;
const canvas = res.node;
const dpr = wx.getWindowInfo().pixelRatio;
canvas.width = res.width * dpr; // 画布物理像素 = CSS 宽 × dpr
canvas.height = res.height * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr); // 坐标系对齐 CSS 像素
return { canvas, ctx, width: res.width, height: res.height };
}
一句话:新项目一律用 Canvas 2D,并按
dpr缩放画布,否则高分屏上绘制结果会模糊;旧版wx.createCanvasContext仅用于兼容老基础库。
4.1 组件内获取 Canvas 节点
在自定义组件中获取 Canvas 节点,必须使用 wx.createSelectorQuery().in(this) 指定查询上下文,否则会查询不到组件内的节点:
// components/share-card/index.js
Component({
methods: {
async getCanvas(id) {
const query = wx.createSelectorQuery().in(this); // 关键:组件内查询
const res = await new Promise((resolve) => {
query.select('#' + id).fields({ node: true, size: true }).exec((d) => resolve(d[0] || null));
});
if (!res) return null;
const canvas = res.node;
const dpr = wx.getWindowInfo().pixelRatio;
canvas.width = res.width * dpr;
canvas.height = res.height * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return { canvas, ctx, width: res.width, height: res.height };
}
}
});
忘记 .in(this) 是组件场景下 Canvas 白屏/拿不到节点的高频原因,排查时优先检查查询上下文。
五、海报生成:绘制 + 保存相册
海报生成是 Canvas 最典型业务(分享裂变、订单截图、会员卡)。流程为「异步加载图片 → 绘制 → 导出临时图 → 保存相册」。
5.1 绘制海报
// 绘制分享海报
async function drawPoster() {
const { ctx, width, height } = await getCanvas('posterCanvas');
// 1. 背景
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
// 2. 异步加载图片(必须先 await 再 drawImage,否则白屏)
const img = await loadImage('https://cdn.example.com/bg.jpg');
ctx.drawImage(img, 0, 0, width, height);
// 3. 文字(换行需自行处理)
ctx.fillStyle = '#333333';
ctx.font = '16px sans-serif';
ctx.fillText('扫码了解详情', 24, 60);
}
// 图片加载工具:兼容不同平台
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = wx.createImage(); // Canvas 2D 环境下的图片对象
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('图片加载失败:' + src));
img.src = src;
});
}
5.2 导出并保存相册
// 导出 Canvas 为临时图片
function exportPoster(canvas) {
return new Promise((resolve, reject) => {
wx.canvasToTempFilePath({
canvas, // Canvas 2D 传入节点对象
fileType: 'png',
quality: 1,
success: (res) => resolve(res.tempFilePath),
fail: reject
});
});
}
// 保存到相册(需 scope.writePhotosAlbum 授权)
async function saveToAlbum(filePath) {
try {
await wx.saveImageToPhotosAlbum({ filePath });
wx.showToast({ title: '已保存到相册' });
} catch (err) {
if (err.errMsg.includes('auth')) {
// 引导用户去设置页开启相册权限
wx.showModal({
title: '需要相册权限',
content: '请在设置中开启「保存到相册」权限',
confirmText: '去设置',
success: () => wx.openSetting()
});
}
}
}
一句话:海报生成三个最容易翻车的点是「图片没加载完就 drawImage(白屏)」「高清屏没按 dpr 缩放(模糊)」「保存相册未处理授权拒绝」。
六、数据可视化:图表库选型
小程序图表需要 Canvas 承载(DOM 图表库无法直接使用),主流方案:
| 库 | 底层 | 特点 | 适用 |
|---|---|---|---|
| ECharts(ec-canvas) | Canvas 2D/旧版 | 功能全、生态大 | 复杂图表、交互图表 |
| F2(f2-canvas) | Canvas | 移动端优先、体积小 | 移动端常规图表 |
| wx-charts | Canvas | 轻量、API 简单 | 简单柱/折线/饼图 |
| 自绘 | Canvas 2D | 完全可控、零依赖 | 单图表、固定样式 |
// ec-canvas 使用示例
const echarts = require('../../ec-canvas/echarts');
function initChart(canvas, width, height, dpr) {
const chart = echarts.init(canvas, null, { width, height, devicePixelRatio: dpr });
canvas.setChart(chart);
chart.setOption({
xAxis: { type: 'category', data: ['周一', '周二', '周三'] },
yAxis: { type: 'value' },
series: [{ type: 'line', data: [120, 200, 150] }]
});
return chart;
}
选型建议:图表简单且数量少用 wx-charts;需要交互、联动、大数据量用 ECharts;只服务特定业务图形(进度环、仪表盘)直接自绘,最省体积。
一句话:图表选型看「交互复杂度 + 包体积预算」:单图自绘、常规图轻量库、复杂交互上 ECharts,别一上来就全家桶。
七、性能与内存注意
Canvas 是内存大户,几个高频雷区必须规避:
- 图片对象复用:同一张图片只
createImage一次并缓存,避免重复解码。 - 离屏 Canvas:复杂图形先绘制到离屏 Canvas,再一次性 draw 到主画布,减少重绘。
- 及时释放:页面
onUnload时置空引用、取消监听,避免 Canvas 节点被持有导致内存泄漏。 - 避免超大画布:单张海报分辨率不宜过高,按实际导出尺寸绘制,
canvasToTempFilePath的destWidth控制输出尺寸。 getImageData慎重:逐像素读取会瞬时占用巨大内存,大图像素处理务必分批。
// 离屏 Canvas 示例:先画到离屏再合并
async function drawWithOffscreen() {
const { canvas, ctx } = await getCanvas('main');
const off = wx.createOffscreenCanvas({ type: '2d', width: 100, height: 100 });
const octx = off.getContext('2d');
octx.fillStyle = '#ff5500';
octx.arc(50, 50, 40, 0, Math.PI * 2);
octx.fill();
ctx.drawImage(off, 16, 16, 100, 100); // 一次性合并到主画布
}
八、常见问题
- Canvas 真机黑屏:图片未加载完成就绘制;或画布宽高为 0;或
type="2d"节点未拿到。 - 绘制模糊:未按
dpr缩放画布物理尺寸。 - 保存相册失败:检查
scope.writePhotosAlbum授权、临时文件路径有效性、小程序后台「隐私协议」声明。 - ECharts 初始化空白:确认
ec-canvas组件包引入正确、在onReady之后初始化。 - 动画卡顿:动画属性用了布局类属性;或高频
setData驱动动画。
九、总结
| 环节 | 方案 | 关键要点 |
|---|---|---|
| 基础动效 | CSS transition/animation | 只动 transform/opacity |
| 程序化动画 | wx.createAnimation | 链式 step、setData 注入 |
| 交互动效 | 手势驱动 + 转场设计 | 参数跟随触摸实时更新 |
| Canvas | Canvas 2D + dpr 适配 | 新项目默认 2D |
| 海报生成 | 绘制 → 导出 → 保存相册 | 图片加载 + 授权处理 |
| 可视化 | ec-canvas / F2 / wx-charts | 按复杂度选型 |
| 内存优化 | 离屏 Canvas + 资源复用 | 避免解码浪费与大画布 |
动画与 Canvas 是提升小程序体验质感与传播效率的两大利器。动画遵循「合成层属性优先、JS 参与最小化」的准则;Canvas 则贯彻「新 API 2D、高清屏 dpr、图片先加载、内存勤释放」四条铁律。把动效做流畅、把绘图做稳定,小程序就能在感官层面与数据层面同时给用户留下深刻印象。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。