评论

如何实现快速生成朋友圈海报分享图

如何快速实现朋友圈海报分享图~

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

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

咱们不多说,直接上手就是干。

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

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

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

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

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

.share-wrap {
  width: 100%;
  visibility: hidden;
}

.share-wrap.active {
  visibility: visible;
}

.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;
  opacity: 0;
  transition: all .3s;
}

.share-back.active {
  opacity: 1;
  transition: all .3s;
}

.share-container {
  width: 100%;
  background: #FFF;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 999;
  border-radius: 16rpx 16rpx 0 0;
  transform: translateY(100%);
  transition: all .3s;
}

.share-container.active {
  transform: translateY(0%);
  transition: all .3s;
}

.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: {
    handleClose() {
      this.setData({
        visible: false
      })
      this.triggerEvent('close')
    },
    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',
                maxLines: 1,
                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() { },
    // 保存图片
    handleSavePhoto() {
      wx.showLoading({
        title: '正在保存...',
        mask: true
      })
      wx.saveImageToPhotosAlbum({
        filePath: this.data.sharePath,
        success: () => {
          wx.showToast({
            title: '保存成功'
          })
          setTimeout(() => {
            this.setData({
              visible: false
            })
            this.triggerEvent('close')
          }, 300)
        },
        fail: () => {
          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
            })
            this.triggerEvent('close')
          }, 300)
        }
      })
    }
  }
})

那么我们该如何引用呢?

json里引用我们封装好的组件share-box
{
  "usingComponents": {
    "share-box": "/components/shareBox/index"
  }
}
以下示例为获取用户头像昵称后再生成图。
<button class="intro" open-type="getUserInfo" bindgetuserinfo="getUserInfo" >点我生成分享图</button>
<share-box isCanDraw="{{isCanDraw}}" bind:initData="createShareImage" bind:close="handleClose" />
调用的地方:
const app = getApp()

Page({
  data: {
    nickName: '',
    avatarUrl: '',
    isCanDraw: false
  },
  onLoad() {
    this.setData({
      nickName: wx.getStorageSync('nickName') || '',
      avatarUrl: wx.getStorageSync('avatarUrl') || ''
    })
  },
  handleClose() {
    this.setData({
      isCanDraw: !this.data.isCanDraw
    })
  },
  getUserInfo(e) {
    if (e.detail.errMsg === 'getUserInfo:ok') {
      wx.setStorageSync('avatarUrl', e.detail.userInfo.avatarUrl)
      wx.setStorageSync('nickName', e.detail.userInfo.nickName)
      this.setData({
        nickName: e.detail.userInfo.nickName,
        avatarUrl: e.detail.userInfo.avatarUrl,
        isCanDraw: !this.data.isCanDraw
      })
    }
  }
})

最后绘制分享图的自定义组件就完成啦~效果图如下:

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为多个变量,然后一个一个判断有没有值。
使用方法(要么只给一个值,要么四个值都给):
css: {
	borderRadius: '16rpx'
	borderRadius: '16rpx 0 0 16rpx'
}

代码片段:https://developers.weixin.qq.com/s/kHEivim374oL

附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。
最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
最后一次编辑于  02-22  
点赞 82
收藏
评论

