评论

微信小程序前端生成图片用于分享朋友圈最终解决方案

微信原生小程序实现前端生成海报图分享朋友圈的完整实现

前段时间一直在做微信小程序的,遇到了许多的坑,其中遇到了需要前端合成图片保存到相册用于分享到朋友圈。借简书记录一下最终解决方案,先看一下最终效果

该文章的所有演示代码托管与github,代码地址,微信调试工具中访问请关闭合法域名检查开启es6转换,真机调试请打开调试vconsole

该文章解决的问题如下:

  1. 微信小程序生成图片,并保存到相册
  2. 微信小程序生成图片实现响应式
  3. 微信小程序canvas原生组件如何给画布添加css动画
  4. 保存高清分享图方案
  5. 微信小程序生成图片实现单屏适应

微信小程序生成图片,并保存到相册

首先,我们希望能实现如下功能,点击用户头像,从底部弹出一个分享弹窗,可以保存合成图片到相册,可以关闭弹层

我们将该功能封装成一个Component自定义组件

定义wxml基本结构

<view class="share {{visible ? 'show' : ''}}">
  <view class="content">
    <canvas class="canvas" canvas-id="share" />
    <view class="footer">
      <view class="save">保存到相册</view>
      <view class="close">关闭</view>
    </view>
  </view>
</view>

定义wxss样式

.share {
  position: fixed;
  top: 0;
  left: 0;
  min-height: 100vh;
  width: 100%;
  background: rgba(61, 61, 61, 0.5);
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.2s ease-in-out;
  z-index: 99999;
}

.share.show {
  visibility: visible;
  opacity: 1;
}

.share .content {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.share .content .footer {
  width: 562rpx;
  height: 100rpx;
  background: #fff;
  border-top: 2rpx solid #e9e9e9;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  font-size: 28rpx;
}

.share .content .footer .close {
  width: 100rpx;
  height: 100rpx;
  line-height: 100rpx;
  flex-grow: 0;
  flex-shrink: 0;
  text-align: center;
  border-left: 2rpx solid #e9e9e9;
}

.share .content .footer .save {
  height: 100rpx;
  line-height: 100rpx;
  flex-grow: 1;
  flex-shrink: 1;
  text-align: center;
}

.share.show .content .canvas {
  display: inline-block;
}

.share .content .canvas {
  display: inline-block;
  background: #fff;
  margin: 60rpx 0 0 0;
  width: 562rpx;
  height: 1000rpx;
}

定义json

{
  "component": true
}

定义组件构造器

Component({
  properties: {
    visible: {
      type: Boolean,
      value: false
    },
    // 由于需要绘制用户信息,由页面传入
    userInfo: {
      type: Object,
      value: false
    }
  },
  methods: {
    draw() {
      // 实际绘制函数,后续绘制代码放于此处
    }
  }
})

基本结构和样式定义完成,接下来开始可一开始我们绘制之旅了,合成图片需要用到微信小程序wx.getImageInfo函数,我们先对它进行Promise化方便后期调用

function getImageInfo(url) {
  return new Promise((resolve, reject) => {
    wx.getImageInfo({
      src: url,
      success: resolve,
      fail: reject,
    })
  })
}

前期的准备工作建立完成,我们开始定义绘制方法draw

const { userInfo } = this.data
const { avatarUrl, nickName } = userInfo

// 获取头像图像信息 
const avatarPromise = getImageInfo(avatarUrl)
// 获取背景图像信息
const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg')

Promise.all([avatarPromise, backgroundPromise])
  .then(([avatar, background]) => {
    // 创建绘图上下文
    const ctx = wx.createCanvasContext('share', this)
    
    const canvasWidth = 281
    const canvasHeight = 500
    // 绘制背景,填充满整个canvas画布
    ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight)
    
    const avatarWidth = 60
    const avatarHeight = 60
    const avatarTop = 40
    // 绘制头像
    ctx.drawImage(
      avatar.path,
      canvasWidth / 2 - avatarWidth / 2,
      avatarTop - avatarHeight / 2,
      avatarWidth,
      avatarHeight
    )

    // 绘制用户名
    ctx.setFontSize(20)
    ctx.setTextAlign('center')
    ctx.setFillStyle('#ffffff')
    ctx.fillText(
      nickName,
      canvasWidth / 2,
      avatarTop + 50,
    )
    ctx.stroke()
    // 完成作画
    ctx.draw()
  })

