Taro小程序开发
系列文章的所有文章的目录
【Taro开发】-简易的checkBoxGroup组件(九)
【Taro开发】-宣传海报,实现canvas实现圆角画布/图片拼接二维码并保存(十一)
【Taro开发】-小程序自动打包上传并生成预览二维码(十三)
【Taro开发】-全局自定义导航栏适配消息通知框位置及其他问题(十四)
文章目录
前言
基于Taro的微信小程序开发,主要组件库为Taro-ui
实现效果:
1.生成二维码
2.多个海报模版叠加二维码
3.左右滑动切换海报模版
4.保存海报
思路:
1.二维码的生成已在前面文章提到,能转换成base64/临时路径
2.海报模版为一张图片,图片上的叠加一张图片(二维码),初期想法是用绝对定位,就能完成效果,但是由于后期还需保存新形成的图片,于是改变方案,使用canvas
3.使用swiper
4.canvas转成base64,并在之前的保存方法上进行优化
提示:以下是本篇文章正文内容,下面案例可供参考
//stylePerInJs()为适配不同机型在js中的转换
this.state = {
codeUrl: "",//二维码显示内容
codeImgUrl: "",//二维码图片临时路径
swiperCurrent: 0,//滑动当前index
};
1.二维码转临时路径/保存base64方法
export function handleSaveCode(refNode, callBack, notSave, base64) {
//refNode(view内即为要保存的图片)
//callBack为方法,传出保存结果/url
//notsave前提下可传入二维码所在node或直接传入base64获得文件临时路径url
!notSave ? getSetting({
success: function ({ authSetting }) {
//没有权限则申请
if (!authSetting["scope.writePhotosAlbum"]) {
authorize({
scope: "scope.writePhotosAlbum",
success: () => {
handleWriteFile(refNode, (res) => callBack && callBack(res), notSave, base64);
}
});
} else handleWriteFile(refNode, (res) => callBack && callBack(res), notSave, base64);
}
}) : handleWriteFile(refNode, (res) => callBack && callBack(res), notSave, base64)
};
export async function handleWriteFile(ref, callBack, notSave, base64Img) {//保存图片到本地/转成url
let base64;
if (!base64Img) {
base64 = ref?.childNodes[0].props.src;
} else {
base64 = base64Img
}
const data = base64?.split(",")[1];
const filePath = `${env.USER_DATA_PATH}/${Date.now()}.png`;
const { writeFile } = getFileSystemManager();
writeFile({
data,
filePath,
encoding: "base64",
success: res => {
!notSave ? saveImageToPhotosAlbum({
filePath,
success: () => {
callBack && callBack(true);
Taro.atMessage({
type: "success",
message: "保存成功!"
});
},
fail: () => {
callBack && callBack(false);
Taro.atMessage({
type: "error",
message: "保存失败!"
});
}
}) : (callBack && callBack(filePath))
},
fail: err => {
callBack && callBack(false);
console.error(err);
}
});
}
2. 二维码节点转文件临时路径url
由于二维码是由第三方生成的,需要有该组件节点的存在,但不显示,此处使用modal框
<AtModal isOpened={false}>//不显示
<View ref={c => (this.codeRef = c)}>
<QRCode
text={codeUrl}
size={stylePerInJs(280)}
scale={4}
errorCorrectLevel="M"
typeNumber={2}
className="qrcode"
/>
</View>
</AtModal>
二维码节点转临时图片路径
onReady() {
handleSaveCode(
this.codeRef,
url => url && this.setState({ codeImgUrl: url }),
true
);
}
3.左右滑动切换模版
const templateList = [
{
id: "canvas1",
name: "模版1",
src:
"https://img1.baidu.com/it/u=2014959784,955233900&fm=253&fmt=auto&app=138&f=JPEG?w=354&h=500",
x: stylePerInJs(270),
y: stylePerInJs(920)
},
{
id: "canvas2",
name: "模版2",
src:
"https://img1.baidu.com/it/u=2055754772,1956304326&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750",
x: stylePerInJs(530),
y: stylePerInJs(860)
},
{
id: "canvas3",
name: "模版3",
src:
"https://img1.baidu.com/it/u=2699933354,1104984833&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=889",
x: stylePerInJs(530),
y: stylePerInJs(1030)
}
];
<Swiper
nextMargin={
swiperCurrent === templateList?.length - 1 ? "0px" : "10px"
}
previousMargin={swiperCurrent === 0 ? "10px" : "20px"}
current={swiperCurrent}
onChange={e => {
this.setState({
swiperCurrent: e.detail.current
});
}}
>
{templateList?.map(item => {
return (
<SwiperItem className="canvasSwiper">
<View
className="trace-filletBoxmd"
ref={c => (this.posterRef = c)}
>
<Canvas
canvasId={item.id}
id={item.id}
className="canvas"
type="2d"
/>
</View>
</SwiperItem>
);
})}
</Swiper>
//index.scss
.poster {
.canvasSwiper {
.trace-filletBoxmd {
margin:20px;
margin-left: 0px;
padding: 0;
border-radius: 20px;
background-color: '#fff';
}
}
.canvas {
width: 100%;
height: 1200px;
}
swiper {
height: 1260px;
}
}
4.canvas绘制图片及二维码
//绘制圆角矩形
circleImg(ctx, img, x, y, w, h, r) {
ctx.save();
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.clip();
if (img) {
//圆角图片
ctx.drawImage(img, x, y, w, h);
} else {
//圆角画布
ctx.fillStyle = "#f5f6f7";
ctx.fillRect(0, 0, w, h);
}
ctx.restore();
}
海报绘制方法
draw(res, bgUrl, codeX, codeY, name) {
const { codeImgUrl } = this.state;
// Canvas 实例
const canvas = res[0].node;
this.canvas = canvas;
// Canvas 的绘图上下文
const ctx = canvas.getContext("2d");
// 设备像素比
// 这里根据像素比来设置 canvas 大小
const dpr = Taro.getSystemInfoSync().pixelRatio;
canvas.width = res[0].width * dpr;
canvas.height = res[0].height * dpr;
ctx.scale(dpr, dpr);
// 圆角背景并设置颜色
this.circleImg(
ctx,
null,
0,
0,
res[0].width,
res[0].height,
stylePerInJs(20)
);
//背景模版
const bgImg = canvas.createImage();
bgImg.src = bgUrl;
bgImg.onload = () => {//canvas没有层级,只有先来后到,因此需要在背景图片加载完成后再绘制二维码和文字,才可覆盖在背景图片上
this.circleImg(
ctx,
bgImg,
0,
0,
res[0].width,
res[0].height,
stylePerInJs(20)
);
// 绘制二维码
const img = canvas.createImage();
img.src = codeImgUrl;
img.onload = () => {
ctx.drawImage(
img,
codeX || 0,
codeY || 0,
stylePerInJs(120),
stylePerInJs(120)
);
};
// 绘制文本
ctx.font = "14px Microsoft YaiHei";
ctx.fillStyle = "white";
ctx.fillText("@lzh" + name, 20, 100);
};
}
将海报绘制到canvas节点上
drawCanvas = item => {
Taro.createSelectorQuery()
.select("#" + item.id)
// 获取节点的相关信息。需要获取的字段在fields中指定。返回值是 nodesRef 对应的 selectorQuery
.fields({ node: true, size: true })
// 执行所有的请求。请求结果按请求次序构成数组,在callback的第一个参数中返回
.exec(res => {
this.draw(res, item.src, item.x, item.y, item.name);
});
};
滑动重新绘制
componentDidUpdate(prevProps, prevState) {
if (
prevState.codeImgUrl !== this.state.codeImgUrl ||
prevState.swiperCurrent !== this.state.swiperCurrent
) {
this.drawCanvas(templateList[this.state.swiperCurrent]);
}
}
5.保存海报
<AtButton
className="trace-btn-default"
type="primary"
circle
onClick={() => {
handleSaveCode(
this.posterRef,
res => res && console.log("11111"),
false,
this.canvas.toDataURL("image/png")
);
}}
>
保存至手机相册
</AtButton>
6.升级版
6.1实现效果及产生的问题解决
实现效果:在原有基础上实现当前选中项突出
思路:当前选中项正常高度,前一项和后一项高度减少
引发的问题:
- 可能由于canvas是原生组件,设置露出前后间距后,切换swiper后发现,swiperItem位置正常,但是canvas位置错乱,叠加在另一张上或者没有间距
- canvas背景图片上需要叠加其他图片,且存在切换时高度不一,切换后叠加图片的y轴偏移不在原来的位置上
如何解决:绘制离屏canvas并转为临时路径图片显示
6.2修改draw方法绘制canvas并返回临时路径
async draw(res, item) {
const { codeImgUrl } = this.state;
let canvasTempUrl;//临时路径
// 把图片画到离屏 canvas 上
const canvas = Taro.createOffscreenCanvas({
type: "2d"
});
// Canvas 的绘图上下文
const ctx = canvas.getContext("2d");
this[item.id] = ctx.canvas;//将对应item的canvas存到全局变量
// 设备像素比
// 这里根据像素比来设置描绘 canvas,再进行缩放,可解决高清屏模糊
const dpr = Taro.getSystemInfoSync().pixelRatio;
canvas.width = stylePerInJs(512) * dpr;
canvas.height = stylePerInJs(916) * dpr;
ctx.scale(dpr, dpr);
// 圆角背景并设置颜色
this.circleImg(
ctx,
null,
0,
0,
stylePerInJs(512),
stylePerInJs(916),
stylePerInJs(20)
);
//背景模版
const bgImg = canvas.createImage();
await new Promise(resolve => {
bgImg.onload = resolve;
bgImg.src = item?.src;
});
this.circleImg(
ctx,
bgImg,
0,
0,
stylePerInJs(512),
stylePerInJs(916),
stylePerInJs(20)
);
const productPic = canvas.createImage();
await new Promise(resolve => {
productPic.onload = resolve;
productPic.src = "https://minio.sciento.cn/st-public/2/892bcb79adae4bcb8205dd0c03914727@apple.jpeg";
});
this.circleImg(
ctx,
productPic,
item?.imgX,
item?.imgY,
stylePerInJs(150),
stylePerInJs(150),
stylePerInJs(75)
);
// 绘制二维码;
const code = canvas.createImage();
await new Promise(resolve => {
code.src = codeImgUrl;
code.onload = resolve;
});
ctx.drawImage(code, item?.x, item?.y, stylePerInJs(86), stylePerInJs(86));
// 绘制文本
ctx.font = "12px Microsoft YaiHei";
ctx.fillStyle = '#000';
ctx.fillText('@lzh-poster', stylePerInJs(60), stylePerInJs(735));
canvasTempUrl = await new Promise(resolve => {//获取临时路径
handleSaveCode(
null,
poster => resolve(poster),
true,
ctx.canvas.toDataURL()
);
});
return canvasTempUrl;
}
6.3二维码生成后遍历模版列表获取所有模版的临时路径
this.state={
...
posterList: []
}
componentDidUpdate(prevProps, prevState) {
if (prevState.codeImgUrl !== this.state.codeImgUrl) {
this.getPosterList();
}
}
getPosterList = async () => {
let posterList = [];
for (let i = 0; i < templateList.length; i++) {
let item = { ...templateList[i] };
item.poster = await this.draw(null, item);
//console.log("获取的本地临时路径-", item.poster);
posterList.push(item);
}
this.setState({ posterList });
return posterList;
};
6.4 swiper调整
<Swiper
className="currentSwiper"
nextMargin={"43px"}
previousMargin={"59px"}//后边距+每个item的marginRight
current={swiperCurrent}
onChange={e => {
this.setState({
swiperCurrent: e.detail.current
});
}}
indicatorDots
indicatorActiveColor="#00C657"
>
{posterList?.map((item, index) => {
return (
<SwiperItem
className={
index === swiperCurrent + 1 || index === swiperCurrent - 1
? "canvasSwiper nearCanvasSwiper"
: "canvasSwiper"
}
>
<View className="trace-filletBoxmd" >
<Image className="canvas" src={item?.poster} />
</View>
</SwiperItem>
);
})}
</Swiper>
@import '@/styles/variables.scss';
.poster {
background-color: #fff;
height: 100vh;
.currentSwiper { //整体swiper盒子
height: 1000px;
margin-top: 90px;
margin-bottom: 40px;
}
.canvasSwiper { //单个swiper
.trace-filletBoxmd {
margin-right: 13px;
margin-bottom: 12px;
border-radius: 20px;
background-color: #f5f6f7;
box-shadow: 0px 0px 10px 2px rgba(15, 57, 33, 0.02);
height: 912px;
.canvas {
width: 100%;
height: 100%;
}
}
}
.nearCanvasSwiper{
.trace-filletBoxmd{
margin-top: 80px;
height: 796px !important;
}
}
}
6.5 分享图片/保存图片
需在真机运行,小程序中会提示无效路径
<AtButton
type="primary"
className="trace-btn-default"
circle
onClick={() => {
Taro.showShareImageMenu({
path: posterList[swiperCurrent]?.poster,
success: res => console.log("分享成功"),
fail: err => console.log("err:", err)
});
}}
>
分享/保存图片
</AtButton>
其他方式,只保存图片
<AtButton
type="primary"
className="footerBtn"
circle
onClick={() => {
handleSaveCode(
null,
res => res && console.log("保存成功"),
false,
this[templateList[swiperCurrent].id].toDataURL("image/png")
);
}}
>
保存至手机相册
</AtButton>
6.6 writeFile空间不足
由于每次进入宣传海报页面都会写入n个临时文件,可能会出现小程序空间不足,因此需在页面卸载时把临时文件也删掉。
export function clearUserTempFile() {
var fs = Taro.getFileSystemManager();
fs.readdir({
dirPath: wx.env.USER_DATA_PATH, // 本地用户目录
success(res) {
console.log("本地目录文件", res);
for (var i = 0; i < res.files.length; i++) {
// 清除所有本地用户文件,注意是否清除你保留的设置文件
if (res.files[i] === "miniprogramLog") continue;
fs.unlink({
filePath: wx.env.USER_DATA_PATH + "/" + res.files[i],
success(res) {
console.log("清除本地临时文件成功", res);
},
fail(err) {
console.log("清除本地临时文件失败", err);
}
});
}
},
fail(err) {}
});
}
componentWillUnmount() {
clearUserTempFile();
}