79 个评论

  • Mr.L
    Mr.L
    2020-07-21

    我用这个方法做了个海报效果不错,已经上线了!

    

    2020-07-21
    赞同 4
    回复 12
    查看更多(7)
  • 精彩极了
    精彩极了
    2019-11-08

    不好意思,刚发现问题就删掉了评论发表

    2019-11-08
    赞同 2
    回复 11
    • 精彩极了
      精彩极了
      2019-11-08回复喵的房东大人
      本地图片可以加载绘制进来,就是有一点错误提示,不太友好,哈哈
      2019-11-08
      1
      回复
    • 精彩极了
      精彩极了
      2019-11-11回复喵的房东大人
      用painter连续加载了三次,生成了三张图片,生成时会闪屏,然后左右滑动时也会闪屏,求解啊大佬
      2019-11-11
      回复
    • 喵的房东大人
      喵的房东大人
      2019-11-11
      canvas绘制图,性能本来就差点。你这个连续绘制3次。建议你写三个painter的内容。写三个canvas
      2019-11-11
      3
      回复
    • 精彩极了
      精彩极了
      2019-11-12回复喵的房东大人
      这样生成图片之后,把临时路径放到swiper里面去,就一直闪一直闪,部分手机是正常的,小米的就狂闪,不知道为什么
      2019-11-12
      2
      回复
    • 精彩极了
      精彩极了
      2019-11-12回复喵的房东大人
      看了一下生成的图片,都控制在1.5M以内
      2019-11-12
      回复
    查看更多(6)
  • 1638.
    1638.
    2020-05-11

    真机为什么不显示图片啊

    2020-05-11
    赞同 1
    回复 9
    • 喵的房东大人
      喵的房东大人
      2020-05-11
      图片没配置合法域名。你试下开启调试是否可以显示了
      2020-05-11
      1
      回复
    • M_yyyyyy_
      M_yyyyyy_
      2020-06-24回复喵的房东大人
      您好,开启调试之后,可以显示了,但是关闭调试之后又不显示了;请问这个如何解决呢?
      2020-06-24
      回复
    • 喵的房东大人
      喵的房东大人
      2020-06-24回复M_yyyyyy_
      小程序后台配置downloadFile合法域名。
      2020-06-24
      2
      回复
    • M_yyyyyy_
      M_yyyyyy_
      2020-06-24回复喵的房东大人
      您好,现在服务端下发给我的url是http,但是小程序后台配置的合法域名是https,开启调试之后,可以显示图片,但是关闭调试之后又不显示图片了,是和配置的https有关系么
      2020-06-24
      回复
    • 喵的房东大人
      喵的房东大人
      2020-06-24回复M_yyyyyy_
      让你们后端返回的图片地址支持下https,downLoadFile域名都得是https的。http的不行
      2020-06-24
      2
      回复
    查看更多(4)
  • 阳台
    阳台
    06-08

    canvas 2d 接口支持同层渲染且性能更佳,建议切换使用。

    需要提升个版本么 嘿嘿

    06-08
    赞同
    回复 14
    • 喵的房东大人
      喵的房东大人
      06-09
      需要哦~ 下载文档上最新的包,base64测试版那个
      06-09
      回复
    • 阳台
      阳台
      06-09回复喵的房东大人
      想动态改文字 怎么传参啊?
      06-09
      回复
    • 喵的房东大人
      喵的房东大人
      06-09回复阳台
      通过变量传递到组件里就好了吖
      06-09
      回复
    • 阳台
      阳台
      06-09回复喵的房东大人
      通过createShareImage传参吗?  小白求指教
      06-09
      回复
    • 喵的房东大人
      喵的房东大人
      06-09回复阳台
      上面可以绑定方法呀。<share-box 你的变量="{{你的变量}}" 例如: content="{{content}}"  组件内进行接收就行了 />
      06-09
      回复
    查看更多(9)
  • 🙈
    🙈
    06-02

    加群加不上啊

    06-02
    赞同
    回复 2
  • Await
    Await
    05-27

    楼主大佬,想请教一下啊,如果我的文本长度不确定,并且文本后面还需要加元素,该怎么布局

    05-27
    赞同
    回复 1
  • 小确幸🐾
    小确幸🐾
    05-18

    首先感谢楼主的分享

    使用该组件,安卓手机绘制圆形图片出现锯齿,不知道有没有解决方案

    05-18
    赞同
    回复 8
    查看更多(3)
  • 小土豆
    小土豆
    05-17

    请问可以使用transform属性吗

    05-17
    赞同
    回复 6
    查看更多(1)
  • Elena
    Elena
    05-10

    有部分机型不能生成图片 可以给点儿思路么~ 就是一直生成中 ...

    05-10
    赞同
    回复 1
    • 喵的房东大人
      喵的房东大人
      05-10
      你是不是没有配置 downLoadFile域名?
      05-10
      回复
  • Alany_🍀
    Alany_🍀
    05-08

    图片模糊怎么处理,还有微信头像真机不显示,换成固定的也不行,

    05-08
    赞同
    回复 1
    • 喵的房东大人
      喵的房东大人
      05-10
      图片没配置downloadFile合法域名。生成图片模糊 给painter组件加上scaleRatio="{{3}}"
      05-10
      回复

正在加载...

登录 后发表内容