【Taro开发】-宣传海报,实现canvas实现圆角画布/图片拼接二维码并保存(十一)

Taro小程序开发

系列文章的所有文章的目录

【Taro开发】-初始化项目(一)

【Taro开发】-路由传参及页面事件调用(二)

【Taro开发】-taro-ui(三)

【Taro开发】-带token网络请求封装(四)

【Taro开发】-自定义导航栏NavBar(五)

【Taro开发】-formData图片上传组件(六)

【Taro开发】-封装Form表单组件和表单检验(七)

【Taro开发】-tabs标签页及子组件的下拉刷新(八)

【Taro开发】-简易的checkBoxGroup组件(九)

【Taro开发】-页面生成二维码及保存到本地(十)

【Taro开发】-宣传海报,实现canvas实现圆角画布/图片拼接二维码并保存(十一)

【Taro开发】-分享给好友/朋友圈(十二)

【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实现效果及产生的问题解决

实现效果:在原有基础上实现当前选中项突出
思路:当前选中项正常高度,前一项和后一项高度减少
引发的问题:

  1. 可能由于canvas是原生组件,设置露出前后间距后,切换swiper后发现,swiperItem位置正常,但是canvas位置错乱,叠加在另一张上或者没有间距
  2. 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();
  }
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-雾里-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值