在小程序中如何保存自定义二维码图片或海报(此文示例demo代码基于Taro框架)
前言:这一个笔记主要是写在小程序生成宣传海报之类的,然后用户点击保存按钮就可以保存到手机的功能。主要使用的还是canvas元素。小程序也有提供对应的一些API帮助我们实现这个需求。下面上一些代码和自我思路当作一个记录吧(P.S:Taro是一个用于多端高速开发的框架,基于react和TS。可以自行百度或谷歌一下查询相关资料,标签元素和API同样适用原生小程序写法,标签换一下即可) 转载请注明出处谢谢(尊重下我花时间在做笔记呗哈哈哈)
思路:一开始只知道得用canvas标签才能做到产品需求所要的效果,毕竟是自定义化的。但是怎么显示和怎么保存就查了相关资料。加上自己实现需求后的一些想法,大概如下:
1.建议canvas标签在页面onload时候就进行绘画渲染,这是为避免用户首次进入后点击生成保存时canvas标签还没完成导致空白图片
2.既然自定义化就避免不了一些UI图片,这一点就比较麻烦,图片多了可能超出小程序上传代码包大小限制,而且对于一些API也只支持网络图片。所以就直接放在网络服务器上,通过wx.getImageInfo获取对应图片的信息,例如宽高和微信临时链接,这里要注意这个API支持https格式的网络图片,别踩坑了~
3.现在canvas画好了,那怎么保存成图呢?想了想去在文档看过,又回去翻了翻,有个wx.canvasToTempFilePath专为这功能而生,就是把制定的canvasId整个转换成图片格式保存生成临时模板文件链接。那就简单啦,直接调用把参数填好就可以啦
HTML部分:
<View> <Canvas className='poster' canvasId='poster' style='width:355px;height:635px;'></Canvas> </View>
CSS部分就不贴上来了,主要是外层容器给个宽高度和定位。根据需求做浮动或者其他即可。至于style的值是要确定好直接赋值的,这样画布大小和后续内容才能定位
JS部分:(data变量就不放上来了,下面的js中要做demo把一些变量随便换成文字和数值就可以了)
// 绘画Canvas-分享图标题 drawTitle(datas: any) { const cvsCtx = Taro.createCanvasContext('poster', this) // 获取到指定的canvas标签 const grd = cvsCtx.createLinearGradient(0, 0, 0, 635); // 进行绘制背景 grd.addColorStop(0, '#FDFCF9'); // 绘制渐变背景色 grd.addColorStop(1, '#FCF4D4'); // 绘制渐变背景色 cvsCtx.fillStyle = grd; // 赋值给到canvas cvsCtx.fillRect(0, 0, 355, 635); // 绘制canvas大小的举行 let text = datas.desc; // 这是要绘制的文本 const chr = text.split(''); // 这个方法是将一个字符串分割成字符串数组 let temp: any = ''; // 截取文字赋值 let row: any = []; // 把截取后的段落汇合成数组 cvsCtx.font = 'normal lighter 15px sans-serif' // 设置绘制文字样式 cvsCtx.setFillStyle('#941D11') // 设置绘制文字颜色 for (let a = 0; a < chr.length; a++) { // 进行循环遍历看是否超出canvas文字所在宽度 if (cvsCtx.measureText(temp).width < 350) { temp += chr[a]; } else { a--; //这里添加了a-- 是为了防止字符丢失 row.push(temp); temp = ''; } } row.push(temp); if (row.length > 4) { // 如果文字段落大于4行 const rowCut = row.slice(0, 5); // 截取最后一行进行省略号处理 const rowPart = rowCut[4]; let test: any = ''; let empty: any = []; for (var a = 0; a < rowPart.length; a++) { if (cvsCtx.measureText(test).width < 320) { test += rowPart[a]; } else { break; } } empty.push(test); const group = empty[0] + "..." // 这里只显示5行,超出的用...表示 rowCut.splice(4, 1, group); row = rowCut; } let i = 0 // 此处计算文本最终所占高度方便后面元素定位 for (let b = 0; b < row.length; b++) { cvsCtx.fillText(row[b], 11, 80 + b * 25, 333); i = 80 + b * 25 } this.state.canvasTextHeight = i cvsCtx.setFontSize(21) // 文字样式 cvsCtx.setFillStyle('#FB4949') // 文字样式 cvsCtx.fillText(`${datas.rise_info.rise_percent}%`, 160, 41, 70) // 文字样式 this.ImageInfo('https://static.jingzhuan.cn/WeChat/longtou/details-title.png').then(res => { // 绘制网络图片 // 获取画布 const cvsCtx = Taro.createCanvasContext('poster', this) // 重新定位canvas对象,双重保险 // 绘制背景底图 cvsCtx.drawImage(res.path, 0, 15, 149.5, 36) let title = datas.name cvsCtx.setFontSize(18) cvsCtx.setFillStyle('#FFFFFF') cvsCtx.fillText(title, 8, 39, 214) cvsCtx.draw(true) // 进行绘画 }) let redNum = Number(datas.rise_info.rise_num) let greenNum = Number(datas.rise_info.drop_num) let total = redNum + greenNum cvsCtx.setFillStyle('red') cvsCtx.fillRect(57, i + 25, (redNum / total) * 237, 16) cvsCtx.setFillStyle('#00CC66') cvsCtx.fillRect(57 + (redNum / total) * 237, i + 25, (greenNum / total) * 237, 16) cvsCtx.setFontSize(17) cvsCtx.setFillStyle('#FB4949') cvsCtx.fillText(`涨 ${redNum}只`, 55, i + 68, 100) cvsCtx.setFontSize(17) cvsCtx.setFillStyle('#00CC66') cvsCtx.fillText(`跌 ${greenNum}只`, 240, i + 68, 100) cvsCtx.font = 'normal lighter 12px sans-serif' cvsCtx.setFillStyle('#941D11') cvsCtx.fillText('主力净流入', 12, i + 136, 120) cvsCtx.fillText('3日涨幅', 106, i + 136, 120) cvsCtx.fillText('5日涨幅', 201, i + 136, 120) cvsCtx.setTextAlign('left') cvsCtx.fillText('10日涨幅', 292, i + 136, 49) this.ImageInfo('https://static.jingzhuan.cn/WeChat/longtou/list-line.png').then(res => { // 获取画布 const cvsCtx = Taro.createCanvasContext('poster', this) // 绘制背景底图 cvsCtx.drawImage(res.path, 65, i + 168, 227, 4) cvsCtx.draw(true) }) cvsCtx.font = 'normal bold 15px sans-serif'; if (datas.rise_info.main_net_purchase > 0) { cvsCtx.setFillStyle('#FB4949') } else { cvsCtx.setFillStyle('#00CC66') } cvsCtx.fillText(`${datas.rise_info.main_net_purchase > 9999 || datas.rise_info.main_net_purchase < -9999 ? `${(datas.rise_info.main_net_purchase / 10000).toFixed(2)}亿` : `${datas.rise_info.main_net_purchase}万`}`, 12, i + 114, 120) if (datas.rise_info.rise_percent_of_3_day > 0) { cvsCtx.setFillStyle('#FB4949') } else { cvsCtx.setFillStyle('#00CC66') } cvsCtx.fillText(`${datas.rise_info.rise_percent_of_3_day}%`, 104, i + 114, 120) if (datas.rise_info.rise_percent_of_5_day > 0) { cvsCtx.setFillStyle('#FB4949') } else { cvsCtx.setFillStyle('#00CC66') } cvsCtx.fillText(`${datas.rise_info.rise_percent_of_5_day}%`, 199, i + 114, 120) if (datas.rise_info.rise_percent_of_10_day > 0) { cvsCtx.setFillStyle('#FB4949') } else { cvsCtx.setFillStyle('#00CC66') } cvsCtx.setTextAlign('left') cvsCtx.fillText(`${datas.rise_info.rise_percent_of_10_day}%`, 290, i + 114, 49) cvsCtx.draw(true) this.state.drawTitleData = datas } // 绘画Canvas-分享图列表 drawList(datas: any) { let i = this.state.canvasTextHeight const cvsCtx = Taro.createCanvasContext('poster', this) cvsCtx.font = 'normal lighter 12px sans-serif' cvsCtx.setFillStyle('#941D11') cvsCtx.fillText('成份股', 13, i + 208, 120) cvsCtx.fillText('现价', 90, i + 208, 120) cvsCtx.fillText('涨跌幅', 139, i + 208, 120) cvsCtx.fillText('主力净流入', 203, i + 208, 120) cvsCtx.fillText('主力净买%', 281, i + 208, 120) for (let b = 0; b < 3; b++) { cvsCtx.font = 'normal bold 15px sans-serif'; cvsCtx.setFillStyle('#FB4949') cvsCtx.fillText(datas[b].name, 13, i + 242 + b * 38, 120) if (datas[b].new_price > 0) { cvsCtx.setFillStyle('#FB4949') } else { cvsCtx.setFillStyle('#00CC66') } cvsCtx.fillText(datas[b].new_price, 91, i + 242 + b * 38, 120) if (datas[b].rise_percent > 0) { cvsCtx.setFillStyle('#FB4949') } else { cvsCtx.setFillStyle('#00CC66') } cvsCtx.fillText(`${datas[b].rise_percent}%`, 139, i + 242 + b * 38, 120) if (datas[b].main_net_purchase > 0) { cvsCtx.setFillStyle('#FB4949') } else { cvsCtx.setFillStyle('#00CC66') } cvsCtx.fillText(`${datas[b].main_net_purchase > 9999 || datas[b].main_net_purchase < -9999 ? `${(datas[b].main_net_purchase / 10000).toFixed(2)}亿` : `${datas[b].main_net_purchase}万`}`, 208, i + 242 + b * 38, 120) if (datas[b].main_net_purchase_strength > 0) { cvsCtx.setFillStyle('#FB4949') } else { cvsCtx.setFillStyle('#00CC66') } cvsCtx.fillText(`${datas[b].main_net_purchase_strength}%`, 285, i + 242 + b * 38, 120) } let cps = 0 this.ImageInfo(`https://小程序二维码网络链接`).then(data => { cvsCtx.drawImage(data.path, 10, i + 360, 60, 60) cvsCtx.draw(true) }) cvsCtx.font = 'normal lighter 12px sans-serif' cvsCtx.setFillStyle('#941D11') cvsCtx.fillText('长按识别小程序码', 80, i + 385, 120) cvsCtx.fillText('领取更多龙头股!', 80, i + 403, 120) }
上面是画canvas的两个function。看懂一个即可,下面是进行保存和生成的JS
// 获得canvas图片信息 ImageInfo(path: any) { return new Promise((resolve, reject) => { // 采用异步Promise保证先获取到图片信息才进行渲染避免报错 Taro.getImageInfo( { src: path, success: function (res) { resolve(res) }, fail: function (res) { reject(res) } } ) }) } // 保存图片 saveImage(imgSrc: any) { Taro.getSetting({ success() { Taro.authorize({ scope: 'scope.writePhotosAlbum', // 保存图片固定写法 success() { // 图片保存到本地 Taro.saveImageToPhotosAlbum({ filePath: imgSrc, // 放入canvas生成的临时链接 success() { Taro.showToast({ title: '保存成功', icon: 'success', duration: 2000 }) } }) }, fail() { Taro.showToast({ title: '您点击了拒绝微信保存图片,再次保存图片需要您进行截屏哦', icon: 'none', duration: 3000 }) } }) } }) } // 点击保存图片生成微信临时模板文件path save() { const that = this setTimeout(() => { Taro.canvasToTempFilePath({ // 调用小程序API对canvas转换成图 x: 0, // 开始截取的X轴 y: 0, // 开始截取的Y轴 width: 355, // 开始截取宽度 height: 635, // 开始截取高度 destWidth: 1065, // 截取后图片的宽度(避免图片过于模糊,建议2倍于截取宽度) destHeight: 1905, // 截取后图片的高度(避免图片过于模糊,建议2倍于截取宽度) canvasId: 'poster', // 截取的canvas对象 success: function (res) { // 转换成功生成临时链接并调用保存方法 that.saveImage(res.tempFilePath) }, fail: function (res) { console.log('绘制临时路径失败') } }) }, 100) // 延时100做为点击缓冲,可以不用 }
到这里也就结束了。在保存按钮调用save方法即可。因为我做的需求是根据数据生成不同的图而且要展示给用户所能看到的。可能代码就多了点。其实原理大同小异,如果是简单的海报直接让UI把图跟二维码分两张然后直接push在canvas上就好了,那是最快的。如果不想显示出来给用户看点击即保存的猿兄们,请参照我上一个记录随机生成canvas分享图的部分思路。最后上几个我实现的效果图(转载请注明出处谢谢)
第一张图是小程序效果图,第二张图是保存下来后的图片(分别是1倍。2倍和3倍保存)
最简单的:两张图生成的海报
加了点文字效果的:
最后是比较多内容的: