先来效果图:
根据不同的页面详情生成不同的海报,可以保存到本地或者选择分享出去.

思路:
用户点击生成海报,执行canvas绘制,所以页面需要一个canvas容器,用来放后面绘制的结果,以及生成的图片;考虑到绘制可能需要点时间,可以写一个假的海报样式,先展示给用户,而后保存的时候保存canvas生成即可.
微信小程序部分相关接口:
接口的详情以及参数示例等可以参考微信官方文档;
- wx.downloadFile(Object object): 下载文件资源到本地,返回文件的本地临时路径(本地路径),单词允许下载的最大文件为50MB;
- wx.saveImageToPhotosAlbum(Object object): 保存图片到系统相册;
- wx.getSystemInfo(Object object): 获取系统信息;
- wx.canvasToTempFilePath(Object object, Object this): 把当前画布指定区域的内容导出生成指定大小的图片(在 draw() 回调里调用该方法才能保证图片导出成功)
- wx.createCanvasContext(string canvasId, Object this): 创建 canvas 的绘图上下文 CanvasContext 对象(canvasId就是canvas组件的canvas-id属性); CanvasContext部分相关绘制指令(参数太多了,还是去官方文档去看吧):
名称 | 作用 |
---|---|
ctx.drawImage() | 绘制图像到画布 |
ctx.save() | 保存绘图上下文 |
ctx.beginPath() | 开始创建一个路径,需要调用 fill 或者 stroke 才会使用路径进行填充或描边 |
ctx.rect() | 创建一个矩形路径 |
ctx.setLineJoin() | 设置线条的交点样式 |
ctx.setLineWidth() | 设置线条的宽度 |
ctx.arc() | 创建一条弧线(可以画圆) |
ctx.clip() | 从原始画布中裁剪出来一块,后面绘图的内容不会超出裁剪出来的区域(可以在裁剪前先用save方法保存当前画布区域,后面可以通过restore方法恢复) |
ctx.restore() | 恢复之前保存的绘图上下文 |
ctx.setFontSize() | 设置字体的字号 |
ctx.setFillStyle() | 设置填充色 |
ctx.fillText() | 在画布上绘制被填充的文本 |
ctx.draw() | 将之前在绘图上下文中的描述(路径、变形、样式)画到canvas中 |
实现步骤:
- 放一个canvas容器,canvas-id用于后面的标识,以及海报样式;
# detail.wxml
<canvas class="sharePoster" canvas-id="poster"></canvas>
<view>
<view>这里写了一个假的海报样式,展示给用户看</view>
<view class="sava-img" catchtap='savePoster'>保存到本地</view>
</view>
- 用户点击生成海报,获取海报的详细信息,并且开始下载对应的网络图片资源,然后绘制canvas并保存图片;
const app = getApp();
getPosterInfo(id) {
let _this = this;
app.vote.getPosterInfo({id: id}).then(res => {
//存入相关信息以及展示相应的状态
this.setData({
shareFlag: false,
sharePoster: true,
shareInfo: res.data
});
//下载对应的网络图片资源,并绘画canvas
wx.downloadFile({
url: this.data.shareInfo.url,
success: function (res) {
_this.setData({
shareImg: res.tempFilePath
});
}
})
//设置延时器,确保图片下载完成
setTimeout(() => {
wx.getSystemInfo({
success: function (res) {
var v = 750 / res.windowWidth; //获取手机比例
_this.drawPoster(v); //开始绘制canvas
}
});
}, 1000);
});
}
绘制canvas, 代码较长,可以不用细看:
drawPoster(v) { //v-- 手机比例 ratio -- 像素比例
let ratio = 0.5;
let _this = this;
//创建 canvas 的绘图上下文CanvasContext 对象
let ctx = wx.createCanvasContext("poster", this);
//绘制背景图片
ctx.drawImage(this.data.imgs, 0, 0, 630 / v, 812 / v);
ctx.save();
ctx.beginPath();
ctx.save();
// 圆的圆心的 x 坐标和 y 坐标,48 / v 是半径,后面的两个参数就是起始弧度和结束弧度,这样就能画好一个圆
ctx.arc(78 / v, 78 / v, 48 / v, 0, 2 * Math.PI);
ctx.clip();
//绘制头像
ctx.drawImage(this.data.userImg, 30 / v, 31 / v, 96 / v, 96 / v);
ctx.restore();
ctx.setFontSize(30 / v);
ctx.setFillStyle("white");
ctx.fillText('正在参赛...', 150 / v, 65 / v);
ctx.restore(); //恢复限制
ctx.rect(30 / v, 157 / v, 570 / v, 380 / v);
ctx.setLineJoin = "round";
ctx.setLineWidth = 20 / v;
//作品图片
ctx.drawImage(worksImg, 30 / v, 157 / v, 570 / v, 380 / v);
//作品名称
ctx.setFontSize(40 / v);
ctx.setFillStyle("white");
ctx.fillText('作品名称', 30 / v, 598 / v, 560 / v);
ctx.save();
ctx.setFontSize(36 * ratio);
ctx.setFillStyle("white");
//可以尝试切割字符串,循环数组,达到换行的效果
let info = this.data.shareInfo.opus_content;
let len = 0;
if (info.length > 15 && info.length < 30) {
//两行以内
for (var a = 0; a < 2; a++) {
let content = info.substr(len, 15);
len += 15;
ctx.fillText(content, 30 * ratio, (658 + a * 48) / v);
}
} else if (info.length > 30) {
//超过三行
let con1 = info.substr(len, 15);
let con2 = info.substr(15, 14) + "...";
ctx.fillText(con1, 30 * ratio, 658 / v);
ctx.fillText(con2, 30 * ratio, (658 + 48) / v);
} else {
//就一行
ctx.fillText(info, 30 * ratio, 658 / v);
}
ctx.restore();
//二维码
ctx.save();
ctx.fillRect(0 / v, 740 / v, 630 / v, 235 / v);
ctx.drawImage(this.data.code, 30 / v, 761 / v, 153 / v, 153 / v);
ctx.setFontSize(36 / v);
ctx.setFillStyle("#666666");
ctx.fillText("长按识别二维码", 210 / v, 826 / v);
ctx.fillText("为好友加油,一起参赛!", 210 / v, 877 / v);
ctx.restore();
let width = 375;
let height = 600;
ctx.draw(true, () => {
//仍然是图片保存下来,有些图片是空白的原因
let timer = setTimeout(() => {
//把当前画布指定区域的内容导出生成指定大小的图片
wx.canvasToTempFilePath(
{
x: 0,
y: 0,
width: width,
height: height,
destWidth: width * v,
destHeight: height * v,
canvasId: "poster",
// fileType: 'jpg', //如果png的话,图片存到手机可能有黑色背景部分
success(res) {
//生成成功
_this.setData({
tempImg: res.tempFilePath
});
clearTimeout(timer);
},
fail: res => {
//生成失败
clearTimeout(timer);
}
},
this
);
}, 100);
});
},
- 保存图片到本地,本来以为分分钟,没想到忘记了授权,不可粗心!!!
let _this = this;
wx.saveImageToPhotosAlbum({
filePath: _this.data.tempImg,
success(res) {
wx.showToast(
{
title: "保存成功",
icon: "success"
},
1000
);
_this.unshare();
_this.shareOff(); //关闭海报窗口
},
fail(err) {
//授权问题报错
if (
err.errMsg === "saveImageToPhotosAlbum:fail:auth denied" ||
err.errMsg === "saveImageToPhotosAlbum:fail auth deny" ||
err.errMsg === "saveImageToPhotosAlbum:fail authorize no response"
) {
wx.showModal({
title: "提示",
content: "需要您授权保存相册",
showCancel: false,
success: modalSuccess => {
wx.openSetting({
success(settingdata) {
//授权状态
if (settingdata.authSetting["scope.writePhotosAlbum"]) {
wx.showToast(
{
title: "获取权限成功,再次点击即可保存",
icon: "none"
},
500
);
} else {
wx.showToast(
{
title: "获取权限失败,将无法保存到相册哦~",
icon: "none"
},
500
);
}
},
fail(failData) {
console.log("failData", failData);
}
});
}
});
}
}
});
问题总结:
- 文本多行显示,超出隐藏: canvas好像不能自动截取,所以就自己计算文本长度,自己拼接省略号,然后通过for循环绘制;
- 测试生成的海报在部分手机生成清晰度不足,所以我就又重新设置宽高,计算比例;
- wx.canvasToTempFilePath()需要在draw()的回调里调用该方法才能保证图片导出成功.
- 授权相册的时候,如果一开始用户没有授权成功,后面要用wx.openSetting()去手动开启授权