项目场景:
- 在uni-app中,通过点击邀请分享海报的方式,可以展示不同的海报,并通过扫描海报上的二维码来实现用户之间的关系绑定,从而实现分销功能;
- 每次生成的海报样式都可能不同,可以根据后台配置的宽度、高度、X坐标和Y坐标的不同,需要灵活调整每个海报中展示二维码的位置。
问题描述
- uni-app中如何使用API生成海报。
- 生成的图片无法正确展示,本地可以看到,真机上看不到。
- 动态改变二维码的位置和大小,位置有所偏差。
- 怎么给头像设置成圆形。
原因分析:
- 当在uni-app中生成海报时,如果使用网络图片,需要使用uni.getImageInfo函数将其转换为本地临时路径。
- 如果后台返回的太阳码是以base64形式的数据,可能会在真机上无法展示,所以需将其转换为本地路径。
- 在canvas中设置的值都是以像素(px)为单位,如果根据750设计图给太阳码设置数值时,需要进行转换,以适应不同屏幕的像素密度。
解决方案:
通过点击“邀请好友返现赚不停”,生成海报,有两种方案去实现以上效果:
- 通过uni-app提供的API,可以实现纯前端的canvas渲染海报,这种方法加载速度快且不需要占用后端资源,但无法通过长按识别二维码和转发海报,只能保存到手机相册再转发给朋友。
- 通过后端来渲染海报,虽然会占用后端资源,但它的优势在于后端直接生成图片。只需在image标签上添加show-menu-by-longpress属性,就可以实现长按识别二维码和直接转发海报给朋友的功能。
本文主要介绍了通过纯前端的canvas生成海报的方式:
1、逻辑梳理:
在展示海报的过程中,首先我们需要使用uView框架提供的popup组件,不过这些细节在这里并不重要,因为下面的JavaScript代码是通用的,只是样式上可能会有些不同,但是你是uni-app的项目,主要步骤如下:
- 首先,我们需要在canvas中展示一张海报图片。
- 然后,根据登录信息获取当前用户的头像和名字。
- 通过接口获取太阳码的数据地址。
- 将获取到的头像、名字和太阳码赋值到对应的位置。
<view class="share-btn">
<button @click="handleShareClick">邀请好友返现赚不停</button>
</view>
<u-popup @close="closePoster" :show="isPosterShow" mode="center" bgColor="transparent" :safeAreaInsetBottom="false"
round="20" :customStyle="{ margin: '0 auto', position: 'relative' }">
<canvas v-if="isPosterShow" :disable-scroll="true" canvas-id="mycanvas"
style="width: 604rpx;height:1080rpx;"></canvas>
<view v-if="isPosterShow" class="poster-btn"><button @click="savaImgLocalClick">保存图片到本地</button></view>
<!-- 这是一个关闭的图标 -->
<!-- <view v-if="isPosterShow" class="poster-close" @click="closePoster">
<image :src="imgs.pclose"></image>
</view> -->
</u-popup>
2、海报的渲染
import { ref } from "vue"
import { onLoad } from "@dcloudio/uni-app"
let isPosterShow = ref<boolean>(false)
pixelRatio.value = device.pixelRatio
onLoad(async (options: any) => {
// 获取当前设备和设计图的比例
let device = uni.getSystemInfoSync()
wid.value = device.windowWidth / 750
pixelRatio.value = device.pixelRatio
})
const handleShareClick = () => {
drawPoster()
}
// 生成海报
const drawPoster = async () => {
isPosterShow.value = false
uni.showLoading({
title: '海报生成中...'
})
let ctx = uni.createCanvasContext("mycanvas")
uni.getImageInfo({
src: '获取你的网络图片地址,接口返回的地址和自己写死一个网络地址都可以',
success: async (imagePoster) => {
// 然后回返回一个本地路径画出来,这个图片的大小和canvas的宽高是一致的,所以坐标从0,0开始;
// drawImage就渲染出海报图了,604 * wid.value 因为我们要转化单位,所以在初始化的时候换算的比例就要在这里用到
ctx.drawImage(imagePoster.path, 0, 0, 604 * wid.value, 1080 * wid.value)
// 加载完后,加载头像和名字
getCanvasAvatar(ctx)
}
})
}
// 获取canvas头像
const getCanvasAvatar = (ctx: any) => {
// userInfo.value.nickname 这是我从本地取出来的数据,这个要替换成你拿的微信昵称;
// 因为微信昵称的名字会很长,在这里我们对拿到的昵称进行一个截取,然后用...取代;
let tip = userInfo.value.nickname.length >= 10 ? userInfo.value.nickname.slice(0, 11) + '...' : userInfo.value.nickname
// userInfo.value.avatar 这是你拿到的头像,也要给到getImageInfo,让它给你转成本地路径,然后进行渲染;
uni.getImageInfo({
src: userInfo.value.avatar,
success(res) {
// 给这个头像画成圆角,这个drawCircleImage方法,我放在下面tool中
drawCircleImage(ctx, res.path, 40, 40, 20)
ctx.setFillStyle("#000") // 为文字设置颜色
ctx.font = "18px PingFang SC-Medium" // 为文字设置字体大小和字体样式
ctx.fillText(tip, 70, 45) // 填充文字和给文字对应的位置
// 获取小程序码
getCanvasSunCode(ctx)
}
})
}
// 获取太阳码
const getCanvasSunCode = async (ctx: any) => {
// 因为我获取的太阳码是base64的,在这需要通过removeSave删除一下,主要是为了清理缓存
// 但是我这边是有错误的,这个并不影响海报的构建,你也可以注释掉,removeSave方法,我放在下面tool中
await removeSave()
// 将你获取的sunCode.value 传给base64Save方法,它将给你返回一个本地路径,这个sunCode.value,是服务端生成好的一个太阳码,里面可能存储了一些参数
let base64Path: any = await base64Save(sunCode.value) // base64Save方法,我放在下面tool中
// codePosition.value是后台返回的数据,里面包含了,这个小程序码的大小和位置,当然你如果你的二维码是固定的,你也可以写死如:ctx.drawImage(base64Path, 236, 236,443, 837)
ctx.drawImage(base64Path, codePosition.value[0] * wid.value, codePosition.value[1] * wid.value, codePosition.value[2] * wid.value, codePosition.value[3] * wid.value)
//通过 draw()将我们的,背景图片、头像和太阳码渲染出来,这个很关键,因为它是负责画处理你drawImage中的内容
ctx.draw()
// 将加载的弹窗隐藏
uni.hideLoading()
// 在点击按钮之前将isPosterShow.value = false,等让所有的canvas执行完成后,把popup展示出来,因为我们的popup中有保存海报的按钮,如果不这样的话,按钮会提前展示,但是这个时候海报还没展示出来
isPosterShow.value = true
}
2、保存海报到本地
const savaImgLocalClick = () => {
uni.showLoading({
title: '保存图片中...'
})
// 保存canvas为图片,width,height,destWidth,destHeight 一般为默认就可以,可以尝试下,看看会有什么区别,我这边并没发现什么区别
uni.canvasToTempFilePath({
canvasId: 'mycanvas',
quality: 1,
width: 604, // 画布宽度(默认为canvas宽度-x)
height: 1080, // 画布高度(默认为canvas高度-y)
destWidth: 604 * pixelRatio.value, // 输出图片宽度(默认为 width * 屏幕像素密度)
destHeight: 1080 * pixelRatio.value, // 输出图片高度(默认为 height * 屏幕像素密度)
complete(res) {
console.log(res)
downloadPoster.value = res.tempFilePath
// usrinfo.bgurl = res.tempFilePath
uni.authorize({
scope: 'scope.writePhotosAlbum',
success: () => {
//保存
uni.saveImageToPhotosAlbum({
filePath: downloadPoster.value, // 保存也是只能用本地路径
success() {
uni.hideLoading()
uni.showToast({
title: '海报已保存至本地!',
icon: 'none'
})
},
fail() {
uni.showToast({
title: '海报保存失败!',
icon: 'none'
})
}
})
},
fail: () => {
uni.showModal({
content: '由于您拒绝保存到您手机里,无法进行保存,点击确定去授权',
success: (res) => {
if (res.confirm) {
/* 这个就是打开设置的API*/
uni.openSetting({
success: () => {
// console.log(res1.authSetting);
}
})
}
}
})
}
})
}
})
}
3、工具类
// 画圆角
export function drawCircleImage(ctx: any, img: string, x: number, y: number, radius: number) {
ctx.save()
const size = 2 * radius
ctx.arc(x, y, radius, 0, 2 * Math.PI)
ctx.clip()
ctx.drawImage(img, x - radius, y - radius, size, size)
ctx.restore()
}
// base64转path
export function base64Save(base64File: any) { //base64File 需要加前缀
const fsm = wx.getFileSystemManager()//获取全局文件管理器
let extName = base64File.match(/data:\S+\/(\S+);/)
if (extName) {
//获取文件后缀
extName = extName[1]
}
//获取自1970到现在的毫秒 + 文件后缀 生成文件名
const fileName = Date.now() + '.' + extName
return new Promise((resolve, reject) => {
//写入文件的路径
const filePath = wx.env.USER_DATA_PATH + '/' + fileName
fsm.writeFile({
filePath,
data: base64File.replace(/^data:\S+\/\S+;base64,/, ''), //替换前缀为空
encoding: 'base64',
success: () => {
console.log(filePath, '222')
resolve(filePath)
},
fail() {
reject('写入失败')
},
})
})
}
// 删除base64
export function removeSave(FILE_BASE_NAME = 'tmp_base64src', format = 'png') {
return new Promise((resolve) => {
// 把文件删除后再写进,防止超过最大范围而无法写入
const fsm = uni.getFileSystemManager() //文件管理器
const FILE_BASE_NAME = 'tmp_base64src'
const format = 'png'
const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`
fsm.unlink({
filePath,
success(res) {
console.log('文件删除成功')
resolve(true)
},
fail(e) {
console.log('readdir文件删除失败:', e)
resolve(true)
}
})
})
}
4、总结
1、使用canvas的场景需要根据具体需求而定,如果必须要使用图片的原生转发属性,那就需要后端来生成然后给我们返回图片。
2、如果加载出来的海报的页面还能滚动,需要将海报弹出的时候,整个外层样式设置为overflow:hidden
,关闭的时候设置为overflow:unset
。
3、uni.getImageInfo()
可以把网络地址转化为本地地址。
4、uni.drawImage()
最后一定要执行ctx.draw()
方法