前言:在弄「新城名录」这个小程序时,需要将发布的信息内容生成一张带小程序码的海报,方便分享和转发。海报的形式是参照“微信圈子”里面的样式,折腾不了不少时间,也踩了很多坑,故在此记录下来。
首先看看实现效果
此小程序是基于uni-app开发的,也就是vue那套写法,所以将海报的生成逻辑弄成了单独的组件。
整个实现流程大致如下图:
一、初始化基本尺寸
通过wx.getSystemInfo获得窗口信息。
因为canvas上绘制文字单位是px,所以要通过像素比来计算文字大小。
let _ = this
wx.getSystemInfo ({
success (res) {
_.windowInfo.width = _.canvasStyle.width = res.windowWidth
_.windowInfo.height = res.windowHeight
_windowInfo.ratio = res.pixelRatio / 2
// 根据像素比,计算文字的大小
_.canvasStyle.textDf *= _windowInfo.ratio
_.canvasStyle.textSm *= _windowInfo.ratio
}
})
二、获取小程序码
2-1: 生成buffer
获取小程序码看文档就行了没什么可说的:传送门
我是通过云函数获取小程序码的Buffer
// 云函数
app.router('wxacode', async (ctx, next) => {
try {
let rs = await cloud.openapi.wxacode.get({
path: event.path,
width: 100,
is_hyaline: true
})
ctx.body = rs
} catch (err) {
ctx.body = err
}
})
wxacode.createQRCode 和 wxacode.get 两个接口加起来最多可生成10万个小程序码。同一参数的path是一个,不限请求次数。
wxacode.getUnlimited 是无限个小程序码,但path有限制,好像不能带参数还没试过。
2-2: 调用云函数获得buffer并转为base64
wx.cloud.callFunction ({
...
}).then(rs => {
let base64 = wx.arrayBufferToBase64(rs.result.buffer.data || rs.result.buffer)
})
在开发的过程中莫名其妙的小程序码就绘制不出来了,最后发现这里rs.result.buffer对象中,一会有data字段一会没有,别问为什么总之遇到了,最好判断下。
let imgSrc = 'data:image/jpeg;base64,' + base64
这里转换的是没有base64前缀的,若要显示在 <image> 标签上,需要加上前缀。
2-3:获得本地临时链接
用wx.getFileSystemManager().writeFile方法写入到本地。
在绘制完成之后,通过wx.getFileSystemManager().unlink删除临时文件。
let filePath = `${wx.env.USER_DATA_PATH}/wxacode.jpeg`
wx.getFileSystemManager().writeFile({
filePath,
data,
encoding: 'base64'
...
})
三、计算画布的高度
首先看看海报的布局
由图可知画布的高度=图片区域+文字区域+小程序码区域+边距
3-1:计算绘制图片所占的高度
用canvas绘制图片,首先要将图片下载成功后才能绘制。
在使用wx.downloadFile下载图片时,如果遇到错误:downloadFile:fail url not in domain list
那么就要在小程序管理后台中:开发>开发设置>服务器域名 去设置downloadFile合法域名
如果用到了云存储,合法域名就在 云开发>存储 中找到文件的https的下载地址
图片下载完成后,通过wx.getImageInfo来获得图片的尺寸,然后根据数量不同采用不同的排版方式。
在这里获取到图片信息时,就计算好坐标、尺寸暂存起来,等绘制的时候直接使用即可。
3-2:计算绘制文字所占的高度
绘制文字主要的问题是,canvas是没有自动换行的,所以要把文字一个个的取出来,然后计算宽度。
小程序提供了测量文本尺寸信息的接口:CanvasContext.measureText
这个玩意呢不建议用,因为在真机测试时,这个接口的运算速度啊慢得要死,文字一多简直不能玩了。
后面找了个现成的函数来获取文本的宽度。原文链接
同样在这里获取文字宽度信息的同时,将坐标计算好暂存起来,等绘制的时候直接使用即可。
最后是小程序区域的高度,是固定高度100
四、开始绘制
4-1: 获取CanvasContext
首先创建 canvas 的绘图上下文 CanvasContext 对象
注意这里要将this参数带上
在自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 canvas ,如果省略则不在任何自定义组件内查找
let ctx = wx.createCanvasContext ('mycanvas', this)
然后就是按顺序绘制图片、文字、小程序码区域
4-2: 导出图片
绘制完成之后就需要用wx.canvasToTempFilePath,将画布的内容导出成指定大小的图片。
官方文档说了:在 draw() 回调里调用该方法才能保证图片导出成功。
理论上应该是如下这样子:
ctx.draw(true, () => {
wx.canvasToTempFilePath({ ... })
})
但是,这个回调它根本不执行呀!
后面查到的是说:绘制速度太快(what ???) 无法进入canvas.draw的回调函数,需要在外层套个setTimeout。
let _ = this
ctx.draw(true, setTimeout(() => {
wx.canvasToTempFilePath ({
fileType: 'jpg',
canvasId: 'mycanvas',
x: 0,
y: 0,
width: _.canvasStyle.width,
height: _.canvasStyle.height,
success (res) {
_.imgSrc = res.tempFilePath
}
}, _)
}, 300))
在使用wx.canvasToTempFilePath记得把this传入进去,同时最好指定好画布区域的宽高,不然可能存在图片空白的情况。
4-3: 预览图片
绘制的<canvas>节点是隐藏在屏幕外的,真正用于预览的是<image>节点
如图:
为什么不直接用<canvas>来预览?
因为预览的尺寸和canvas的尺寸不一样,所以就要做缩放,将<canvas>标签用css3 transform:scale(.8, .8) 在真机上是没有作用的!!!
所以只能把wx.canvasToTempFilePath导出的图片路径,放到<image>上来做显示。
但是,
导出来的图片有可能存在留白区域,像就是没绘制完,这里就要如4-2所说,把导出写到draw回调中,同时一定要把延时加上。
五、保存图片
当你满怀欣喜的爬完上面的坑,以为调用一下wx.saveImageToPhotosAlbum接口把图片保存完,就大功告成了码?
不存在的!!!
此接口需要用户授权,才能成功将图片保存,而如果用户不小心点了拒绝授权,那么是不是要手动调用下跳转到授权设置页面。
wx.getSetting({
success(res) {
if (!res.authSetting['scope.writePhotosAlbum']) {
wx.authorize({
scope: 'scope.writePhotosAlbum',
...
fail () {
wx.openSetting(...)
}
})
}
}
})
跳转授权?跳转得了屁勒!
翻看wx.openSetting的官方文档,有那么一小撮字告诉你:用户发生点击行为后,才可以跳转打开设置页
所以你还得整个对话框,让用户点一下子。
wx.getSetting({
success(res) {
// 进行授权检测,未授权则进行弹层授权
if (!res.authSetting['scope.writePhotosAlbum']) {
wx.authorize({
scope: 'scope.writePhotosAlbum',
// 拒绝授权时,则进入手机设置页面,可进行授权设置
fail(err) {
wx.showModal({
title: '提示',
content: '需要您授权才能保存到相册',
success: (res) => {
if (res.confirm) {
wx.openSetting({
...
})
}
}
})
}
})
}
}
})
最后附上完整代码地址:https://github.com/yiPian/poster