接下来,我们需要监测visible属性的变化,决定是否开始绘制

Component({
  properties: {
    visible: {
      type: Boolean,
      value: false,
      observer(visible) {
        // 当开始显示分享弹窗时开始绘制
        if (visible) {
          this.draw()
        }
      }
    },
  },
  ....省略其他代码
})

此时,前端的绘制已基本成型,运行小程序变可看见合成图,由于我们的绘制尺寸是基于iphone6s进行绘制的,在iphone6s及部分相同分辨率查看,尺寸完全吻合,没有任何问题,然而当我们用iphone6s plus或者其他不同分辨率的手机打开时却变成了下面这个样子

绘制的图像没有完全占满画布了,为什么呢?这个是遇到的第二个问题

微信小程序生成图片实现响应式

其实我们的画布宽高单位都是基于rpx单位,因此在不同分辨率的手机上,实际的尺寸也就不同,然而我们绘制图片的尺寸都是以px为单位,自然无法实现响应式,因此我们需要一个js方法用于转换rpx值为px值

解读微信官方文档我们定义如下一个简单的转换方法

function createRpx2px() {
  const { windowWidth } = wx.getSystemInfoSync()

  return function(rpx) {
    return windowWidth / 750 * rpx
  }
}

const rpx2px = createRpx2px()

定义好了单位转换函数,我们只需转换相关值即可

const { userInfo } = this.data
const { avatarUrl, nickName } = userInfo

// 获取头像图像信息 
const avatarPromise = getImageInfo(avatarUrl)
// 获取背景图像信息
const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg')

Promise.all([avatarPromise, backgroundPromise])
  .then(([avatar, background]) => {
    // 创建绘图上下文
    const ctx = wx.createCanvasContext('share', this)
    
    const canvasWidth = rpx2px(281 * 2)
    const canvasHeight = rpx2px(500 * 2)
    // 绘制背景,填充满整个canvas画布
    ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight)
    
    const avatarWidth = rpx2px(60 * 2)
    const avatarHeight = rpx2px(60 * 2)
    const avatarTop = rpx2px(40 * 2)
    // 绘制头像
    ctx.drawImage(
      avatar.path,
      canvasWidth / 2 - avatarWidth / 2,
      avatarTop - avatarHeight / 2,
      avatarWidth,
      avatarHeight
    )

    // 绘制用户名
    ctx.setFontSize(rpx2px(20 * 2))
    ctx.setTextAlign('center')
    ctx.setFillStyle('#ffffff')
    ctx.fillText(
      nickName,
      canvasWidth / 2,
      avatarTop + rpx2px(50 * 2),
    )
    ctx.stroke()
    // 完成作画
    ctx.draw()

此时不管在什么分辨率下的手机都能正常显示了

微信小程序canvas原生组件如何给画布添加css动画

我们都知道微信小程序的canvas是原生组件,对于原生组件有许多的限制,比如不可以使用css动画,官方文档如下:

首先我们试着给canvas父层标签View.content标签添加弹出动画,修改样式如下:

.share .content {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  
  // 新增动画控制
  transform: translate3d(0, 100%, 0);
  transition: transform 0.2s ease-in-out;
}

// 新增动画控制
.share.show .content {
  transform: translate3d(0, 0, 0);
}

在调试器中使用,一切都很美好,完全按着预期由底部弹出,然后淡隐,不过当你用真机调试,canvas部分的效果变得不那么顺畅,流畅,没有弹出动画,没有淡隐效果,一切都变得那么的僵硬,那我们该怎么办呢?

解决办法的思路如下:

  1. 提供一个canvas标签,不可以做隐藏(隐藏会导致绘制失效),通过css tansform属性移除屏幕让其不可见
  2. 用image标签代替canvas标签显示给用户查看
  3. 当画布绘制完成后,我们保存绘制的图像到临时目录中,并获取图片地址
  4. 将地址提供给image标签用于展示

基于以上思路,首先改造我们的文档结构

<view class="share {{ visible ? 'show' : '' }}">
  <canvas class="canvas-hide" canvas-id="share" />
  <view class="content">
    <image class="canvas" src="{{imageFile}}" />
    <view class="footer">
      <view class="save">保存到相册</view>
      <view class="close" bindtap="handleClose">关闭</view>
    </view>
  </view>
</view>

新增样式

.share .canvas-hide {
  position: fixed;
  top: 0;
  left: 0;
  transform: translateX(-100%);
  width: 562rpx;
  height: 1000rpx;
}

想要保存canvas绘制的图像到临时目录,我们需要利用微信小程序的一个api接口wx.canvasToTempFilePath,因此首先我们还是对其进行Promise化

function canvasToTempFilePath(option, context) {
  return new Promise((resolve, reject) => {
    wx.canvasToTempFilePath({
      ...option,
      success: resolve,
      fail: reject,
    }, context)
  })
}

在组件的data属性中新增imageFile

// 仅列出新增部分,省略之前的代码
Component({
  data: {
    imageFile: ''
  }
})

修改我们的绘制方法

// 仅列出新增部分,省略之前的代码
// 修改画布的draw函数如下
ctx.draw(false, () => {
  canvasToTempFilePath({
    canvasId: 'share',
  }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath }))
})

此时在真机上运行调试,可以看到完美的满足我们的需求(沾沾自喜)

保存高清分享图方案

接下来我们需要实现保存到相册中,用于分享给朋友圈或者其他微博

保存图片到相册需要调用微信小程序api,wx.saveImageToPhotosAlbum,依照惯例进行Promise化

function saveImageToPhotosAlbum(option) {
  return new Promise((resolve, reject) => {
    wx.saveImageToPhotosAlbum({
      ...option,
      success: resolve,
      fail: reject,
    })
  })
}

我们为保存相册新增点击事件

<view class="save" bindtap="handleSave">保存到相册</view>

最后定义我们的保存方法

// 仅列出新增部分,省略之前的代码
Component({
  methods: {
    handleSave() {
      const { imageFile } = this.data

      if (imageFile) {
        saveImageToPhotosAlbum({
          filePath: imageFile,
        }).then(() => {
          wx.showToast({
            icon: 'none',
            title: '分享图片已保存至相册',
            duration: 2000,
          })
        })
      }
    }
  }
})

至此保存到相册功能完成了,但是有点瑕疵,原本我们用于绘制的图片非常的高清,可以绘制后保存的图片变得模糊了,没那么高清,这是过不了UED小姐姐那关的

那如何保证保存的图片不会失真呢,我们可以考虑把canvas大小放大到3倍,绘制3倍的图

修改样式

.share .content .canvas {
  display: inline-block;
  background: #fff;
  margin: 60rpx 0 0 0;
  width: 1686rpx; // 修改为之前的3倍
  height: 3000rpx; // 修改为之前的3倍
}

修改绘制函数,增长绘制大小为3倍

const { userInfo } = this.data
const { avatarUrl, nickName } = userInfo

// 获取头像图像信息 
const avatarPromise = getImageInfo(avatarUrl)
// 获取背景图像信息
const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg')

