小程序现在没有提供直接分享到朋友圈的接口,所以只能采取折中的策略,即先将要分享的内容先保存为图片,保存到用户相册,然后再由用户将该图片分享到朋友圈
生成小程序码需要access token,后端生成比较方便
小程序的绘图使用的是标签
<canvas canvas-id="myCanvas" style="border: 1px solid;">canvas?
在onLoad
或者mounted
(mpvue)中绘图:
const ctx = wx.createCanvasContext('myCanvas')
ctx.setFillStyle('red')
// 从左上角(0, 0)开始,画一个150 x 75px 的矩形。
ctx.fillRect(10, 10, 150, 75)
ctx.draw()
canvas
左上角的坐标是(0,0)
,默认宽度300px,高度225px,canvas-id不可重复
<canvas style="width: 300px; height: 200px;" canvas-id="firstCanvas">canvas>
<canvas style="width: 400px; height: 500px;" canvas-id="secondCanvas">canvas>
更多小程序中canvas组件介绍看这里。
小程序提供的API与原生的API非常相似。
drawImage()
参数 | 描述 |
---|---|
img | 规定要使用的图像、画布或视频。 |
sx | 可选。开始剪切的 x 坐标位置。 |
sy | 可选。开始剪切的 y 坐标位置。 |
swidth | 可选。被剪切图像的宽度。 |
sheight | 可选。被剪切图像的高度。 |
x | 在画布上放置图像的 x 坐标位置。 |
y | 在画布上放置图像的 y 坐标位置。 |
width | 可选。要使用的图像的宽度。(伸展或缩小图像) |
height | 要使用的图像的高度。(伸展或缩小图像) |
context.drawImage(img,x,y,width,height);
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var img=document.getElementById("tulip");
ctx.drawImage(img,10,10);
context.drawImage(img,x,y);
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var img=document.getElementById("tulip");
ctx.drawImage(img,10,10,240,160);
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var img=document.getElementById("scream");
ctx.drawImage(img,90,130,90,80,20,20,90,80);
小程序中如果绘制的图片是网络图片,在开发者工具中可以预览,但是在手机端不会显示,需要先试用getImageInfo
方法将网络图片下载到本地,然后用图片的本地路径绘制
// 绘制网络图片
const imgPath2 = 'https://m.360buyimg.com/babel/jfs/t17068/121/2715217240/96992/93902dda/5b07a381N85db5073.png';
wx.getImageInfo({
src: imgPath2,
success(res) {
ctx.drawImage(res.path, left, 320, 200, 150);
}
});
rect()
context.rect(x,y,width,height);
参数 | 描述 |
---|---|
x | 矩形左上角的 x 坐标 |
y | 矩形左上角的 y 坐标 |
width | 矩形的宽度,以像素计 |
height | 矩形的高度,以像素计 |
// 红色矩形
ctx.beginPath();
ctx.setLineWidth(1);
ctx.setStrokeStyle("red");
ctx.rect(left, 200, 200, 20);
ctx.stroke();
fillRect()
context.fillRect(x,y,width,height);
参数 | 描述 |
---|---|
x | 矩形左上角的 x 坐标 |
y | 矩形左上角的 y 坐标 |
width | 矩形的宽度,以像素计 |
height | 矩形的高度,以像素计 |
默认的填充颜色是黑色。使用setFillStyle
方法填充颜色
ctx.setFillStyle('green');
ctx.fillRect(52, 55, 150, 75);
fillRect()
与strokeRect()
在调用后会立即在画布上画面效果,而rect()
不会立即将图形画出,只有在调用了`stroke()方法之后,才会实际作用于画布。
fillText()
context.fillText(text,x,y,maxWidth);
参数 | 描述 |
---|---|
text | 规定在画布上输出的文本。 |
x | 开始绘制文本的 x 坐标位置(相对于画布)。 |
y | 开始绘制文本的 y 坐标位置(相对于画布)。 |
maxWidth | 可选。允许的最大文本宽度,以像素计。 |
ctx.setFontSize(20);
ctx.setFillStyle('#6F6F6F');
ctx.setTextAlign('center');
ctx.fillText('妖妖灵', deciveWidth/2 , 220, 200);
要注意的是,x/y坐标位置一个点,setTextAlign是相对于这个点定位:
Canvas的文字排版不支持自动换行,如果不设定maxWitdh
,文字会一直横排下去,如果设定了maxWidth
,会令文字变窄而不会换行。解决这个问题的方法有两个思路:
1. 使用measureText
这个API,实现根据尺寸调整文字排版
2. 借助SVG 直接把CSS效果绘制上去
具体参考张鑫旭的博客,但是貌似第二种方法在小程序是不OK的
下面来看第一种方法的具体实现:
measureText()
方法返回包含一个对象,该对象包含以像素计的指定字体宽度。
context.measureText(text).width;
思路就是将字符串转换为数组后在逐项累加,判断累加的字符串的长度与maxWidth,当超过maxWidth时将之前的额字符串进行一次fillText输出,然后继续循环,并累加行高,向下移动再次进行除数。
// 文字换行
fillTextWrap(ctx, text, x, y, maxWidth, lineHeight) {
// 设定默认最大宽度
const systemInfo = wx.getSystemInfoSync();
const deciveWidth = systemInfo.screenWidth;
// 默认参数
maxWidth = maxWidth || deciveWidth;
lineHeight = lineHeight || 20;
// 校验参数
if (typeof text !== 'string' || typeof x !== 'number' || typeof y !== 'number') {
return;
}
// 字符串分割为数组
const arrText = text.split('');
// 当前字符串及宽度
let currentText = '';
let currentWidth;
for (let letter of arrText) {
currentText += letter;
currentWidth = ctx.measureText(currentText).width;
if (currentWidth > maxWidth) {
ctx.fillText(currentText, x, y);
currentText = '';
y += lineHeight;
}
}
if (currentText) {
ctx.fillText(currentText, x, y);
}
}
使用:
const textHeight = this.fillTextWrap(ctx, text, left, 220, 190, 30);
绘制头像需要用的API有一下几个:
save()
/restor()
保存/恢复当前绘图的上下文(否则的话真个画布就只剩下头像了)
beginPath()
开始一条路径,或重置当前的路径。
arc()
创建圆或部分圆
context.arc(x,y,r,sAngle,eAngle,counterclockwise);
ctx.arc(100,75,50,0,2*Math.PI);
参数 | 描述 |
---|---|
x | 圆的中心的 x 坐标。 |
y | 圆的中心的 y 坐标。 |
r | 圆的半径。 |
sAngle | 起始角,以弧度计。(弧的圆形的三点钟位置是 0 度)。 |
eAngle | 结束角,以弧度计。 |
counterclockwise | 可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针。 |
clip()
从原始画布中剪切任意形状和尺寸。
一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。您也可以在使用
clip()
方法前通过使用save()
方法对当前画布区域进行保存,并在以后的任意时间对其进行恢复(通过restore()
方法)。
drawImage
绘制图片,需要注意的是,绘制圆形的时候坐标是圆心的坐标,而图片的坐标是图片左上角的坐标,两个坐标系之间差了一个半径长度
// 绘制头像
ctx.save();
// 保存画布
ctx.beginPath();
// 圆心中点
const circleX = deciveWidth / 2;
const circleY = 80;
// 圆半径
const circleRadius = 50;
ctx.arc(circleX, circleY, circleRadius, 0, 2 * Math.PI);
// 剪切
ctx.clip();
// 绘制图片
const imgPath = '/static/1.jpg';
ctx.drawImage(imgPath, circleX - circleRadius, circleY - circleRadius, circleRadius * 2, circleRadius * 2);
// 恢复画布
ctx.restore();
使用的API是[wx.canvasToTempFilePath()
]https://developers.weixin.qq.com/miniprogram/dev/api/canvas/temp-file.html),把当前画布指定区域的内容导出生成指定大小的图片,并返回文件路径。在自定义组件下,第二个参数传入组件实例this,以操作组件内 组件
wx.canvasToTempFilePath(OBJECT, this)
OBJECT参数:
参数 | 类型 | 说明 |
---|---|---|
x | Number | 画布x轴起点(默认0) |
y | Number | 画布y轴起点(默认0) |
width | Number | 画布宽度(默认为canvas宽度-x) |
height | Number | 画布高度(默认为canvas高度-y) |
destWidth | Number | 输出图片宽度(默认为 width * 屏幕像素密度) |
destHeight | Number | 输出图片高度(默认为 height * 屏幕像素密度) |
canvasId | String | 画布标识,传入 的canvas-id(必须) |
fileType | String | 目标文件的类型,只支持 ‘jpg’ 或 ‘png’。默认为 ‘png’ |
quality | Number | 图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理 |
success | Function | 接口调用成功的回调函数 |
fail | Function | 接口调用失败的回调函数 |
complete | Function | 接口调用结束的回调函数(调用成功、失败都会执行) |
在draw
回调里调用该方法才能保证图片导出成功。
ctx.draw(false, function() {
wx.canvasToTempFilePath({
canvasId: 'myCanvas',
success: function(res) {
// 获得图片临时路径
this.imageTempPath = res.tempFilePath;
}
})
});
保存图片到系统相册。需要用户授权scope.writePhotosAlbum
wx.authorize(OBJECT)
提前向用户发起授权请求。调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口。如果用户之前已经同意授权,则不会出现弹窗,直接返回成功。
可以通过wx.getSetting
先查询一下用户是否授权
// 可以通过 wx.getSetting 先查询一下用户是否授权了 "scope.record" 这个 scope
wx.getSetting({
success(res) {
if (!res.authSetting['scope.record']) {
wx.authorize({
scope: 'scope.record',
success() {
// 用户已经同意小程序使用录音功能,后续调用 wx.startRecord 接口不会弹窗询问
wx.startRecord()
}
})
}
}
})
在生成朋友圈图片的前提下,没有必要提前向用户发起授权请求,所以直接使用saveImageToPhotosAlbum
时获取授权即可。
wx.saveImageToPhotosAlbum()
wx.saveImageToPhotosAlbum(OBJECT)
OBJECT参数:
参数 | 类型 | 说明 |
---|---|---|
filePath | String | 图片文件路径,可以是临时文件路径也可以是永久文件路径,不支持网络图片路径 |
success | Function | 接口调用成功的回调函数 |
fail | Function | 接口调用失败的回调函数 |
complete | Function | 接口调用结束的回调函数(调用成功、失败都会执行) |
wx.saveImageToPhotosAlbum({
filePath: this.filePath,
success(res) {
console.log(res.errMsg)
},
fail(res) {
console.log(res.errMsg)
}
})
position
设定在屏幕很远之外,或者设定visibity
为hidden
,但不能通过v-if
控制父容器的显示(即设定display
为none
),否则无法正常生产图片saveImageToPhotosAlbum
)临时图片路径已经生成成功(canvasToTempFilePath
),而临时图片路径的成功生成又依赖于Canvas绘制完毕(draw
),所以最好将有依赖关系的调用都放到其success
回调函数中,集体来看分两种情况sucess
回调中,这样保证用户点击分享(正常情况下)不会有额外提示,流程顺畅,但是分享时间会相对较长demo在这里。
注意,有一个问题,使用诸如wx.getImageInfo
等借口异步获取图片信息的时候,如果最后的draw
方法不在回调函数中,会导致回调中对应的绘画不被绘制到canvas
上
本以为掌握了以上技巧,写了个小demo就能飞黄腾达直达人生巅峰了实现业务需求了,结果实践证明我还是too naive too young了,还有多个坑在等着我呢
这些坑有的是canvas的坑,有的是小程序的坑,更多的是自己脑子进水的坑。
绘制网络图片时需要首先通过wx.getImageInfo
获取图片的临时路径,但是如果图片地址的域名没有被设置到小程序管理后台的downloadFile
的白名单中,就无法获取图片的临时路径。
在调试时我们一般会打开“不校验域名”的选项,这就导致了图片在调试工具中处理成功,而在真机环境失败。
解决这个问题就是登陆小程序管理后台的设置
→开发设置
→服务器域名设置
中,将图片域名添加到downloadFileh合法域名
的白名单中,对于网络请求也一样,需要添加到request合法域名
中
canvas是原生组件,总是处于顶级的,是没有办法通过CSS的z-index来控制其上下层的显示的,解决方法就是通过小程序提供的cover-view
组件
cover-view
会覆盖在原生组件之上,可以覆盖的原生组件包括map
、video
、canvas
、camera
、live-player
、live-pusher
,其内部只能嵌套cover-view
和cove-image
cover-image
是覆盖在原生组件的图片视图,通过src属性设置图片的临时路径、网络地址,暂不支持base64格式
呃,截止到2018.06.14,canvas不支持base64格式的图片
因为测试环境下,网络请求都是走的内网地址,首先PC能够访问成功是因为配置了host,但是手机没有配置,导致网络请求失败
解决方法就是通过fiddler为手机设置代理,让手机的网络请求也走内网,前提是手机也连入了PC相同的局域网,具体的设置方法参考这篇文章。
在小程序开发工具更新后可以像在chrome中一样进行断点调试,但是在开发过程中遇到了无法打断点的情况,找到对应的.vue
或.js
文件,打了断点执行时不会在期待的地方停止程序的运行
原因就是开启了开发工具的ES6转ES5
的选项,关闭这个选项再打断点就OK了。
原有的流程是这样的:首先进入列表A,点击其中某个连接进入页面B1,B1中生成canvas并绘制到页面上,在B1页面可以点击保存按钮将canvas绘制的图片保存到相册,也可以点击canvas进行图片预览
本来想进行一个优化,因为保存相册和图片预览都需要通过wx.canvasToTempFilePath
获取canvas转换为图片的临时路径,所以将临时图片保存到了组件的data中的this.filePath
,然后保存图片或者游览时判断是否已经有了临时路径,如果有了就省略canvasToTempFilePath
这一步
但是带来了问题:在进入B1生成了图片P1并预览后,退出到列表A,进入B2生成图片P2,但是预览时并不是P2,还是P1
问题原因其实不难查找,出现在小程序路由的触发与生命周期函数的对应上:
当页面返回时,并没有触发mpvue的destroy
生命周期,也就是说实例没有被销毁,其属性都保存了,所以以后再次进入B1/B2/…,图片预览或者保存都是P1
这和vue-router跳转不同组件时的行为表现是不同的,与使用了动态参数的vue-router跳转的行为表现相同:
当使用路由参数时,例如从
/user/ligang
导航到user/lg
,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
解决方法有两个:
1. 不优化了,将canvasToTempFilePath
放在canvas绘制图片draw
方法的回调中,每次绘制图片都生成新的临时路径
2. 每次返回时当前页面触发的是小程序自己的声明周期onUnload
,所以在这个钩子中将this.filePath
清空
P.S. 在图片预览时触发的当前页面的钩子是onHide
如果canvas生成的图片过大,在进行图片预览时会被上方小程序自带的页面信息遮挡:
解决方法就是别在图片的这里写东西在canvasToTempFilePath
控制生成图片的尺寸,可以将画布宽度高度全部输入,但是通过输出图片的宽度、高度控制来避免图片过大,当然这会带来图片的压缩和变形,这就要进行取舍了。