原生微信小程序使用了多个生成海报保存到相册的插件,没有一个是不坑的

其中painter,会出现部分华为继续图片缺失,而且官方是GitHub,似乎2年都没更新了

最终选择了微信小程序官方推荐的wxml-to-canvas,生成海报保存相册

wxml-to-canvas问题:

1. 图片圆角时灵时不灵

2. 所有半透明背景代码失效, 半透明图片是黑色

3. 没有做海报的展示,让用户看到海报是否加载完毕,就直接保存到相册,海报如果体积太大,会出现保存的图片部分缺失

解决方法:

(更新根本解决方法:  先将所有域名返回的图片, 使用wx.downloadFile下载下来, 再重新汇总, 放入canvas标签结构中, 再执行canvas生成代码)

1. 圆角图片不要用代码设置,直接使用已生成的圆角图片贴上去

2. 放弃半透明背景代码, 直接让UI切个半透明的图, 底下要垫一张非半透明的图, 一起贴上去

3. 出现图片缺失,是因为用户网速或系统或图片域名等因素各不相同,影响了体积较大的图片加载未完成,就直接调用保存到相册,导致图片缺失,根本解决方法,始终只有一个,就是要捕捉到图片加载完毕,已全部显示出来,再保存到相册,我使用了很多方法,比如定时器延长时间,让图片加载,但定时器也不能完全满足,最终使用了微信小程序提供的image图片标签里的图片加载完毕事件,bindload事件,完美解决

代码如下:

wxml页面:

<!-- 显示相册授权的弹窗 -->
<van-overlay show="{{ isModal }}" bind:click="onClickHide"> 这个是vant的弹窗
    <view class="wrapper">
        <view class="block">
            <text style="margin-top:46rpx;">温馨提示</text>
            <text style="color:#666666;letter-spacing: 0;">请选择相册授权</text>
            <view class="btns">
                <button bindtap="onClickHide">取消</button>
                <view></view>
                <button open-type="openSetting" bindopensetting="callback">授权</button>
            </view>
        </view>
    </view>
</van-overlay>


页面里的图片和海报,全部用定位给隐藏到边缘看不见,但实际上海报加载完毕的bindload依然会触发

sharePath => 海报地址是画布生成的地址

<!-- 海报图片 -->
<image class="canvas-img" src="{{sharePath}}" bindload="canvasImgLoad" style="height: 1505rpx;width: 750rpx;position: absolute; top: -9999rpx;"></image>

<!-- 生成的海报 -->
<wxml-to-canvas width="250" height="502" class="widget" style="position: absolute; top: -9999rpx;"></wxml-to-canvas>

上面 canvas 的 width 和 height, 可以控制海报的体积大小, 实际生成的海报像素比例, 是这里值的 3 倍

JS:

data: {
    isModal: false, // 强制授权相册弹窗
    canvasImgYes: false, // 海报图片是否加载完毕
    allImgObj: {}, // canvas使用所有图片
  },

// onLoad周期里绘制海报

onLoad(options) {
    // 获取海报 canvas
    this.widget = this.selectComponent('.widget')
  },

  // 整理下载的图片 => 最关键的就是这个方法, 一定要先将所有图片重新下载完毕, 再绘制到 canvas 上
  async downLoadImg() {
    let imgArr = [
      {
        label: 'qrCodeUrl',
        url: this.data.shareObj.qrCodeUrl
      },
      {
        label: 'topImg',
        url: this.data.userInfo.topImg
      },
      {
        label: 'contentBg',
        url: this.data.shareObj.addressImageUrl
      },
      {
        label: 'blueOp',
        url: this.data.blueOp
      },
      {
        label: 'centerPlaceRadius',
        url: this.data.centerPlaceRadius || this.data.shareObj.addressImageUrl
      },
    ]
    let label = ''
    imgArr.map(async item => {
       // 使用 downloadFile 方法下载不同域名的图片
      await wx.downloadFile({ 
        url: item.url,
        success: (res) => {
          label = `allImgObj.${item.label}`
          this.setData({
            [label]: res.tempFilePath
          })
          if (Object.keys(this.data.allImgObj).length == imgArr.length) {
            console.log(Object.keys(this.data.allImgObj).length);
            // 当所有图片都下载完毕, 开始绘制海报
            this.renderToCanvas()
          }
        }
      })
    })
  },

//海报架构
async renderToCanvas() {

    // 这里汇总canvas要渲染的所有图片和文字
    let obj = {
      qrCodeUrl: this.data.allImgObj.qrCodeUrl, // 二维码
      topImg: this.data.allImgObj.topImg, // 头像
      contentBg: this.data.allImgObj.contentBg, // 场地图片
      blueOp: this.data.allImgObj.blueOp, // 蓝色渐变图
      name: this.data.userInfo.name, // 姓名,
      centerPlaceRadius: this.data.allImgObj.centerPlaceRadius, // 圆角图
      placeName: this.data.placeName, // 名称
      visitTime: this.data.shareObj.visitTime, // 时间
      visitorsAddress: this.data.shareObj.visitorsAddress, // 地址
    }

    const wxmls = (info) => {
      return `
      <view class="container">
          <image class="bg" src="${info.contentBg}" />                            
          <image class="blueOp" src="${info.blueOp}" />                            
          <view class="content">
              <image class="topImg" src="${info.topImg}" />
              <text class="name">${info.name}</text>
              <text class="welcome">${info.placeName}</text>
              <view class="bgCenter">
                  <image class="centerImg" src="${info.centerPlaceRadius}" /> 
              </view>
              <view class="nameBox">
                  <text class="placeName">${info.placeName}</text>
              </view>
              <view class="timePlaceBox">
                  <text class="timePlace">时间:${info.visitTime}</text>
              </view>
              <view class="timePlaceBox">
                  <text class="timePlace">地址:${info.visitorsAddress}</text>
              </view>
              <image class="qrCodeUrl" src="${info.qrCodeUrl}" /> 
              <text class="hold">长按扫码</text>
          </view>
          
      </view>
      `
    }

    // 传入要填入的动态数据
    let wxml = wxmls(obj)

    // 下方的尺寸全部除以 3 , 除以3之后的宽高, 等于wxml页面中的宽高值, 除以 3 之前的单位, 
    // 就是设计图的尺寸大小, 3 也是控制生成海报尺寸大小的, 越大则海报体积越小

    const style = {
      container: {
        position: 'relative',
        width: 750 / 3, 
        height: 1505 / 3,
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        backgroundColor: '#5886FF',
      },
      bg: {
        width: 750 / 3,
        height: 489 / 3,
      },
      blueOp: {
        position: 'absolute',
        left: 0,
        top: 0,
        width: 750 / 3,
        height: 489 / 3,
      },
      content: {
        position: 'relative',
        width: 702 / 3,
        height: 1205 / 3,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        flexDirection: 'column',
        backgroundColor: '#FFFFFF',
        borderRadius: '15',
        marginTop: -224 / 3,
      },
      topImg: {
        position: 'absolute',
        width: 80 / 3,
        height: 80 / 3,
        top: 64 / 3,
        left: 30 / 3,
        borderRadius: 40 / 3,
      },
      name: {
        position: 'absolute',
        width: 200 / 3,
        height: 64 / 3,
        top: 60 / 3,
        left: 130 / 3,
        fontWeight: 600,
        color: '#000000',
        fontSize: 32 / 3,
        fontFamily: 'PingFangSC-Medium',
      },
      welcome: {
        position: 'absolute',
        width: 500 / 3,
        height: 28 * 3,
        fontSize: 28 / 3,
        color: '#666666',
        fontWeight: 400,
        left: 130 / 3,
        top: 107 / 3,
        fontFamily: 'PingFangSC-Regular',
      },
      bgCenter: {
        width: 642 / 3,
        height: 274 / 3,
        position: 'absolute',
        top: 177 / 3,
        left: 30 / 3,
      },
      centerImg: {
        width: 642 / 3,
        height: 274 / 3,
      },
      nameBox: {
        width: 340 / 3,
        height: 80/ 3,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        marginTop: 60 / 3,
      },
      placeName: {
        width: 240 / 3,
        height: 80 / 3,
        fontSize: 40 / 3,
        fontWeight: 500,
        color: '#000000',
        fontFamily: 'PingFangSC-Medium',
      },
      timePlaceBox: {
        width: 542/ 3,
        height: 64 / 3,
      },
      timePlace: {
        width: 542/ 3,
        height: 64 / 3,
        fontSize: 32 / 3,
        fontWeight: 400,
        color: '#666666',
        fontFamily: 'PingFangSC-Regular',
      },
      qrCodeUrl: {
        width: 300 / 3,
        height: 300 / 3,
        position: 'absolute',
        top: 757 / 3,
        left: 206 / 3,
      },
      hold: {
        width: 220 / 3,
        height: 52 / 3,
        fontSize: 26 / 3,
        color: '#666666',
        position: 'absolute',
        top: 1062 / 3,
        left: 247 / 3,
        fontFamily: 'PingFangSC-Regular',
      }
    }
    
    const p1 = this.widget.renderToCanvas({ wxml, style })

    p1.then((res) => {

      // 该方法是开始生成海报链接的方法
      this.createImage()
      
    }).catch(err => {
      // 下方的提示, 就是如果onLoad中的方法报错, 给的提示, 一般重新进入页面, 就不会出错
      wx.showToast({
        title: '加载失败,请重新进入页面',
        icon: 'none'
      })
    })
  },

  // 生成海报链接
  createImage() {
    const p2 = this.widget.canvasToTempFilePath()
    p2.then(res => {
      // 这就是海报的链接
      // console.log(res.tempFilePath)
      this.setData({
        sharePath: res.tempFilePath
      })
    }).catch(err=>{
      wx.showToast({
        title: '加载失败,请重新进入页面',
        icon: 'none'
      })
    })
  },

  // 页面中海报图片加载完毕回调
  canvasImgLoad() {
    console.log('海报图片加载完毕');
    this.setData({
      canvasImgYes: true // 用来判断海报是否已加载渲染完毕
    })
  },

  //点击保存按钮 该方法给页面中的保存到相册按钮
  extraImage() {
    if (this.data.canvasImgYes) { // 判断是否加载完毕的变量
      // 如果还是有图片缺失, 就在这里加个定时器如 2 秒后再执行下方方法
      this.savePhotos(this.data.sharePath)
    } else {
      wx.showToast({
        title: '正在生成海报,请等待~',
        icon: 'none',
        duration: 3000, //持续的时间
        success: res => {
          this.extraImage() // 还没加载完就调自己继续判断
        }
      })
    }
  },

  // 保存到相册的方法
  savePhotos(path) {
    wx.saveImageToPhotosAlbum({
      filePath: path,
      success(res) {
        wx.hideLoading()
        wx.showToast({
            title: '海报生成成功,已保存到相册',
            icon: 'none'
        })
      },
      fail: (res) => {
        wx.getSetting({
          success: res => {
            let authSetting = res.authSetting
            if (!authSetting['scope.writePhotosAlbum']) { // 如果没有授权相册, 弹窗授权相册
              wx.hideLoading()
              this.setData({
                isModal: true, // 控制是否弹窗强制授权的变量
              })
            }
          }
        })
      }
    })
  },

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