Promise.all([avatarPromise, backgroundPromise])
  .then(([avatar, background]) => {
    // 创建绘图上下文
    const ctx = wx.createCanvasContext('share', this)
    
    const canvasWidth = rpx2px(281 * 2 * 3) // 扩大3倍
    const canvasHeight = rpx2px(500 * 2 * 3) // 扩大3倍
    // 绘制背景,填充满整个canvas画布
    ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight)
    
    const avatarWidth = rpx2px(60 * 2 * 3) // 扩大3倍
    const avatarHeight = rpx2px(60 * 2 * 3) // 扩大3倍
    const avatarTop = rpx2px(40 * 2 * 3) // 扩大3倍
    // 绘制头像
    ctx.drawImage(
      avatar.path,
      canvasWidth / 2 - avatarWidth / 2,
      avatarTop - avatarHeight / 2,
      avatarWidth,
      avatarHeight
    )

    // 绘制用户名
    ctx.setFontSize(rpx2px(20 * 2 * 3)) // 扩大3倍
    ctx.setTextAlign('center')
    ctx.setFillStyle('#ffffff')
    ctx.fillText(
      nickName,
      canvasWidth / 2,
      avatarTop + rpx2px(50 * 2 * 3), // 扩大3倍
    )
    ctx.stroke()
    // 完成作画
    ctx.draw(false, () => {
      canvasToTempFilePath({
        canvasId: 'share',
      }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath }))
    })

我们重新保存图片,发现图片变得高清了,hu~~~

最后我们可以兴高采烈的把成果交给小测试了,一切看起来都很顺利,可惜终究过不了各种机型分辨率的测试,由于我们的设计基于iphone6s尺寸设计,在部分宽高比不同的机型,高度会超出屏幕高度,变成下面这个样子

按钮被挡住了,这下无奈了

微信小程序生成图片实现单屏适应

我们希望分享弹窗内容能在一个屏幕下显示完全,那可以根据当前手机宽高比与设计稿尺寸宽高比求出一个缩放比例对整体内容进行缩放即可

定义缩放比例计算

// 仅列出新增部分,省略之前的代码
Component({
  data: {
    responsiveScale: 1, // 缩放比例默认为1
  },
  lifetimes: {
    ready() {
      const designWidth = 375
      const designHeight = 603 // 这是在顶部位置定义,底部无tabbar情况下的设计稿高度

      // 以iphone6为设计稿,计算相应的缩放比例
      const { windowWidth, windowHeight } = wx.getSystemInfoSync()
      const responsiveScale =
        windowHeight / ((windowWidth / designWidth) * designHeight)
      if (responsiveScale < 1) {
        this.setData({
          responsiveScale,
        })
      }
    },
  },
})

修改wxml文档

<view class="share {{ visible ? 'show' : '' }}">
  <canvas class="canvas-hide" canvas-id="share" />
  <view class="content" style="transform:scale({{responsiveScale}});-webkit-transform:scale({{responsiveScale}});">
    <image class="canvas" src="{{imageFile}}" />
    <view class="footer">
      <view class="save" bindtap="handleSave">保存到相册</view>
      <view class="close" bindtap="handleClose">关闭</view>
    </view>
  </view>
</view>

修改wxss样式表

.share .content {
  // 省略其他定义
  // 新增缩放中心控制为顶部中心
  transform-origin: 50% 0;
}

整体分享遇到的坑都得到了解决,代码较多,所有的代码都托管到了github,欢迎访问运行代码地址,只有亲力亲为才能真正的掌握知识

最后一次编辑于  01-14  
点赞 3
收藏
评论

2 个评论

  • 普罗米亚天下第一今石洋之嫁我
    普罗米亚天下第一今石洋之嫁我
    05-15

    请问怎么养才能识别出扫码用户是谁呢

    05-15
    赞同
    回复 1
    • Faure
      Faure
      05-15

      可以通过给分享链接添加分享人用户id识别

      05-15
      回复
  • CalmDown
    CalmDown
    02-23

    使用第三方画布绘制的地图  老铁有没有思路怎么生成分享图

    02-23
    赞同
    回复