这几天在做一个绘制长图的功能, 试了很多方法均无法有效的实现, 经过长时间折腾, 总算搞了出来. 期间走了很多坑, 一次次的重试以及换方法, 几近崩溃, 我觉得很有必要把这个艰难过程记录下来. 实现起来确实有点繁琐, 我想不出更好的方案来代替, 如哪位朋友有更简单或者更正宗更高效的实现方式, 还望私信或评论告知.
需求是这样的, 就像写信一样, 有标题, 有正文, 还有署名以及日期. 不同的是加了图片, 图片数量不超过九张; 另外, 还需要一张图片信纸作为整封信的背景图. 因此整理一下写信所需元素:
-
title
标题 -
content
正文内容 -
images
所选图片 (非必须) -
backgroundImage
信纸背景图
之前有过了解, 基本上前端语言都会提供相对较底层的绘制技术, 微信小程序提供了CanvasContext
API 用于实现各种绘制功能, 基本上可以满足日常需求. 点击查看微信小程序CanvasContext相关API
最难的就是绘制文字!!!
简直是天坑!!!
简直是神坑!!!
简直是巨坑!!!
我以前并没使用过绘制文字的 API , 看着文档描述的看起来很简单. 先不说里面的坑, 开始写代码, wxml 文件需要创建一个画布, 这里的宽高单位是px
:
<canvas canvas-id="canvasId" style="width:{{canvasWidth}}px;height:{{canvasHeight}}px;background:red;position:fixed;top:{{canvasTop}}px;" ></canvas>
画布 Canvas 的width
和height
默认 width 为300px, height 为150px, 显然不是想要的
-
width
: 根据需求选用屏幕的宽度, 这里有个坑值得注意, 如果直接设置为wx.getSystemInfoSync().windowWidth
, 绘制出的图片很模糊, 质量差, 一看就是分辨率不够. 因此宽度需要设置的比屏幕宽度大, 可以是其的两倍或三倍大小. 做iOS
开发的时候知道在不同机型有可能使用两倍图, 如iPhone8
; 而iPhone8 Plus
则使用三倍图. 查看 API 得知wx.getSystemInfoSync().pixelRatio
即为设备屏幕的像素比, 因此设置:
let info = wx.getSystemInfoSync(); // 设备系统信息
this.setData({ canvasWidth: info.windowWidth * info.pixelRatio }); // 画布的宽度
-
height
: 因为正文内容是未知的, 图片数量未知, 每张图片宽高也不一样, 所以画布高度就很难确定, 需要计算文字的高度, 为了不让图片变形以及图片宽度不超过画布, 需等比例缩小图片. 最后累加起来得到所有图片的高度. 从上至下绘制的时, 文字与图片、图片和图片之间有一定的间隔, 间隔大小也需乘以设备屏幕的像素比
艰难的绘制文字
先设置canvasHeight
为300px :
this.setData({ canvasHeight: 300 })
试着绘制一小段文字, 代码如下:
this.data.content = '今天的天气真不错阳光明媚';
let ctx = wx.createCanvasContext('canvasId');
// 绘制画布背景色
ctx.setFillStyle('#95B095');
ctx.fillRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);
// 开始绘制文字
let fontSize = 17 * this.data.scale;
ctx.setFontSize(fontSize);
ctx.setFillStyle('#00000');
let x = 0;
let y = fontSize + 5;
ctx.fillText(this.data.content, x, y - 5);
ctx.draw();
通过wx.canvasToTempFilePath()
预览图片后如下图(所有截图的灰色部分代表模拟器外区域, 黑色代表模拟器, 浅绿色代表画布):
看起来挺好的, 没什么问题, 但当正文内容很长的时候, 会发现显示的不完整, 没有换行. 有可能是缺少一个参数导致, 没有设置绘制文字的最大宽度导致的, 因此改写绘制代码如下:
ctx.fillText(this.data.content, x, y - 5, this.data.canvasWidth);
整段文字被压缩在画布范围内了, 被挤变形, 不忍直视, 效果如下:
图2 --查了一圈官方文档也没有找到解决方法, 网上找了点资料大多说是遍历字符串, 计算每个字的宽度, 累加起来如果大于画布宽度则换行, 一个字一个字的绘制, 看着似乎有效, 于是按照这个方法来实现一遍. 开始使用for
循环去遍历, 最开始遍历字符串时没啥问题, 后来试多几次遇到一个小问题, 当文字内容中有emoji
表情()的时候, 绘制不出表情字符, 原因是遍历的时候就无法获取表情字符. 于是后面改用for...of
来遍历, 才没出现此问题. 代码如下:
this.data.content = '今天的天气真不错阳光明媚,非常适合去郊游,哥儿们几个去不去呢,大家也好久没聚在一起聊聊了,非常难得的机会';
let ctx = wx.createCanvasContext('canvasId');
// 设置画布背景色
ctx.setFillStyle('#95B095');
ctx.fillRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);
// 开始绘制文字
let fontSize = 17 * this.data.scale;
ctx.setFontSize(fontSize);
ctx.setFillStyle('#FFFFFF');
let x = 0;
let y = fontSize + 5;
let rowWidth = 0.0;
let i = 0;
for (let str of this.data.content) {
let width = ctx.measureText(str).width;
rowWidth += width;
if (rowWidth >= this.data.canvasWidth) { // 换行
x = 0;
i = 0;
y += fontSize;
rowWidth = 0.0;
} else { // 本行
if (i != 0) {
x += width;
}
}
ctx.fillText(str, x, y, this.data.canvasWidth);
I++;
}
ctx.draw();
效果如下:
图3 --似乎完美解决之前的问题, 需求就要实现了, 但有时候就是比较衰, 也可能是上天帮自己, 每次输入不同文字去绘制, 突然有一次我切换为系统双拼输入法时, 不小心输入了一个英文字母a
, 发现绘制不出来, 仔细一看原来是与前面一个汉字重叠了, 如下图:
后面又试了一些数字、英文符号, 全部会出现重叠的情况, 这个问题至今无法解决. 最后仔细思考后, 觉得是单独绘制数字、英文字母、英文符号才会出现此问题, 于是想着如果不单独绘制应该不会出现, 但是又想如果不单独绘制那还能怎么绘制, 因为整体绘制所有文字是没办法换行的, 之前就试过了.
经过连胜六局王者荣耀后猛的来了灵感, 为何不一行一行的绘制呢, 之前实践过的整体绘制无法换行, 如果截取一部分, 也就是一个小整体, 应该是没问题的, 但是要怎么实现呢. 苦思冥想后有了思路, 同样采用遍历的方法, 不同的是先声明一个字符串数组来表示需要绘制的行字符串(数组转字符串). 遍历整个字符串时, 计算每个字符的宽度并累加起来, 同时此字符添加到字符串数组里, 当累加后的宽度大于等于画布宽度的时候就说明要换行了, 实现如下:
this.data.content = '今天的天气真不错aha阳光明媚,非常适合去郊游,哥儿几个233去不去,大家也好久没聚在了,非,常&难.得k的(["/机会';
let ctx = wx.createCanvasContext('canvasId');
// 设置画布背景色 今天的天气真不错阳光明媚,非常适合去郊游,哥儿几个去不去,大家也好久没聚在了,非常难得的机会
ctx.setFillStyle('#95B095');
ctx.fillRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);
// 开始绘制文字
let fontSize = 17 * this.data.scale;
ctx.setFontSize(fontSize);
ctx.setFillStyle('#FFFFFF');
let x = 0;
let y = fontSize;
let rowWidth = 0.0;
let strs = [];
for (let str of this.data.content) {
let width = ctx.measureText(str).width;
if ((rowWidth + width) >= this.data.canvasWidth) {
// 需要换行了, 先把这行绘制了
let drawStr = strs.join('');
ctx.fillText(drawStr, x, y);
// 这行绘制完了, 清空
strs.length = 0;
rowWidth = 0.0;
// 下一行了, y坐标增加
y += fontSize;
}
// 不需要换行, 不需要绘制, 字符添加到数组, 累加宽度
strs.push(str);
rowWidth += width;
}
if (strs.length > 0) {
// 剩余的最后一行
let drawStr = strs.join('');
ctx.fillText(drawStr, x, y);
}
ctx.draw();
效果很好, 请看图:
图5 --看来万事大吉了, 很是欣慰, 看着绘制出的完美文字, 心里甚是高兴. 但手指也没停, 依然在不断的输入文字点击立即去绘制, 然而当在输入框里输入换行时, 绘制出来的却没有换行, 而是以空格代替. 顿时心里哇凉哇凉的, 还是不够完美, 也庆幸自己测试次数多, 及早发现问题. 其实这个换行也好解决, 遍历的时候判断字符是否是换行符, 只需要修改上述的循环内的代码即可:
for (let str of this.data.content) {
let width = ctx.measureText(str).width;
// 判断有没有换行符
let isLineFeed = false;
if (str === '\n') {
isLineFeed = true;
}
if ((rowWidth + width) >= this.data.canvasWidth || isLineFeed) {
// 需要换行了, 先把这行绘制了
let drawStr = strs.join('');
ctx.fillText(drawStr, x, y);
// 这行绘制完了, 清空
strs.length = 0;
rowWidth = 0.0;
// 下一行了, y坐标增加
y += fontSize;
}
// 不需要换行, 不需要绘制, 字符添加到数组, 累加宽度
if (!isLineFeed) {
strs.push(str);
rowWidth += width;
}
}
如下效果:
图6 --信纸绘制
先来看一下最终实现的效果图:
图7 --要实现最终效果还需要信纸作为背景, 设计师给的背景图尺寸为1035*2745
, 宽度基本足够. 但当文字特别多, 或者图片有九张的时候, 这高度肯定就不够了, 只能靠从上至下平铺多张, 至于多少张可以计算出来.
期间还有很多很多细节比较繁琐, 花费相当多的时间去打磨, 去验证, 这里就不一一赘述. 此外, 在写这篇文章的时候, 偶然发现新的官方Canvas组件已经可以使用, 以后有机会再来研究吧.
网友评论