微信支付宝小程序二维码图片下载那些事~

973 阅读8分钟

近期公司业务更新,小程序端增加了类似分享拉新的活动,但是考虑到大数量的分享会导致链接被鹅厂封掉,决定采用二维码图片方式进行分享。这样从交互上就会有新的问题,当生成二维码之后怎么才能保存图片再分享出去呢?当然最简单的操作是让用户截屏分享,但是我们不能把操作都甩给用户(这真的是我的真实想法~),最后的最后决定研究下小程序二维码下载,因此就有了这篇分享。

在进入正文之前先抛出两个小问题:

1、微信小程序中设置元素隐藏但是需要这个元素做交互,如何操作?

这个问题其实就是要在页面上隐藏一个 canvas 元素,但是这个元素还必须能通过js获取到。如果我们使用类似 display:none 方式,那元素就彻底不见了,逻辑上就获取不到了。因此考虑到可以使用visibility或者option去设置,这样元素既能存在又可以隐藏。然而理想总是丰满的,在支付宝和微信开发工具里这样是没问题的,元素不显示,但是在真机微信小程序中,这个canvas绘制之后依旧会显示,貌似是css在这个时候是不生效的。

只好变换个思路,我们把 canvas 元素设置成fixed定位,然后top和left都设置成 -9999 类似的值,这样就可以避免其出现在视口内,同时元素又存在。

2、canvas 的fillText方法无法使文字折行显示

在用canvas绘制文字的时候,可能由于文字较多正常是折行显示的,但是canvas并不会自动折行。虽然小程序相关方法提供了类似 ### maxWidth 控制文本的宽度显示,但是这个属性智能控制文字显示宽度,如果超过这个宽度值,文字会被直接压缩,导致无法正常阅读(想问小程序这个属性有什么用。。。)。

这个问题没有更好的办法,只能自己写方法去进行控制,基本思路就是把文本转换成字符串,然后根据已经绘制的文本的长度去判断是否已经超过规定长度,超过则换行显示,直接上代码。

@params: 
imgCanvas: 需要绘制文字的canvas id
title:绘制文本内容
x:文办起始位置x
y:文本起始位置y
maxW:设置的最大宽度
rpx2px:将rpx转化为px(因为canvas设置是px单位,具体方法见下)

canvasText(imgCanvas, title, x, y, maxW) {
    let that = this
    var chr = title.split("");
    var temp = "";
    var row = [];
    // 设置文字样式和字号大小
    imgCanvas.setFontSize(that.rpx2px(26))
    imgCanvas.setFillStyle("#9a9a9a");
    imgCanvas.setTextBaseline("middle");

    for (var a = 0; a < chr.length; a++) {
        if (imgCanvas.measureText(temp).width < maxW) {
            console.log('正常范围内')
        } else {
            row.push(temp);
            temp = "";
        }
        temp += chr[a];
    }
    row.push(temp);
    console.log(row)

    for (var b = 0; b < row.length; b++) {
      imgCanvas.fillText(row[b], x, y + (b + 1) * that.rpx2px(28));
    }
}
rpx2px(rpx) {
    const windowW = wx.getSystemInfoSync().windowWidth;
    const px = rpx * windowW / 750;
    return px;
}

小问题得以处理,下面进入正文重点部分,主要通过两个角度说一下下载图片问题:第一是直接下载生成的二维码图片;第二是将二维码和其他元素(图片、文本)合成一张图进行下载。 两者基本思路是一样的,区别就是最终是否要去合成图片。当然微信小程序和支付宝小程序基本流程一样,本文主要以微信小程序为例,后面会简单说下支付宝小程序的异同点。(注:下文代码里的rpx2px方法均同上)

一、直接下载二维码图片

1、生成二维码图片同时拿到下载需要的路径

这里我们生成二维码使用的是 weapp-qrcode 工具库,可以把js文件下载到本地,在项目中引用。思路就是传入url,在回调里调用小程序 canvasToTempFilePath 拿到二维码下载地址:

// canvasWidth: 334, canvasHeight: 334  二维码宽高,单位均为px单位
<!-- canvas.wxml -->
<canvas canvas-id="sharecode" style="width: {{canvasWidth}}rpx; height: {{canvasHeight}}rpx;"/>

<!-- canvas.js -->
let url = 'https://www.test.com?a=1&b=2'
let that = this
QRcode({
    width: that.rpx2px(canvasWidth),
    height: that.rpx2px(canvasHeight),
    canvasId: 'sharecode',
    text: url,
    _this: that,
    callback: res => {
      setTimeout(()=>{
        wx.canvasToTempFilePath({     // 微信小程序方法,获取canvas下载地址
          x: 0,
          y: 0,
          width: this.rpx2px(canvasWidth),
          height: this.rpx2px(canvasHeight),
          canvasId: 'sharecode',
          success: function(res) {
              console.log('[[[[[[', res)
              let path = res.tempFilePath
              that.setData({
                qrCodePath: path,   // 二维码下载路径
              })
            },
        })
      }, 100)
    }
});

