微信、支付宝小程序生成海报分享图详解

时间:2024-03-06 15:44:44
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种彩蛋(坑)了吧~

这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能)

咱们不多说,直接上手就是干。最终效果如下


 

 

首先我们新增一个自定义组件,在该组件的json中引入painter

{
  "component": true,
  "usingComponents": {
    "painter": "/painter/painter"
  }
}

 

然后组件的WXML (代码片段在最后)

<view class="share-wrap" wx:if="{{visible}}" catchtouchmove="preventDefault">
  <view class="share-back"></view>
  <view class="share-container">
    <view class="close" bindtap="handleClose"></view>
    <image mode="widthFix" src="{{sharePath}}" class="share-image" />
    <view class="share-tips">保存图片,叫伙伴们来参与吧</view>
    <view class="save-btn" bindtap="handlePhotoSaved"></view>
  </view>
</view>
<painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" />

 

接着组件的WXSS (代码片段在最后)

.share-wrap {
  width: 100%;
}

.share-back {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.6);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 888;
}

.share-container {
  width: 100%;
  background: #FFF;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 999;
}

.close {
  width: 30rpx;
  height: 30rpx;
  overflow: hidden;
  position: absolute;
  right: 64rpx;
  top: 64rpx;
}

.close::after {
  transform: rotate(-45deg);
}

.close::before {
  transform: rotate(45deg);
}

.close::before,
.close::after {
  content: \'\';
  position: absolute;
  height: 3rpx;
  width: 100%;
  top: 50%;
  left: 0;
  margin-top: -2rpx;
  background: #9C9C9C;
}

.share-image {
  width: 420rpx;
  margin: 66rpx auto 0;
  display: block;
  border-radius: 16rpx;
  box-shadow: 0px 4rpx 8px 0px rgba(0, 0, 0, 0.1);
}

.share-tips {
  width: 100%;
  text-align: center;
  color: #3C3C3C;
  font-size: 28rpx;
  margin: 32rpx 0;
}

.save-btn {
  width: 336rpx;
  height: 96rpx;
  margin: 0 auto 94rpx;
  background: url(\'https://qiniu-image.qtshe.com/20190506save-btn.png\') center center;
  background-size: 100% 100%;
}

 

重点来了 JS (代码片段在最后)

Component({
  properties: {
    //属性值可以在组件使用时指定
    isCanDraw: {
      type: Boolean,
      value: false,
      observer(newVal, oldVal) {
        newVal && this.drawPic()
      }
    }
  },
  data: {
    imgDraw: {}, //绘制图片的大对象
    sharePath: \'\', //生成的分享图
    visible: false
  },
  methods: {
    handlePhotoSaved() {
      this.savePhoto(this.data.sharePath)
    },
    handleClose() {
      this.setData({
        visible: false
      })
    },
    drawPic() {
      if (this.data.sharePath) { //如果已经绘制过了本地保存有图片不需要重新绘制
        this.setData({
          visible: true
        })
        this.triggerEvent(\'initData\') 
        return
      }
      wx.showLoading({
        title: \'生成中\'
      })
      this.setData({
        imgDraw: {
          width: \'750rpx\',
          height: \'1334rpx\',
          background: \'https://qiniu-image.qtshe.com/20190506share-bg.png\',
          views: [
            {
              type: \'image\',
              url: \'https://qiniu-image.qtshe.com/1560248372315_467.jpg\',
              css: {
                top: \'32rpx\',
                left: \'30rpx\',
                right: \'32rpx\',
                width: \'688rpx\',
                height: \'420rpx\',
                borderRadius: \'16rpx\'
              },
            },
            {
              type: \'image\',
              url: wx.getStorageSync(\'avatarUrl\') || \'https://qiniu-image.qtshe.com/default-avatar20170707.png\',
              css: {
                top: \'404rpx\',
                left: \'328rpx\',
                width: \'96rpx\',
                height: \'96rpx\',
                borderWidth: \'6rpx\',
                borderColor: \'#FFF\',
                borderRadius: \'96rpx\'
              }
            },
            {
              type: \'text\',
              text: wx.getStorageSync(\'nickName\') || \'青团子\',
              css: {
                top: \'532rpx\',
                fontSize: \'28rpx\',
                left: \'375rpx\',
                align: \'center\',
                color: \'#3c3c3c\'
              }
            },
            {
              type: \'text\',
              text: `邀请您参与助力活动`,
              css: {
                top: \'576rpx\',
                left: \'375rpx\',
                align: \'center\',
                fontSize: \'28rpx\',
                color: \'#3c3c3c\'
              }
            },
            {
              type: \'text\',
              text: `宇宙最萌蓝牙耳机测评员`,
              css: {
                top: \'644rpx\',
                left: \'375rpx\',
                align: \'center\',
                fontWeight: \'bold\',
                fontSize: \'44rpx\',
                color: \'#3c3c3c\'
              }
            },
            {
              type: \'image\',
              url: \'https://qiniu-image.qtshe.com/20190605index.jpg\',
              css: {
                top: \'834rpx\',
                left: \'470rpx\',
                width: \'200rpx\',
                height: \'200rpx\'
              }
            }
          ]
        }
      })
    },
    onImgErr(e) {
      wx.hideLoading()
      wx.showToast({
        title: \'生成分享图失败,请刷新页面重试\'
      })
    },
    onImgOK(e) {
      wx.hideLoading()
      this.setData({
        sharePath: e.detail.path,
        visible: true,
      })
      //通知外部绘制完成,重置isCanDraw为false
      this.triggerEvent(\'initData\') 
    },
    preventDefault() { },
    // 保存图片
    savePhoto(path) {
      wx.showLoading({
        title: \'正在保存...\',
        mask: true
      })
      wx.saveImageToPhotosAlbum({
        filePath: path,
        success: (res) => {
          wx.showToast({
            title: \'保存成功\',
            icon: \'none\'
          })
          setTimeout(() => {
            this.setData({
              visible: false
            })
          }, 300)
        },
        fail: (res) => {
          wx.getSetting({
            success: res => {
              let authSetting = res.authSetting
              if (!authSetting[\'scope.writePhotosAlbum\']) {
                wx.showModal({
                title: \'提示\',
                content: \'您未开启保存图片到相册的权限,请点击确定去开启权限!\',
                success(res) {
                  if (res.confirm) {
                    wx.openSetting()
                  }
                }
              })
              }
            }
          })
          setTimeout(() => {
            wx.hideLoading()
            this.setData({
              visible: false
            })
          }, 300)
        }
      })
    }
  }
})

 

如此一个绘制分享图的自定义组件就完成啦。

效果图如下:

tips:

  • 暂不支持绘制图片 圆角为:10rpx 0rpx 0rpx 0rpx 类似的。
  • 文字居中实现可以看下我代码片段
  • 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么一行超出将会展示为省略号
    当然如果想支持四个圆角的 试试 把Pen.js文件78行的_doClip方法重写,代码:
_doClip(borderRadius, width, height) {
    if (borderRadius && width && height) {
      let border = borderRadius.split(\' \')
      let r1 = 0
      let r2 = 0
      let r3 = 0
      let r4 = 0
      if (border.length==1){
        r1 = r2 = r3 = r4 = Math.min(border[0].toPx(), width / 2, height / 2);
      }else{
        r1 = Math.min(border[0] == 0 ? 0 : border[0].toPx(), width / 2, height / 2);
        r2 = Math.min(border[1] == 0 ? 0 : border[1].toPx(), width / 2, height / 2);
        r3 = Math.min(border[2] == 0 ? 0 : border[2].toPx(), width / 2, height / 2);
        r4 = Math.min(border[3] == 0 ? 0 : border[3].toPx(), width / 2, height / 2);
      }
      //const r = Math.min(borderRadius.toPx(), width / 2, height / 2);
      // 防止在某些机型上周边有黑框现象,此处如果直接设置 setFillStyle 为透明,在 Android 机型上会导致被裁减的图片也变为透明, iOS 和 IDE 上不会
      // setGlobalAlpha 在 1.9.90 起支持,低版本下无效,但把 setFillStyle 设为了 white,相对默认的 black 要好点
      this.ctx.setGlobalAlpha(0);
      this.ctx.setFillStyle(\'white\');
      this.ctx.beginPath();
      this.ctx.arc(-width / 2 + r1, -height / 2 + r1, r1, 1 * Math.PI, 1.5 * Math.PI);
      this.ctx.lineTo(width / 2 - r2, -height / 2);
      this.ctx.arc(width / 2 - r2, -height / 2 + r2, r2, 1.5 * Math.PI, 2 * Math.PI);
      this.ctx.lineTo(width / 2, height / 2 - r3);
      this.ctx.arc(width / 2 - r3, height / 2 - r3, r3, 0, 0.5 * Math.PI);
      this.ctx.lineTo(-width / 2 + r4, height / 2);
      this.ctx.arc(-width / 2 + r4, height / 2 - r4, r4, 0.5 * Math.PI, 1 * Math.PI);
      this.ctx.closePath();
      this.ctx.fill();
      // 在 ios 的 6.6.6 版本上 clip 有 bug,禁掉此类型上的 clip,也就意味着,在此版本微信的 ios 设备下无法使用 border 属性
      if (!(getApp().systemInfo &&
          getApp().systemInfo.version <= \'6.6.6\' &&
          getApp().systemInfo.platform === \'ios\')) {
        this.ctx.clip();
      }
      this.ctx.setGlobalAlpha(1);
    }
  }

 

主要是把borderRadius属性Split为多个变量,然后一个一个判断有没有值。
使用方法(要么只给一个值,要么四个值都给):


链接:https://www.jianshu.com/p/3a76d719409b