微信小程序实现电子签名
这是我参与8月更文挑战的第5天,活动详情查看: 8月更文挑战
背景
因小程序业务需要用户签单确认订单,确认后生成图片回显。整理一下实现的过程和碰到的问题。 实现效果如图:
代码实现
1.需求分析
看的这个需求先把需求拆解一下,梳理一下实现的流程,参照上图,可以看出实现的大概步骤为:
- 页面横屏,页面布局,初始化
Canvas
容器; Canvas
绘制操作提醒等文字;touchstart()
事件创建路径的起点,将其坐标存入路径坐标数组中;touchmove()
事件中不断往数组中存入路径坐标;touchend()
事件清空数组,以便下一次的绘制;
2.手操实现
1.页面横屏,页面布局,初始化Canvas
容器
首先是第一步,因为是在小程序内,需要将手机横屏,以获取较大的空间,小程序内可以通过设置"pageOrientation": "landscape"
实现,这种方式是最方便的,如果使用画布旋转的方式,最终生成的图片也是旋转的,还需要反向旋转回来。再一个小程序Canvas
组件在(本文末尾会附上完整代码,这里只对js部分做说明)。
initCanvas() {
let {
width,
height
} = this.data
width = wx.getSystemInfoSync().windowWidth
height = wx.getSystemInfoSync().windowHeight
this.data.ctx = wx.createCanvasContext('canvas',this)
this.setData({
width,
height
})
this.clearCanvas()
},
因为ctx
对象需要在多个方法里共享,所以可以将其定义在data
中或者外部变量。
2.Canvas
绘制操作提醒等文字
这一步绘制的是提醒文字,文字左右居中显示可以设置setTextAlign('center')
然后fillText
时x轴的位置为宽度的一半。
clearCanvas() {
this.data.drawCount = 0
this.data.ctx.setTextBaseline('top')
this.data.ctx.setTextAlign('center')
this.data.ctx.setFontSize(20)
this.data.ctx.setFillStyle('#616165');
this.data.ctx.fillText("请在灰色区域内完成签名", this.data.width / 2, 30)
this.data.ctx.draw(false)
},
3.touchstart()
事件创建路径的起点,将其坐标存入路径坐标数组中
上一步绘制的提醒文字类似于输入框的placeholder
需要在Canvas
被触发事件时清除掉,所以在这里判断一下如果是第一次触发的话就调用一下draw(false)
事件,false
参数代表覆盖上一次的绘制,传入true
的话是保留上一次的绘制
catchtouchstart(e) {
if (this.data.drawCount == 0) {
this.data.ctx.draw(false)
}
this.data.drawCount++
this.data.points.push(e.changedTouches[0])
},
touch事件中的touches和changedTouches的不同:
- touches: 当前屏幕上所有触摸点的列表;
- changedTouches: 触发当前事件的触摸点的列表
例:两个手指同时触发事件,此时这两个属性内元素都是2个;两个手指先后触发事件,并且第一个手指不离开屏幕的话,当第二个手指触发事件时changedTouches
内只有一个元素,为当前触发事件的触摸点信息,touches
内有两个元素,为当前两个手指所在触摸点的信息。
4.touchmove()
事件中不断往数组中存入路径坐标
因为在手势移动时需要实时绘制路线,所以将draw()
事件放在touchmove()
事件内执行。setShadow()
事件给轨迹设置阴影,使笔迹看起来比较圆滑。
catchtouchmove(e) {
if (e.touches.length > 1) {
return
}
this.data.points.push(e.changedTouches[0])
let points = this.data.points
for (let i in points) {
if (i == 0) {
this.data.ctx.moveTo(points[0].clientX, points[0].clientY)
} else {
this.data.ctx.lineTo(points[i].clientX, points[i].clientY)
}
}
this.data.ctx.setStrokeStyle('#000000');
this.data.ctx.setLineWidth(3);
this.data.ctx.setShadow(0, 0, 0.5, '#000000')
this.data.ctx.setLineCap('round');
this.data.ctx.setLineJoin('round');
this.data.ctx.stroke()
this.data.ctx.draw(true)
},
5.touchend()
事件清空数组,以便下一次的绘制
catchtouchend(e) {
this.data.points = []
},
3.代码测试优化
将代码运行在真机时,发现会随着绘制的轨迹越长开始产生卡顿:

