Uniapp绘制海报终极解决方案(微信小程序同样适用)
问题原因与分析
背景介绍
在微信小程序开发过程中,我们发现分享只能发送给好友,并没有“分享到朋友圈的功能”
以下是任意小程序点开分享界面的截图
所以我们通常的做法是绘制一个带小程序二维码的海报,然后保存到本地图库
最后引导用户把该张图片分享至朋友圈,朋友看到后扫码即可进入小程序
以下是腾讯新闻小程序解决方案——绘制海报
今天,我们就要实现将指定图片(网络路径)绘制成为海报并保存到用户本地的功能
坐稳,我们现在出发:
我们首先分析下程序中存在的难点与痛点
- 网络图片需要做本地存储后才能开始绘画
- 【重点】网络存在不稳定性,如何保证绘画顺序和期望一致(因为uniapp对大部分接口都做了Promise封装)
- 绘制好的图片如何保存至用户本地
- 如何处理设备适配问题
针对以上几点,画出设计思路图
设计思路图示
解决方案
万事俱备,撸起袖子开始编码!(以下为编码讲解,文末有完整代码)
-
首先编写模板代码
<template> <view class="content"> <canvas canvas-id="myCanvas"></canvas> <button @click="generateImg">点我</button> </view> </template>
我们首先定义了一个id为myCanvas的画布,用于绘制我们的海报
-
设计绘图队列以及数据结构
data() { return { drawQueue: //画图队列 [ { path: 'https://zftsw-oss.oss-cn-hangzhou.aliyuncs.com/5c354e4a710461580958674035.jpg', x: 0, y: 0, width: 600, height: 300, type: 'image', }, { path: 'https://zftsw-oss.oss-cn-hangzhou.aliyuncs.com/tmp_cb726c3d5765754dc08ba01c6fa8fa522dff4b08c3aad6d4.jpg', x: 520, y: 20, width: 60, height: 60, type: 'image', }, { text: '我是文章标题', size: 32, x: 50, y: 365, type: 'text', }, { text: '扫码关注我哟', size: 24, x: 300, y: 365, type: 'text', }, ], ctx: null, //画布上下文 counter: -1, //计数器 drawPathQueue: [], //画图路径队列 } },computed: { myPx() { return this.screenWidth / 750 }, }
这里简单说一下:drawQueue为待绘制队列,其中path是图片网络路径,xy为坐标,type为区分是绘制图片还是文字,drawPathQueue为处理害的绘图队列,其中每个元素会以index标注绘制顺序
-
在App.vue应用加载时获取当前屏幕宽度(也可以使用uniapp内置api:upx2px)
<script> import Vue from 'vue' export default { onLaunch: function() { console.log('App Launch') uni.getSystemInfo({ success: (res)=> { Vue.prototype.screenWidth = res.screenWidth } }) }, onShow: function() { console.log('App Show') }, onHide: function() { console.log('App Hide') } } </script> <style> /*每个页面公共css */ </style>
-
在页面初始化时加载canvas实例
onLoad() { this.ctx = uni.createCanvasContext('myCanvas', this) }
-
写两个监听器,分别监听处理后的队列生成进度和绘制进度
watch: { drawPathQueue(newVal, oldVal) { /* 所有元素入队则开始绘制 */ if (newVal.length === this.drawQueue.length) { console.log('生成的队列:'+JSON.stringify(newVal)); console.log('开始绘制...') for (let i = 0; i < this.drawPathQueue.length; i++) { for (let j = 0; j < this.drawPathQueue.length; j++) { let current = this.drawPathQueue[j] /* 按顺序绘制 */ if (current.index === i) { /* 文本绘制 */ if (current.type === 'text') { console.log('绘制文本:' + current.text); this.ctx.setFillStyle('#000') this.ctx.setFontSize(current.size * this.myPx) this.ctx.fillText(current.text, current.x * this.myPx, current.y * this.myPx) this.counter-- } /* 图片绘制 */ if (current.type === 'image') { console.log('绘制图片:' + current.path); this.ctx.drawImage(current.path, current.x * this.myPx, current.y * this.myPx, current.width * this.myPx, current.height * this.myPx) this.counter-- } } } } console.log('final counter', this.counter); } }, counter(newVal, oldVal) { if (newVal === 0) { this.ctx.draw() /* draw完不能立刻转存,需要等待一段时间 */ setTimeout(() => { uni.canvasToTempFilePath({ canvasId: 'myCanvas', success: (res) => { console.log('in canvasToTempFilePath'); // 在H5平台下,tempFilePath 为 base64 console.log('图片已保存至本地:', res.tempFilePath) uni.saveFile({ tempFilePath: res.tempFilePath, success: function(res) { console.log('本地保存路径为',res.savedFilePath); } }); } }) }, 1000) } } },
-
最后添加生成点击事件
generateImg() { this.counter = this.drawQueue.length this.drawPathQueue = [] /* 将图片路径取出放入绘图队列 */ for (let i = 0; i < this.drawQueue.length; i++) { let current = this.drawQueue[i] current.index = i /* 如果是文本直接放入队列 */ if (current.type === 'text') { this.drawPathQueue.push(current) continue } /* 图片需获取本地缓存path放入队列 */ uni.getImageInfo({ src: current.path, success: (res) => { current.path = res.path this.drawPathQueue.push(current) } }) } /* setTimeout(()=>{ console.log(JSON.stringify(this.drawPathQueue)); },10000) */ }
-
调整样式并测试
<style lang="scss"> page { .content { canvas { width: 600rpx; height: 450rpx; border: 1px solid black; margin: 0 auto; } } } </style>
-
效果图
因为只是demo,图片没有认真找,随意放了几张图片,只要将队列中的图片换成自己想要的图片和小程序二维码即可
完整代码
以下是本示例index.vue下的完整代码
<template>
<view class="content">
<canvas canvas-id="myCanvas"></canvas>
<button @click="generateImg">点我</button>
</view>
</template>
<script>
export default {
data() {
return {
drawQueue: //画图队列
[
{
path: 'https://zftsw-oss.oss-cn-hangzhou.aliyuncs.com/5c354e4a710461580958674035.jpg',
x: 0,
y: 0,
width: 600,
height: 300,
type: 'image',
},
{
path: 'https://zftsw-oss.oss-cn-hangzhou.aliyuncs.com/tmp_cb726c3d5765754dc08ba01c6fa8fa522dff4b08c3aad6d4.jpg',
x: 520,
y: 20,
width: 60,
height: 60,
type: 'image',
},
{
text: '我是文章标题',
size: 32,
x: 50,
y: 365,
type: 'text',
},
{
text: '扫码关注我哟',
size: 24,
x: 300,
y: 365,
type: 'text',
},
],
ctx: null, //画布上下文
counter: -1, //计数器
drawPathQueue: [], //画图路径队列
}
},
computed: {
myPx() {
return this.screenWidth / 750
},
},
onLoad() {
this.ctx = uni.createCanvasContext('myCanvas', this)
},
watch: {
drawPathQueue(newVal, oldVal) {
/* 所有元素入队则开始绘制 */
if (newVal.length === this.drawQueue.length) {
console.log('生成的队列:'+JSON.stringify(newVal));
console.log('开始绘制...')
for (let i = 0; i < this.drawPathQueue.length; i++) {
for (let j = 0; j < this.drawPathQueue.length; j++) {
let current = this.drawPathQueue[j]
/* 按顺序绘制 */
if (current.index === i) {
/* 文本绘制 */
if (current.type === 'text') {
console.log('绘制文本:' + current.text);
this.ctx.setFillStyle('#000')
this.ctx.setFontSize(current.size * this.myPx)
this.ctx.fillText(current.text, current.x * this.myPx, current.y * this.myPx)
this.counter--
}
/* 图片绘制 */
if (current.type === 'image') {
console.log('绘制图片:' + current.path);
this.ctx.drawImage(current.path, current.x * this.myPx, current.y * this.myPx, current.width * this.myPx,
current.height * this.myPx)
this.counter--
}
}
}
}
console.log('final counter', this.counter);
}
},
counter(newVal, oldVal) {
if (newVal === 0) {
this.ctx.draw()
/* draw完不能立刻转存,需要等待一段时间 */
setTimeout(() => {
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: (res) => {
console.log('in canvasToTempFilePath');
// 在H5平台下,tempFilePath 为 base64
console.log('图片已保存至本地:', res.tempFilePath)
uni.saveFile({
tempFilePath: res.tempFilePath,
success: function(res) {
console.log('本地保存路径为',res.savedFilePath);
}
});
}
})
}, 1000)
}
}
},
methods: {
generateImg() {
this.counter = this.drawQueue.length
this.drawPathQueue = []
/* 将图片路径取出放入绘图队列 */
for (let i = 0; i < this.drawQueue.length; i++) {
let current = this.drawQueue[i]
current.index = i
/* 如果是文本直接放入队列 */
if (current.type === 'text') {
this.drawPathQueue.push(current)
continue
}
/* 图片需获取本地缓存path放入队列 */
uni.getImageInfo({
src: current.path,
success: (res) => {
current.path = res.path
this.drawPathQueue.push(current)
}
})
}
/*
setTimeout(()=>{
console.log(JSON.stringify(this.drawPathQueue));
},10000) */
}
}
}
</script>
<style lang="scss">
page {
.content {
canvas {
width: 600rpx;
height: 450rpx;
border: 1px solid black;
margin: 0 auto;
}
}
}
</style>