先写wxml,再根据 wxml 来动态地画canvas有诸多好处:提升体验(渲染快)、便于升级迭代(后期瓜皮改需求)、方便维护等。
文章目录
- 先睹为快
- canvas海报
- 1、优雅地授权 (Be elegant.)
- 1.1、封装
- 1.2、使用
- 2、动态获取wxml显示信息
- 3、绘制canvas (Be patient.)
- 3.1、缓存图片
- 3.2、开始绘制
- 3.3、从缓存存入本地
- 踩坑小记
- 绘制出的海报没有二维码
先睹为快
通过本篇博客你可以掌握的东西
canvas海报
“学习一个东西,认知很重要,这有益于把控整体及系统掌握。”
——光
大抵流程也就是按大小标题的顺序来的,可以前往文章目录回顾下。
为防止用户疑惑,下文提到的page
均为页面的实例
总步骤:
// 点击生成海报按钮
async tapSave() {
try {
await page.beforeSave(); // 生成前的校验
await page.askAuth(); // 处理授权
await page.drawPoster(); // 绘制海报
await page.saveFile(); // 保存到本地
} catch (e) {
console.error(e);
}
},
1、优雅地授权 (Be elegant.)
毋庸置疑,canvas绘制海报需要保存到用户本地,因此需要用户的授权;而小程序中需要判断用户是否授权的业务不少,对此功能封装一下可以增强项目的模块性,方便维护。
1.1、封装
/utils/util.js
module.exports = {
getAuthStatus(scopeName) {
return new Promise((resolve, reject) => {
wx.getSetting({
success(res) {
// 已拒绝过,弹设置
if (res.authSetting[`scope.${scopeName}`] === false) {
resolve(false);
// 已同意
} else if (res.authSetting[`scope.${scopeName}`]) {
resolve(true);
} else {
resolve("none");
}
}
});
});
}
}
1.2、使用
海报页面 /pages/spread_poster/spread_poster
中,封装一个判断及授权步骤的方法
const utils = require("../../utils/util");
// ...
// 获取授权
askAuth() {
return new Promise((resolve, reject) => {
utils.getAuthStatus("writePhotosAlbum").then(isAuthed => {
if (isAuthed === "none") {
// 无信息,弹授权
wx.authorize({
scope: "scope.writePhotosAlbum",
success() {
resolve();
},
fail(e) {
page.initAuthStatus();
wx.showToast({
title: "保存需要您的授权哦~",
icon: "none"
});
reject(e);
}
});
} else if (isAuthed) {
// 已同意,直接保存
resolve();
} else {
// 已拒绝过,弹设置
reject(res);
page.openSetting();
}
});
});
},
// 弹出授权设置
openSetting() {
wx.openSetting({
success(e) {
console.log(e);
},
fail(e) {
console.error(e);
}
});
},
2、动态获取wxml显示信息
在wxml内开发完成后,就可以开始绘制canvas了,言之动态即在于此,为了方便取节点,可以对需要查询的节点id取名,方便后面处理(见3.2)
// 获取界面dom节点的位置和尺寸
nodeRect(selector, root) {
if (page.query === undefined) {
page.query = wx.createSelectorQuery();
}
return new Promise((resolve, reject) => {
try {
page.query
.select(selector)
.boundingClientRect(rect => {
if (!root) {
rect.left = rect.left - page.data.originX;
rect.top = rect.top - page.data.originY;
}
resolve(rect);
})
.exec();
} catch (e) {
console.error("获取节点" + selector + "信息失败");
reject(e);
}
});
},
3、绘制canvas (Be patient.)
至此,我们就可以正式开始使用cavans绘制了。
3.1、缓存图片
如果绘制canvas的过程中用到了图片,直接使用URI是不行的,需要先将图片缓存到本地,对此可以封装一个方法:
// 获取任意网络图片的本地url
_getLocalSrc(url) {
console.log("url", url);
return new Promise((resolve, reject) => {
// 保存网络图片到本地缓存
wx.downloadFile({
url,
success(res) {
if (res.statusCode !== 200) {
wx.showToast({
title: "保存二维码到本地失败",
icon: "none"
});
reject(res);
} else {
resolve(res.tempFilePath);
}
},
fail(e) {
reject(e);
}
});
});
},
需要多张图片的情况下,再封装一层
getGoodsNeedImgSrc() {
const { QrcodeUrl, p } = page.data;
return Promise.all([
page._getLocalSrc(p.imageList[0]),
page._getLocalSrc(posterConfig.titleBgUrl),
page._getLocalSrc(QrcodeUrl)
]);
},
3.2、开始绘制
drawGoodsPoster() {
return new Promise(async (resolve, reject) => {
try {
let [
goodsImgSrc,
titleBgSrc,
QrcodeSrc
] = await page.getGoodsNeedImgSrc();
const p = page.data.p;
let ratio = page.ratio;
if (!ratio) {
ratio = 750 / wx.getSystemInfoSync().windowWidth;
page.ratio = ratio;
}
console.log("开始画画");
const ctx = wx.createCanvasContext("myCanvas");
/**
* r:获取到的dom节点的位置大小信息
* 后面覆盖前面
*/
// 画背景
let r = await page.nodeRect("#goods");
// 设原点坐标
page.setData({
originX: r.left,
originY: r.top
});
// console.log('背景rect', r);
const canvasWidth = r.width;
const canvasHeight = r.height;
ctx.setFillStyle(posterConfig.bgColor);
ctx.fillRect(0, 0, r.width, r.height);
// 画商品
r = await page.nodeRect("#goodsImg");
ctx.drawImage(goodsImgSrc, r.left, r.top, r.width, r.height);
// 画标题一
// 标题背景
r = await page.nodeRect("#titleBg");
ctx.drawImage(titleBgSrc, r.left, r.top, r.width, r.height);
ctx.setTextBaseline("top");
// 标题一文字
// ctx.save()
r = await page.nodeRect("#p1-1");
// ctx.font = `sans-serif ${62/ratio}px bold`
ctx.setFontSize(54 / ratio);
ctx.setFillStyle("#fff");
ctx.fillText(p.couponPriceUi + "元", r.left, r.top);
r = await page.nodeRect("#p1-2");
ctx.setFontSize(36 / ratio);
ctx.fillText("券", r.left, r.top);
r = await page.nodeRect("#p1-3");
ctx.setFontSize(54 / ratio);
// ctx.font = `${62/ratio}px sans-serif bold;`
ctx.fillText("+" + p.rebatePriceUi + "元", r.left, r.top);
r = await page.nodeRect("#p1-4");
ctx.setFillStyle("#fff");
ctx.setFontSize(36 / ratio);
ctx.fillText("返利", r.left, r.top);
// ctx.restore()
// 画标题二
// 标题二背景
r = await page.nodeRect("#goodsDetail");
ctx.setFillStyle("#fff");
ctx.fillRect(r.left, r.top, r.width, r.height);
// 标题二文字
r = await page.nodeRect("#p2-1");
// ctx.font = `sans-serif ${28/ratio}px bold`
ctx.setFontSize(28 / ratio);
ctx.setFillStyle("#000");
ctx.fillText(p.couponName, r.left, r.top);
r = await page.nodeRect("#p2-2");
// ctx.font = `${28/ratio} sans-serif`
ctx.setFontSize(28 / ratio);
ctx.fillText(p.goodsName1, r.left, r.top);
r = await page.nodeRect("#p2-3");
ctx.fillText(p.goodsName2, r.left, r.top);
// 画标题三
ctx.setTextBaseline("bottom");
r = await page.nodeRect("#p3-1");
ctx.setFontSize(26 / ratio);
ctx.setFillStyle("#F92E0C");
ctx.fillText("到手价:¥", r.left, r.top + r.height);
r = await page.nodeRect("#p3-2");
ctx.setFontSize(38 / ratio);
ctx.setFillStyle("#F92E0C");
ctx.fillText(p.showPriceIntUi, r.left, r.top + r.height);
r = await page.nodeRect("#p3-3");
ctx.setFontSize(26 / ratio);
ctx.setFillStyle("#F92E0C");
ctx.fillText(p.showPriceFloatUi, r.left, r.top + r.height);
r = await page.nodeRect("#p3-4");
ctx.setFontSize(26 / ratio);
ctx.setFillStyle("#999");
ctx.fillText(p.marketPriceUi, r.left, r.top + r.height);
// 画删除线
ctx.fillRect(r.left, r.top + r.height / 2, r.width, 1);
r = await page.nodeRect("#p3-5");
ctx.setFontSize(26 / ratio);
ctx.setFillStyle("#999");
ctx.fillText(p.saleTextUi, r.left, r.top + r.height);
// 画tips
ctx.setTextBaseline("top");
r = await page.nodeRect("#p4-1");
ctx.setTextAlign("center");
ctx.setFontSize(30 / ratio);
ctx.setFillStyle("#fff");
ctx.fillText(
page.data.QrcodeText1,
r.left + r.width / 2,
r.top
);
r = await page.nodeRect("#p4-2");
ctx.setFontSize(30 / ratio);
ctx.fillText(
page.data.QrcodeText2,
r.left + r.width / 2,
r.top
);
// 画二维码
r = await page.nodeRect("#Qrcode");
ctx.save();
ctx.beginPath(); //开始绘制
ctx.arc(
r.width / 2 + r.left,
r.width / 2 + r.top,
r.width / 2,
0,
Math.PI * 2,
false
);
ctx.setLineWidth(0);
// ctx.stroke(); //画空心圆
ctx.closePath();
ctx.clip();
ctx.drawImage(QrcodeSrc, r.left, r.top, r.width, r.height);
ctx.restore(); //恢复之前保存的绘图上下文 恢复之前保存的绘图问下文即状态
console.log("开始绘制...");
await page._drawCanvas({
canvasWidth,
canvasHeight,
ctx
});
wx.hideLoading();
resolve();
} catch (e) {
reject(e);
}
});
},
3.3、从缓存存入本地
_drawCanvas({ canvasWidth = 680, canvasHeight = 1030, ctx }) {
return new Promise((resolve, reject) => {
// 绘制到canvas
ctx.draw(true, () => {
setTimeout(() => {
cb();
}, 300);
const cb = () => {
page.setData({
isCanvasDrawed: true
});
// 将canvas存入本地
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: canvasWidth,
height: canvasHeight,
destWidth: canvasWidth * 2,
destHeight: canvasHeight * 2,
canvasId: "myCanvas",
success(res) {
console.log("canvasToTempFilePath res", res);
if (res.errMsg !== "canvasToTempFilePath:ok") {
reject();
wx.showToast({
title: "保存canvas到缓存失败,请稍后再试",
icon: "none"
});
} else {
resolve();
page.posterPath = res.tempFilePath;
}
},
fail(e) {
console.error(e);
}
});
};
});
});
},
踩坑小记
绘制出的海报没有二维码
- 基于业务需要,海报通常包含小程序二维码,忽略控制二维码加载与绘制海报的运行顺序的话会出现绘制出的海报没有二维码等异常情况,解决方法是将步骤解耦顺序执行,结
imgae
组件的bindload
:
// 生成前校验
beforeSave() {
return new Promise((resove, reject) => {
if (!page.data.isQrcodeLoaded) {
reject("二维码未加载完成");
wx.showToast({
title: "请等待二维码加载哦~",
icon: "none"
});
} else {
resove();
}
});
},
// 二维码image bindload事件
QrcodeLoaded(e) {
page.setData({
isQrcodeLoaded: true
});
},