title: ‘网络错误请重试’,
icon: ‘loading’
})
}
})
})
let avatarurl_width = 60, //绘制的头像宽度
avatarurl_heigth = 60, //绘制的头像高度
avatarurl_x = 28, //绘制的头像在画布上的位置
avatarurl_y = 36; //绘制的头像在画布上的位置
ctx.save(); // 先保存状态 已便于画完圆再用
ctx.beginPath(); //开始绘制
//先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针
ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false);
ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片
这里举个例子说下如何绘制文字,比如我要绘制如下这个“字”,需要动态获取前面字数的总宽度,这样才能设置“字”的x轴坐标,这里我本来是想通过measureText来测量字体的宽度,但是在iOS端第一次获取的宽度值不对,关于这个问题,我还在微信开发者社区提了bug,所以我想用另一个方法来实现,就是先获取正常情况下一个字的宽度值,然后乘以总字数就获得了总宽度,亲试是可以的。
let allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325;
ctx.font = ‘normal normal 30px sans-serif’;
ctx.setFillStyle(‘#ffffff’)
ctx.fillText(‘字’, allReading, 150);
④绘制公众号二维码,和获取头像是一样的,也是先通过接口返回图片网络地址,然后再通过getImageInfo获取公众号二维码图片信息⑤如何绘制小程序码,具体官网文档也给出生成无限小程序码接口,通过生成的小程序可以打开任意一个小程序页面,并且二维码永久有效,具体调用哪个小程序二维码接口有不同的应用场景,具体可以看下官方文档怎么说的,也就是说前端通过传递参数调取后端接口返回的小程序码,然后绘制在画布上(和上面写的绘制头像和公众号二维码一样的)
ctx.drawImage(‘小程序码的本地地址’, x轴, Y轴, 宽, 高)
⑥最终绘制完把canvas画布转成图片并返回图片地址
wx.canvasToTempFilePath({
canvasId: ‘myCanvas’,
success: function (res) {
canvasToTempFilePath = res.tempFilePath // 返回的图片地址保存到一个全局变量里
that.setData({
showShareImg: true
})
wx.showToast({
title: ‘绘制成功’,
})
},
fail: function () {
wx.showToast({
title: ‘绘制失败’,
})
},
complete: function () {
wx.hideLoading()
wx.hideToast()
}
})
⑦保存到系统相册;先判断用户是否开启用户授权相册,处理不同情况下的结果。比如用户如果按照正常逻辑授权是没问题的,但是有的用户如果点击了取消授权该如何处理,如果不处理会出现一定的问题。所以当用户点击取消授权之后,来个弹框提示,当它再次点击的时候,主动跳到设置引导用户去开启授权,从而达到保存到相册分享朋友圈的目的。
// 获取用户是否开启用户授权相册
if (!openStatus) {
wx.openSetting({
success: (result) => {
if (result) {
if (result.authSetting[“scope.writePhotosAlbum”] === true) {
openStatus = true;
wx.saveImageToPhotosAlbum({
filePath: canvasToTempFilePath,
success() {
that.setData({
showShareImg: false
})
wx.showToast({
title: ‘图片保存成功,快去分享到朋友圈吧~’,
icon: ‘none’,
duration: 2000
})
},
fail() {
wx.showToast({
title: ‘保存失败’,
icon: ‘none’
})
}
})
}
}
},
fail: () => { },
complete: () => { }
});
} else {
wx.getSetting({
success(res) {
// 如果没有则获取授权
if (!res.authSetting[‘scope.writePhotosAlbum’]) {
wx.authorize({
scope: ‘scope.writePhotosAlbum’,
success() {
openStatus = true
wx.saveImageToPhotosAlbum({
filePath: canvasToTempFilePath,
success() {
that.setData({
showShareImg: false
})
wx.showToast({
title: ‘图片保存成功,快去分享到朋友圈吧~’,
icon: ‘none’,
duration: 2000
})
},
fail() {
wx.showToast({
title: ‘保存失败’,
icon: ‘none’
})
}
})
},
fail() {
// 如果用户拒绝过或没有授权,则再次打开授权窗口
openStatus = false
console.log(‘请设置允许访问相册’)
wx.showToast({
title: ‘请设置允许访问相册’,
icon: ‘none’
})
}
})
} else {
// 有则直接保存
openStatus = true
wx.saveImageToPhotosAlbum({
filePath: canvasToTempFilePath,
success() {
that.setData({
showShareImg: false
})
wx.showToast({
title: ‘图片保存成功,快去分享到朋友圈吧~’,
icon: ‘none’,
duration: 2000
})
},
fail() {
wx.showToast({
title: ‘保存失败’,
icon: ‘none’
})
}
})
}
},
fail(err) {
console.log(err)
}
})
}
总结
至此所有的步骤都已实现,在绘制的时候会遇到一些异步请求后台返回的数据,所以我用promise和async和await进行了封装,确保导出的图片信息是完整的。在绘制的过程确实遇到一些坑的地方。比如初开始导出的图片比例大小不对,还有用measureText测量文字宽度不对,多次绘制(可能受网络原因)有时导出的图片上的文字颜色会有误差等。如果你也遇到一些比较坑的地方可以一起探讨下做个记录,下面附下完整的代码
import regeneratorRuntime from ‘…/…/utils/runtime.js’ // 引入模块
const app = getApp(),
api = require(‘…/…/service/http.js’);
var ctx = null, // 创建canvas对象
canvasToTempFilePath = null, // 保存最终生成的导出的图片地址
openStatus = true; // 声明一个全局变量判断是否授权保存到相册
// 获取微信公众号二维码
getCode: function () {
return new Promise(function (resolve, reject) {
api.fetch(‘/wechat/open/getQRCodeNormal’, ‘GET’).then(res => {
console.log(res, ‘获取微信公众号二维码’)
if (res.code == 200) {
console.log(res.content, ‘codeUrl’)
resolve(res.content)
}
}).catch(err => {
console.log(err)
})
})
},
// 生成海报
async createCanvasImage() {
let that = this;
// 点击生成海报数据埋点
that.setData({
generateId: ‘点击生成海报’
})
if (!ctx) {
let codeUrl = await that.getCode()
wx.showLoading({
title: ‘绘制中…’
})
let code = new Promise(function (resolve) {
wx.getImageInfo({
src: codeUrl,
success: function (res) {
resolve(res.path)
},
fail: function (err) {
console.log(err)
wx.showToast({
title: ‘网络错误请重试’,
icon: ‘loading’
})
}
})
})
let headImg = new Promise(function (resolve) {
wx.getImageInfo({
src: ${app.globalData.baseUrl2}${that.data.currentChildren.headImg}
,
success: function (res) {
resolve(res.path)
},
fail: function (err) {
console.log(err)
wx.showToast({
title: ‘网络错误请重试’,
icon: ‘loading’
})
}
})
})
Promise.all([headImg, code]).then(function (result) {
const ctx = wx.createCanvasContext(‘myCanvas’)
console.log(ctx, app.globalData.ratio, ‘ctx’)
let canvasWidthPx = 690 * app.globalData.ratio,
canvasHeightPx = 1085 * app.globalData.ratio,
avatarurl_width = 60, //绘制的头像宽度
avatarurl_heigth = 60, //绘制的头像高度
avatarurl_x = 28, //绘制的头像在画布上的位置
avatarurl_y = 36, //绘制的头像在画布上的位置
codeurl_width = 80, //绘制的二维码宽度
codeurl_heigth = 80, //绘制的二维码高度
codeurl_x = 588, //绘制的二维码在画布上的位置
codeurl_y = 984, //绘制的二维码在画布上的位置
wordNumber = that.data.wordNumber, // 获取总阅读字数
// nameWidth = ctx.measureText(that.data.wordNumber).width, // 获取总阅读字数的宽度
// allReading = ((nameWidth + 375) - 325) * 2 + 380;
// allReading = nameWidth / app.globalData.ratio + 325;
allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325;
console.log(wordNumber, wordNumber.toString().length, allReading, ‘获取总阅读字数的宽度’)
ctx.drawImage(‘/img/study/shareimg.png’, 0, 0, 690, 1085)
ctx.save(); // 先保存状态 已便于画完圆再用
ctx.beginPath(); //开始绘制
//先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针
ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false);
ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片
ctx.restore(); //恢复之前保存的绘图上下文状态 可以继续绘制
ctx.setFillStyle(‘#ffffff’); // 文字颜色
ctx.setFontSize(28); // 文字字号
ctx.fillText(that.data.currentChildren.name, 103, 78); // 绘制文字
ctx.font = ‘normal bold 44px sans-serif’;
ctx.setFillStyle(‘#ffffff’); // 文字颜色
ctx.fillText(wordNumber, 325, 153); // 绘制文字
ctx.font = ‘normal normal 30px sans-serif’;
ctx.setFillStyle(‘#ffffff’)
ctx.fillText(‘字’, allReading, 150);
ctx.font = ‘normal normal 24px sans-serif’;
ctx.setFillStyle(‘#ffffff’); // 文字颜色
ctx.fillText(‘打败了全国’, 26, 190); // 绘制文字
ctx.font = ‘normal normal 24px sans-serif’;
ctx.setFillStyle(‘#faed15’); // 文字颜色
ctx.fillText(that.data.percent, 154, 190); // 绘制孩子百分比
ctx.font = ‘normal normal 24px sans-serif’;
ctx.setFillStyle(‘#ffffff’); // 文字颜色
ctx.fillText(‘的小朋友’, 205, 190); // 绘制孩子百分比
ctx.font = ‘normal bold 32px sans-serif’;
ctx.setFillStyle(‘#333333’); // 文字颜色
ctx.fillText(that.data.singIn, 50, 290); // 签到天数
ctx.fillText(that.data.reading, 280, 290); // 阅读时长
ctx.fillText(that.data.reading, 508, 290); // 听书时长
// 书籍阅读结构
ctx.font = ‘normal normal 28px sans-serif’;
ctx.setFillStyle(‘#ffffff’); // 文字颜色
ctx.fillText(that.data.bookInfo[0].count, 260, 510);
ctx.fillText(that.data.bookInfo[1].count, 420, 532);
ctx.fillText(that.data.bookInfo[2].count, 520, 594);
ctx.fillText(that.data.bookInfo[3].count, 515, 710);
ctx.fillText(that.data.bookInfo[4].count, 492, 828);
ctx.fillText(that.data.bookInfo[5].count, 348, 858);
ctx.fillText(that.data.bookInfo[6].count, 212, 828);
ctx.fillText(that.data.bookInfo[7].count, 148, 726);
ctx.fillText(that.data.bookInfo[8].count, 158, 600);
ctx.font = ‘normal normal 18px sans-serif’;
ctx.setFillStyle(‘#ffffff’); // 文字颜色
ctx.fillText(that.data.bookInfo[0].name, 232, 530);
ctx.fillText(that.data.bookInfo[1].name, 394, 552);
ctx.fillText(that.data.bookInfo[2].name, 496, 614);
ctx.fillText(that.data.bookInfo[3].name, 490, 730);
ctx.fillText(that.data.bookInfo[4].name, 466, 850);
ctx.fillText(that.data.bookInfo[5].name, 323, 878);
ctx.fillText(that.data.bookInfo[6].name, 184, 850);
ctx.fillText(that.data.bookInfo[7].name, 117, 746);
ctx.fillText(that.data.bookInfo[8].name, 130, 621);
ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 绘制头像
ctx.draw(false, function () {
// canvas画布转成图片并返回图片地址
wx.canvasToTempFilePath({
canvasId: ‘myCanvas’,
success: function (res) {
canvasToTempFilePath = res.tempFilePath
that.setData({
showShareImg: true
})
console.log(res.tempFilePath, ‘canvasToTempFilePath’)
wx.showToast({
title: ‘绘制成功’,
})
},
fail: function () {
wx.showToast({
title: ‘绘制失败’,
})
},
complete: function () {
wx.hideLoading()
wx.hideToast()
}
})
})
})
}
},
// 保存到系统相册
学习分享,共勉
Android高级架构师进阶之路
题外话,我在阿里工作多年,深知技术改革和创新的方向,Android开发以其美观、快速、高效、开放等优势迅速俘获人心,但很多Android兴趣爱好者所需的进阶学习资料确实不太系统,完整。今天我把我搜集和整理的这份学习资料分享给有需要的人
- Android进阶知识体系学习脑图
- Android进阶高级工程师学习全套手册
- 对标Android阿里P7,年薪50w+学习视频
- 大厂内部Android高频面试题,以及面试经历
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
tion () {
wx.showToast({
title: ‘绘制失败’,
})
},
complete: function () {
wx.hideLoading()
wx.hideToast()
}
})
})
})
}
},
// 保存到系统相册
学习分享,共勉
Android高级架构师进阶之路
题外话,我在阿里工作多年,深知技术改革和创新的方向,Android开发以其美观、快速、高效、开放等优势迅速俘获人心,但很多Android兴趣爱好者所需的进阶学习资料确实不太系统,完整。今天我把我搜集和整理的这份学习资料分享给有需要的人
- Android进阶知识体系学习脑图
[外链图片转存中…(img-1SFFGKGR-1715258286409)]
- Android进阶高级工程师学习全套手册
[外链图片转存中…(img-3h9gDL7x-1715258286410)]
- 对标Android阿里P7,年薪50w+学习视频
[外链图片转存中…(img-7Ttn015k-1715258286411)]
- 大厂内部Android高频面试题,以及面试经历
[外链图片转存中…(img-Z45M9x2r-1715258286411)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!