背景
由于小程序本身不具备分享到朋友圈的能力,所以我们要采用曲线救国的方式来实现。即生成“分享海报”后,由用户自主保存,再去朋友圈分享。
下面是最终的实现效果:
又是图片合成,又是保存图片,看来是免不了使用canvas了,还是有点小慌(canvas菜鸡一枚)。中途也是遇到不少的问题。下面一起来看看吧~
欢乐踩坑之旅
踩坑之前提供一下,微信的文档,读完文档,走遍天下:小程序cavas文档
下面的几点,乍一看还是感觉很简单的。我当然也是这么想的,然后就差点没爬出来(调试时间花的比较多,总体难度还好)。
接下来,就一起把这些坑都踩平吧。
画布适配
首先介绍一下小程序canvas的特点:
- canvas元素大小可以使用小程序特有的
rpx
单位,但是内部绘制的时候,单位是px
。所以需要获取设备的屏幕宽度自行计算。方法也比较简单; - canvas元素在真机上不可以使用css3动画效果;
- canvas元素在真机上的层级最高(无法实现在上面做一个自定义的弹窗)。
利用比例换算适配各种屏幕:
wx.getSystemInfo({
success: res => {
if (res.errMsg === 'getSystemInfo:ok') {
let width = res.windowWidth
this.setData({
system: res.system.indexOf('iOS') > -1 ? 'ios' :'android', // 判断系统,为什么要用之后会说明
canvasRatio: width / 375, // 之后用于其他内容的比例转换
canvasWidth: res / 375 * this.data.canvasWidth, // 画布宽度
canvasHeight:res / 375 * this.data.canvasHeight // 画布高度
})
} else {
// 失败,一般不会进这里
}
},
fail: res => {
// 失败,一般不会进这里
}
})
农历年计算
这个问题难点在于不了解农历和公历之间的换算规则(感觉两者没有什么关系啊= =)。有兴趣的上网搜索一下,很容易能找到,方法就不贴了,相当长。
绘制文本
写文本会有问题吗?
多行文本不是可以自动换行吗?
文本溢出显示省略号,不是直接用-webkit-line-clamp
就好了吗?
文本加粗直接一个bold
不就搞定了吗?
在canvas中,还真有问题,上面的想法都不能实现。因为只提供了这样一个API:CanvasContext.fillText(string text, number x, number y, number maxWidth)
。
可以看到,这个API只能设置填充的文本,开始填充的x轴距离,y轴距离,绘制文本的总宽度。
也就是说,我们除了设置要填充哪些字,从哪里开始绘制,绘制到哪里结束以外,其他的都束手无策。
可想而知,多行文本、文字过多、过少都没有办法处理,因此需要写一个方法进行处理。
思路:
- 小程序中还提供了一个很好的API:
Object CanvasContext.measureText(string text)
,使用这个接口可以量出当前传入的文本所占的宽度,有了这个数据,我们就可以通过循环叠加每个字的方式来判断一行是否占满,占满则记为一组,如果超过了设置的显示行数,则回退一个字改为“…”,这样同时实现了换行和溢出省略号; - 用多行文本需要设置行高,关系到下一行从哪里开始画;
- 加粗的实现采用,左右偏移0.5px进行绘制,看起来就有加粗的效果了。
下面是实现的方案:
/**
* @func 绘制多行文本内容
* @params {object} ctx canvas画布
* @params {string} config.text 文本
* @params {number} config.line 行数
* @params {number} config.fontsize 字号
* @params {string} config.color 颜色
* @params {number} congig.X 画布 x 位置
* @params {number} config.Y 画布 y 位置
* @params {number} config.paddingRight 右边的距离
* @params {boolean} config.bold 是否加粗
* @returns {number} 画完的高度
*/
drawText (ctx, config) {
let tempContent = '' // 临时文本,用于存储拼接的字符
let row = [] // 临时数组,用于存储换行的内容
let lineHeight = config.lineHeight ? config.lineHeight : config.fontsize * 1.7 // 行高
ctx.setFontSize(config.fontsize) // 这会影响到计算宽度,必须先设置
ctx.setFillStyle(config.color)
for (let i = 0; i < config.text.length; i++) {
if (ctx.measureText(tempContent).width < this.data.canvasWidth - config.X - config.paddingRight - lineHeight) { // 未超出宽度继续拼接
tempContent += config.text[i]
} else {
i--
row.push(tempContent) // 记录换行
tempContent = ''
}
}
row.push(tempContent)
if (row.length > config.line) { // 此时说明有多行
let rowCut = row.slice(0, config.line) // 截取需保留的行数
let rowPart = rowCut[config.line - 1] // 取最后一行(这一行需要加省略号)
let lastLine = ''
let empty = []
for (let i = 0; i < rowPart.length; i++) {
if (ctx.measureText(lastLine).width < this.data.canvasWidth - lineHeight - config.X) { // 宽度预留省略号的位置
lastLine += rowPart[i]
} else {
break
}
}
empty.push(lastLine)
let group = empty[0] + '...'
rowCut.splice(config.line - 1, 1, group)
row = rowCut // 此时 row就是每行的文本
}
for (let b = 0; b < row.length; b++) {
ctx.fillText(row[b], config.X, config.Y + lineHeight + b * lineHeight, this.data.canvasWidth) // 写入文字内容
if (config.bold) {
ctx.fillText(row[b], config.X + .5, config.Y + lineHeight + b * lineHeight, this.data.canvasWidth)
ctx.fillText(row[b], config.X - .5, config.Y + lineHeight + b * lineHeight, this.data.canvasWidth)
}
}
}
小程序码获取展示
小程序码获取展示并没有什么问题,需要注意的是要记得把downFile中添加相应的域名,千万不要忘记。
正文图片显示
正文图片是从文章中正则抽取出来的第一张图,图片大小、宽高比均不确定。这就需要一个显示的策略。为了保证相对好的效果,采用的方案是保证短边显示出来,并取长边正中的部分。如下图:
具体实现见下列代码:
/**
* @func 取图片绘制区域
* @params {object} ctx 画布
* @params {object} imgInfo 图片信息,包含图片路径、宽、高等
* @params {number} x 绘制区域 x
* @returns {number} y 绘制区域 y
*/
drawImageArea (ctx, imgInfo,x, y) {
let imageWidth = 155 * this.data.canvasRatio // 画布中图片宽度
let imageHeight = 111 * this.data.canvasRatio // 画布中图片高度
let actWidth = 0 // 实际宽度
let actHeight = 0 // 实际高度
let sx = 0 // 起始点 sx
let sy = 0 // 起始点 sy
let dw = imageWidth / imgInfo.width // 画布与图片的宽度比例
let dh = imageHeight / imgInfo.height // 画布与图片的高度比例
if (dw < dh) { // 根据宽高比切高度
actWidth = imageWidth / dh
actHeight = imgInfo.height
sx = (imgInfo.width - actWidth) / 2
sy = 0
} else {
actWidth = imgInfo.width
actHeight = imageHeight / dw
sx = 0
sy = (imgInfo.height - actHeight) / 2
}
if (actWidth < 2000 && actHeight < 2000 || this.data.system === 'ios') {
ctx.drawImage(imgInfo.path, sx, sy, actWidth, actHeight, x, y, imageWidth, imageHeight)
}
}
正本图片来源
由于图片的来源是外部的链接,要解析图片就必须用wx.getImageInfo
获取,而获取图片的前提是域名加在downFile列表中。所以说,这个需求在弱小的前端手中就无法实现了。
想到的实现方案是把图片地址取出来给后端,由后端转存到自己的oss上进行访问。搞定。
总结
总体看下来,其实没有很难的地方,具体实现起来可能没有看起来这么轻松。有空的时候都可以试试看,实现一下这个还算比较常见的需求。