2、用户点击按钮保存图片

众所周知,小程序涉及到隐私问题,是需要用户授权的,因此在正式进入保存逻辑之前,先需要查一下保存图片到系统的授权情况(重点方法在代码里注释,相关api可以去查看文档~)。

// 保存图片授权逻辑
savePicAuth: function(){
    const that = this;
    // 没有二维码路径直接提示错误
    if(!that.data.qrCodePath){
      wx.showToast({
        title: '二维码信息获取失败,请稍后再试',
        icon: 'none'
      })
      return false
    }
    // 查询授权情况
    wx.getSetting({
      success:(res)=>{
        // 未授权相册权限
        if (!res.authSetting['scope.writePhotosAlbum']){
          // 打开授权面板
          wx.authorize({
            scope:'scope.writePhotosAlbum',
            success:(res)=>{
              console.log('save photo:', res)
              this.save()
            },
            fail: err => {
              if(err.errMsg.includes('authorize:fail')) {
                // 重新打开相册权限
                wx.openSetting({
                  success(settingdata) {
                    if (settingdata.authSetting['scope.writePhotosAlbum']) {
                      // 获取权限成功,给出再次点击图片保存到相册的提示
                      this.save()
                    }else {
                      // 获取权限失败,给出不给权限就无法正常使用的提示
                      wx.showToast({
                        title: '授权失败,无法保存图片',
                        icon: 'none',
                      })
                    }
                  }
                })
              }
            }
          })
        } else {
          this.save();
        }
      }
    })
},

授权逻辑通过,接下来进入保存流程,同样的微信小程序也提供了相关方法 saveImageToPhotosAlbum

save: function(){
    wx.saveImageToPhotosAlbum({
      filePath: that.data.qrCodePath,   // 上面保存的二维码下载路径
      success: (res) => {
        wx.showToast({
          title: '图片保存成功',
          icon: 'none',
        })
      },
      fail:(res)=>{
        wx.showToast({
          title: '图片保存失败',
          icon: 'none',
        })
      }
    })
}

至此,二维码图片就能成功下载了,撒花~~~

当我们欢呼雀跃的时候,产品小哥出现了,指着手机里硕大的二维码图片说,这是不是太大了,我们能不能做成这样的图片去保存,说完打开了某宝的分享图片......

二、二维码和图片合成一张图下载

合成二维码图片的整体逻辑和单独下载二维码基本一致,区别就在于在在授权通过后,不是直接去下载二维码,而是要通过canvas合成一张图片后进行下载。

因为合成还是一个canvas,我们需要一个新的载体来承载这个canvas图片

<canvas canvas-id="sharecode_img" style="width: {{shareCanvasWidth}}rpx; height: {{shareCanvasHeight}}rpx;"/>

然后我们在原有的 save 方法里添加canvas绘制逻辑,绘制完成后在通过wx.canvasToTempFilePath方法重新拿到新的路径后,再去用wx.saveImageToPhotosAlbum进行保存。

// 此处只写大体流程,细节请根据业务需求自己添加
// canvasWidth: 626, canvasHeight: 838  新和成canvas尺寸,单位均为px单位

