持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
使用Canvas实现的手写签名,由page-container弹窗模拟全屏,在弹窗上横屏手写签名之后,将签名转为正常竖屏显示
效果
签名前 | 横屏签名 | 将横屏签名转正 |
---|---|---|
动画效果:
实现思路
- 弹窗使用 page-container组件。好处是当用户进行返回操作,关闭该容器不关闭页面。返回操作包括三种情形,右滑手势、安卓物理返回键和调用
navigateBack
接口 - 使用一个canvas进行手写签名的绘制
- 将手写签名的canvas的内容转为图片,将图片旋转绘制到另一个canvas后,在将这个canvas转为图片,最终显示
这里用到了两个canvas,第一个是用来记录手写的,第二个是为了旋转签名内容的。因为用户有可能会重复签名,所以第一个canvas上手写的内容,需要保留,旋转图片只能另外用一个canvas,第二个canvas是隐藏的状态。
page-container
小程序如果在页面内进行复杂的界面设计(如在页面内弹出半屏的弹窗、在页面内加载一个全屏的子页面等),用户进行返回操作会直接离开当前页面,不符合用户预期,预期应为关闭当前弹出的组件。 为此提供“假页”容器组件,效果类似于
popup
弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。返回操作包括三种情形,右滑手势、安卓物理返回键和调用navigateBack
接口
相比自己实现的popup弹窗,其最大的特点就是能响应用户的返回操作,而不至于直接离开当前页面,简单用法:
<page-container show="{{showPage}}" close-on-slideDown="{{false}}">
<view style="height: 100vh;">
<!--弹窗内容-->
<!--放置第一个canvas用于手写签名的绘制-->
</view>
</page-container>
实现手写canvas
重点代码说明(完整代码请查看后文)
//创建 canvas 的绘图上下文 CanvasContext 对象,第一个参数为canvas 组件 canvas-id 属性值
this.canvasContext = wx.createCanvasContext('canvas');
/* 设置线条颜色 */
this.canvasContext.setStrokeStyle('#0081ff'); //2A2A2A
/* 设置线条粗细 */
this.canvasContext.setLineWidth(4);
/* 设置线条的结束端点样式 */
this.canvasContext.setLineCap('round');
/* 手写画线 */
this.canvasContext.moveTo(this.drawStartX, this.drawStartY);
this.canvasContext.lineTo(tempX, tempY);
this.canvasContext.stroke();
/* draw(xxx),参数默认是false,表示绘制前会将画布清空;true表示不清空画布。这里写true */
this.canvasContext.draw(true);
//清空画布,使用draw()即可,默认参数false,表示绘制前会将画布清空
this.canvasContext.draw();
签名旋转
由第一个canvas生成图片,在将这张图片旋转绘制到新的canvas
画布canvas内容导出生成图片使用wx.canvasToTempFilePath(Object object, Object this)
画布绘制图片使用drawImage
方法
图片需要旋转270°
图片的旋转实际上是要旋转canvas,使用CanvasContext.rotate(number rotate)
,另外还需要将原点移动到中心,方便图片居中显示。原点移动使用CanvasContext.translate(number x, number y)
。旋转后的canvas示意图如下所示:
其他细节
- 在使用wx.canvasToTempFilePath()将canvas内容转为图片时,需要在
draw()
回调里调用该方法才能保证图片导出成功,示例
_this.canvas.draw(false, ()=>{
wx.canvasToTempFilePath({})
})
- 隐藏canvas,可以使用下列代码
position:fixed;left:100%;
page-container与canvas搭配的bug??
大概率出现的情况:page-container设置为隐藏后,canvas画布没有隐藏,触摸一下屏幕,又隐藏了。目前解决方式就是隐藏page-container后把canvas的style设置为position:fixed;left:100%;
,手动将canvas隐藏
完整代码
.js文件
Page({
data: {
sign: null,
isSignatureFinish: false,
moveLength: 0, //移动一定距离后才能提交保存
showPage: false,
canvasTempWidth: wx.getSystemInfoSync().screenWidth,
canvasTempHeight: wx.getSystemInfoSync().screenWidth * 0.5,
},
onReady() {
//第一个canvas
this.canvasContext = wx.createCanvasContext('canvas');
this.initCanvas()
//第二个canvas
this.ctx2 = wx.createCanvasContext('canvas-temp');
},
showSignPage() {
this.setData({
showPage: true
});
},
hideSignPage() {
this.setData({
showPage: false
});
},
confirmSign() {
let _this = this
_this.generateSignImage('canvas').then(res => {
console.log('图片地址:', res)
wx.getImageInfo({
src: res,
success(info) {
let width = info.width
let height = info.height
//先重置canvas-temp
_this.ctx2.draw(false, ()=>{
let dx = _this.data.canvasTempWidth
let dy = _this.data.canvasTempHeight
//把原点移动到中心点位置
_this.ctx2.translate(dx / 2, dy / 2)
_this.ctx2.rotate(270 * Math.PI / 180)
let dWidth = dx / height * width
let dHeight = dx
//drawImage参数说明
//imageResource
//imageResource的左上角在目标 canvas 上 x 轴的位置
//imageResource的左上角在目标 canvas 上 y 轴的位置
//在目标画布上绘制 imageResource 的宽度,允许对绘制的 imageResource 进行缩放
//在目标画布上绘制 imageResource 的高度,允许对绘制的 imageResource 进行缩放
_this.ctx2.drawImage(res, -dWidth / 2,-dHeight / 2, dWidth, dHeight)
//canvas-temp图片绘制完成后
_this.ctx2.draw(false, ()=>{
_this.generateSignImage('canvas-temp').then(res => {
console.log('图片地址:', res)
_this.setData({
sign: res
})
_this.hideSignPage()
})
})
})
}
})
})
},
initCanvas() {
/* 设置线条颜色 */
this.canvasContext.setStrokeStyle('#0081ff'); //2A2A2A
/* 设置线条粗细 */
this.canvasContext.setLineWidth(4);
/* 设置线条的结束端点样式 */
this.canvasContext.setLineCap('round');
},
/* 触摸开始 */
handleTouchStart(e) {
console.log(e)
this.drawStartX = e.changedTouches[0].x;
this.drawStartY = e.changedTouches[0].y;
console.log('触摸开始', this.drawStartX, this.drawStartY)
this.canvasContext.beginPath();
},
/* 触摸移动 */
handleTouchMove(e) {
/* 记录当前位置 */
const tempX = e.changedTouches[0].x;
const tempY = e.changedTouches[0].y;
// console.log('触摸移动', tempX, tempY)
this.data.moveLength += Math.abs(this.drawStartX - tempX) + Math.abs(this.drawStartY - tempY)
/* 画线 */
this.canvasContext.moveTo(this.drawStartX, this.drawStartY);
this.canvasContext.lineTo(tempX, tempY);
this.canvasContext.stroke();
/* 旧版draw方法,新版本不需要draw */
this.canvasContext.draw(true);
/* 重新记录起始位置 */
this.drawStartX = tempX;
this.drawStartY = tempY;
},
/* 触摸结束 */
handleTouchEnd(e) {
console.log('触摸结束')
this.canvasContext.save();
this.setData({
isSignatureFinish: this.data.moveLength > 100
})
},
/* 触摸取消 */
handleTouchCancel(e) {
console.log('触摸取消')
this.canvasContext.save();
},
/* 清空画布 */
clearCanvas() {
console.log('清空画布')
this.canvasContext.draw()
this.initCanvas()
this.data.moveLength = 0
this.setData({
isSignatureFinish: false
})
},
/* 生成签名图片 */
generateSignImage(canvasId) {
console.log('Canvas生成图片')
return new Promise((resolve, reject) => {
wx.canvasToTempFilePath({
x: 0,
y: 0,
canvasId: canvasId, // 旧版使用id
fileType: 'png',
quality: 1,
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
reject(err);
}
})
})
},
})
.wxml
<view class="image-container" bindtouchend="showSignPage">
<image mode="aspectFit" src="{{sign}}" />
<text wx:if="{{!sign}}" class="text-center">签名区域</text>
</view>
<button style="margin-top: 30rpx;" bindtap="submitSignature" disabled="{{!isSignatureFinish}}">提交保存</button>
<page-container show="{{showPage}}" overlay="{{false}}" round="{{false}}" close-on-slideDown="{{false}}" z-index="1000">
<view class="flex" style="height: 100vh;">
<view class="sign-btn-container">
<view class="cu-btn" bindtap="hideSignPage">取消/返回</view>
<view class="cu-btn {{isSignatureFinish?'bg-blue light':''}}" bindtap="clearCanvas">重新签名</view>
<view class="cu-btn {{isSignatureFinish?'bg-blue light':''}}" bindtap="confirmSign">完成签名</view>
</view>
<canvas id="canvas" style="position:{{showPage?'relative':'fixed'}};left:{{showPage?'':'100%'}};" canvas-id="canvas" class="canvas" :disable-scroll="true" bindtouchstart="handleTouchStart" bindtouchmove="handleTouchMove" bindtouchend="handleTouchEnd" bindtouchcancel="handleTouchCancel" disable-scroll="true" />
</view>
</page-container>
<canvas id="canvas-temp" style="width: {{canvasTempWidth}}px; height: {{canvasTempHeight}}px;" canvas-id="canvas-temp" class="canvas-temp"></canvas>
wxss
.flex{
display: flex;
}
/* 签名 */
.canvas {
width: 90vw;
height: 100vh;
}
.canvas-temp {
position:fixed;left:100%;
}
.sign-btn-container {
display: flex;
flex-direction: column;
height: 100%;
width: 10vw;
justify-content: space-around;
}
.sign-btn-container view {
transform: rotate(90deg);
width: 100px;
margin-left: -8vw;
}
.image-container {
margin-top: 50rpx;
width: 100%;
height: 320rpx;
position: relative;
}
.image-container image {
border: 1rpx dashed #eee;
position: absolute;
z-index: 1;
height: 320rpx;
width: 98vw;
}
.image-container text {
position: absolute;
text-align: center;
height: 100%;
width: 100%;
line-height: 320rpx;
z-index: 2;
}
完整demo代码下载:wwnl.lanzoul.com/iIeEt0qr3xp…