因为每次绘制时,绘制的都是所有轨迹点,这样轨迹点越多消耗的性能越大。于是换一个思路:每两个点连成一条线段 touchstart()
事件触发时作为第一条线段的起点, touchmove()
事件触发时为第一条线段的终点,绘制出线段后再将这个点设为下一条线段的起点依次绘制。这样就很明显的解决了卡顿问题。
catchtouchstart(e) {
if (this.data.drawCount == 0) {
this.data.ctx.draw(false)
}
this.data.drawCount++
this.data.ctx.moveTo(e.changedTouches[0].clientX, e.changedTouches[0].clientY)
},
catchtouchmove(e) {
if (this.data.drawState == "stop") return
this.data.drawState = "ing"
if (e.touches.length > 1) {
return
}
this.data.ctx.setStrokeStyle('#000000');
this.data.ctx.setLineWidth(3);
this.data.ctx.setShadow(0, 0, 0.5, '#000000')
this.data.ctx.setLineCap('round');
this.data.ctx.setLineJoin('round');
this.data.ctx.lineTo(e.changedTouches[0].clientX, e.changedTouches[0].clientY)
this.data.ctx.stroke()
this.data.ctx.draw(true)
this.data.ctx.moveTo(e.changedTouches[0].clientX, e.changedTouches[0].clientY)
},
修改后的效果:

结尾
在真机测试时发现还会有多点触控的情况发生,想到的解决方案是多点触控时判断changedTouches[0]
的坐标与上一次绘制的坐标点作比较,小于某个值时则认为是同一个手指触发的行为。但是方案被产品否了,场景出现的几率较低,只需要判断单点触控时才绘制就可以。
文末附上完整代码:
<canvas canvas-id="canvas" style="width:{{width+'px'}};height:{{height+'px'}}" catchtouchstart="catchtouchstart" catchtouchmove="catchtouchmove" catchtouchend="catchtouchend"></canvas>
<view class="btn-reset" catchtap="clearCanvas">重新绘制</view>
<view class="btn-ok" catchtap="canvasToImg">确认</view>
Page({
data: {
ctx: null,
width: null,
height: null,
drawCount: 0,
drawState: "init"
},
onShow: function () {
this.initCanvas()
},
initCanvas() {
let {
width,
height
} = this.data
width = wx.getSystemInfoSync().windowWidth
height = wx.getSystemInfoSync().windowHeight
console.log(wx.getSystemInfoSync())
this.data.ctx = wx.createCanvasContext('canvas')
this.setData({
width,
height
})
this.clearCanvas()
},
clearCanvas() {
this.data.drawCount = 0
this.data.drawState = "ing"
this.data.ctx.setTextBaseline('top')
this.data.ctx.setTextAlign('center')
this.data.ctx.setFontSize(20)
this.data.ctx.setFillStyle('#616165');
this.data.ctx.fillText("请灰色区域内完成签名", this.data.width / 2, 30)
this.data.ctx.draw(false)
},
catchtouchstart(e) {
if (this.data.drawCount == 0) {
this.data.ctx.draw(false)
}
this.data.drawCount++
this.data.ctx.moveTo(e.changedTouches[0].clientX, e.changedTouches[0].clientY)
},
catchtouchmove(e) {
if (this.data.drawState == "stop") return
this.data.drawState = "ing"
if (e.touches.length > 1) {
return
}
this.data.ctx.setStrokeStyle('#000000');
this.data.ctx.setLineWidth(3);
this.data.ctx.setShadow(0, 0, 0.5, '#000000')
this.data.ctx.setLineCap('round');
this.data.ctx.setLineJoin('round');
this.data.ctx.lineTo(e.changedTouches[0].clientX, e.changedTouches[0].clientY)
this.data.ctx.stroke()
this.data.ctx.draw(true)
this.data.ctx.moveTo(e.changedTouches[0].clientX, e.changedTouches[0].clientY)
},
canvasToImg() {
if (this.data.drawState == "init") return
this.data.drawState = "stop"
wx.canvasToTempFilePath({
canvasId: 'canvas',
success: res => {
console.log(res.tempFilePath)
// ...
// 上传服务器
wx.navigateTo({
url: '/pages/showImg/showImg?src=' + res.tempFilePath,
})
}
})
}
})
page{
position: relative;
background-color: #f2f2f2;
width: 100%;
height: 100%;
}
canvas{
width: 100%;
height: 100vh;
}
.btn-reset{
width: 100rpx;
position: absolute;
bottom: 10rpx;
right: 160rpx;
padding: 8rpx;
text-align: center;
border: 1rpx solid #4965B3;
color: #4965B3;
font-size: 18rpx;
border-radius: 8rpx;
box-sizing: border-box;
}
.btn-ok{
width: 100rpx;
position: absolute;
bottom: 10rpx;
right: 30rpx;
padding: 8rpx;
text-align: center;
background-color: #4965B3;
border: 1rpx solid #4965B3;
color: #fff;
font-size: 18rpx;
border-radius: 8rpx;
box-sizing: border-box;
}
转载自:https://juejin.cn/post/7000633542153601055