save() {
    const that = this;
    wx.showLoading({
      title: '下载图片生成中...',
      icon: 'loading'
    })
    // 二维码和图片合成
    // 二维码信息,即上一步生成的二维码图片信息
    let promise1 = new Promise((resolve)=>{
      wx.getImageInfo({   // 获取图片相关信息
        src: that.data.qrCodePath,  // 支持网络路径(域名需要先在管理平台配置)、本地路径、代码包路径
        success: (result) => {
          console.log('二维码图片信息:', result)
          resolve(result)
        },
      })
    })
    // 其他图片信息
    let promise2 = new Promise((resolve)=>{
      wx.getImageInfo({
        src: 'https://www.test.com/images/test.png',
        success: (result) => {
          console.log('其他图片信息', result)
          resolve(result)
        },
      })
    })
    Promise.all([promise1, promise2]).then((res)=>{
      console.log('all res===>', res)
      // 新和成canvas载体
      let imgCanvas = wx.createCanvasContext('sharecode_img')
      // 整个canvas图片大小
      let shareCanvasWidth = that.rpx2px(that.data.shareCanvasWidth)
      let shareCanvasHeight = that.rpx2px(that.data.shareCanvasHeight)

      // 二维码大小及位置
      let canvasWidth = that.rpx2px(that.data.canvasWidth)
      let canvasHeight = that.rpx2px(that.data.canvasHeight)
      let qrcode_x = that.rpx2px(146)  // 二维码起始位置横坐标,视ui而定
      let qrcode_y = that.rpx2px(354)  // 二维码起始位置纵坐标,视ui而定
      
      // 其他图片大小及位置(如有多张图,可以多次设置变量,多次进行绘制)
      let personW = that.rpx2px(52)
      let personH = that.rpx2px(52)
      let person_x = that.rpx2px(40)
      let person_y = that.rpx2px(58)
      
      // 短文案内容
      let personT = '我是小短'
      let personTSize = that.rpx2px(32)
      let personT_x = that.rpx2px(107)
      let personT_y = that.rpx2px(68)
      
      // 长文案内容
      let personM = `我是特别长的文案我是特别长的文案我是特别长的文案我是特别长的文案我是特别长的文案`
      let personM_x = that.rpx2px(107)
      let personM_y = that.rpx2px(73)
      let personM_max = that.rpx2px(466)
      
      // 绘制背景 
      imgCanvas.setFillStyle('white')
      imgCanvas.fillRect(0, 0, shareCanvasWidth, shareCanvasHeight)
      imgCanvas.drawImage(res[1].path, person_x, person_y, personW, personH)
      imgCanvas.restore()
      // 绘制二维码
      imgCanvas.drawImage(res[0].path, qrcode_x, qrcode_y, canvasWidth, canvasHeight)
      // 绘制短文案
      imgCanvas.setFontSize(personTSize)
      imgCanvas.setFillStyle('#000000')
      imgCanvas.fillText(personT, personT_x, personT_y)
      // 绘制长文案,方法在文章开始介绍过
      that.canvasText(imgCanvas, personM, personM_x, personM_y, personM_max)

      imgCanvas.draw()
      
      setTimeout(()=>{
        // 获取新生成的canvas链接
        wx.canvasToTempFilePath({
          x: 0,
          y: 0,
          width: shareCanvasWidth,
          height: shareCanvasHeight,
          canvasId: 'sharecode_img',
          success: function(res) {
            console.log('share img mes===>', res)
            let path = res.tempFilePath
            that.setData({
              shareFilePath: path,
            }, ()=>{
              wx.hideLoading()
              wx.saveImageToPhotosAlbum({
                filePath: that.data.shareFilePath,
                success: (res) => {
                  wx.showToast({
                    title: '图片保存成功',
                    icon: 'none',
                  })
                },
                fail:(res)=>{
                  wx.showToast({
                    title: '图片保存失败',
                    icon: 'none',
                  })
                }
              })
            })
          },
          fail: function(err){
            console.log('生成分享图失败===>', err)
            wx.hideLoading()
          }
        })
      }, 100)
    })
  },

经过上述整体流程,微信小程序就能完美实现图片合成下载了,再次撒花~~~

三、支付宝小程序相关流程差异简述

微信小程序完美解决,接下来要搞定支付宝了。通常情况下这种操作流程都是类似的,copy过去改一下就行了。 但是copy过去之后发现根本行不通!因为某宝的小程序相关方法和鹅厂并不一样,无奈只好查了半天api后在进行修改。

虽然相关方法有差异,但是整体流程还是类似的,就不再叙述整体流程,简单说下差异性:

1、关于canvas id的定义:

微信里id是通过 canvas-id="" 方式添加;而支付宝里是通过 id="" 添加

2、获取二维码路径方法

支付宝里没有canvasToTempFilePath方法,而是通过canvas的实体类方法获取:

let qecodeCanvas = my.createCanvasContext('sharecode')
qecodeCanvas.toTempFilePath({
    x: 0,
    y: 0,
    width: canvasWidth,
    height: canvasHeight,
    canvasId: 'sharecode',
    success: function(res) {
      console.log('share code===>', res)
      let path = res.apFilePath
      that.setData({
        qrCodePath: path,
      })
    },
    fail(res) {
      console.log('get img error===>', res.error);
    }
})
3、用户相册权限授权问题

支付宝保存图片调用的是 my.saveImage 方法,此方法会自己根据用户是否受过权去决定是否唤起授权窗口。虽然看起来方便了,但是如果用户针对某个小程序关闭了相册授权,那他就不能直接判断状态了。因此我们还是需要加一层授权判断。

savePicAuth: function(){
    // 查询授权设置状态
    my.getSetting({
      success:(res)=>{
        // 此处需要判断是否为false,因为初次进来未授权res.authSetting里不会存在writePhotosAlbum字段,这样会导致直接打开设置的页面,但是因为并未受过权,设置页面不会有相册打开按钮,逻辑就走不通了。
        if (res.authSetting['writePhotosAlbum'] === false){
          // 重新打开相册权限
          my.openSetting({
            success(settingdata) {
              console.log('again open===>', settingdata)
              if (settingdata.authSetting['writePhotosAlbum']) {
                // 获取权限成功,给出再次点击图片保存到相册的提示
                that.save()
              }else {
                // 获取权限失败,给出不给权限就无法正常使用的提示
                my.showToast({
                  content: '授权失败,无法保存图片'
                })
              }
            }
          })
        } else {
          that.save();
        }
      }
    })
}
4、保存图片方法

支付宝小程序保存图片使用的是 my.saveImage 同样是需要传入图片url。

以上就是关于小程序下载图片的简单逻辑,同时开发过程中也相当于变相熟悉了下canvas的使用,毕竟日常开发中使用canvas并不多。以此文记录开发问题和过程,也希望能够帮助有需要的小伙伴~

如有问题,欢迎指正~~