先上效果图:
index.jsx
import React, { Component } from "react";
import { router } from 'umi';
import { Toast, List, NavBar, Icon } from 'antd-mobile';
import styles from "./index.less";
let pointerArr = []; // 绘制路径
let startX = 0;
let startY = 0; // 线条起始点
let puts = []; // 经过的九个点的数组
let currentPointer = null; // 当前点是否已经连接
const pwd = [1,2,4,5,7];// 密码
const confirmPwd = [];// 确认密码
const unlockFlag = false;// 是否解锁的标志
let isMouseDown = false;
let arr = [];// 九个点的坐标数组
let canvas = null;
let ctx = null;
let width = null;
let height = null;
let canvasShow = null;
let ctxshow = null;
let widthShow = null;
let heightShow = null;
class PatternLock extends Component {
constructor(props) {
super(props);
this.state = {
patternPassWord: [],
setOrCheck: false,// 设置或验证手势密码页面设置true获取false
tooEasy: false, // 至少连接4个点
setAgain: false, // 再次设置手势
isFit: true, // 第二次设置手势是否与第一次一致
}
}
componentDidMount() {
canvas = document.getElementById('canvas'); // 绘制手势画布
ctx = canvas.getContext('2d'); // 得到画布的上下文对象
canvas.width = this.refs.drawPattern.clientWidth;
canvas.height = this.refs.drawPattern.clientHeight;
width = canvas.width;
height = canvas.height; // 画布的宽高
canvasShow = document.getElementById('canvasShow'); // 手势展示的画布
ctxshow = canvasShow.getContext('2d');
canvasShow.width = this.refs.showPattern.clientWidth;
canvasShow.height = this.refs.showPattern.clientHeight;
widthShow = canvasShow.width;
heightShow = canvasShow.height; // 画布的宽高
console.log(canvas.offsetTop, width, height)
// 九宫格中9个点的坐标对象
const lockCicle = {
x: 0, // x坐标
y: 0, // y坐标
showX: 0, // 手势展示的点坐标
showY: 0,
color: "#999999",
state: "1", // 当前点状态,是否已经被链接过
stateShow: "1", // 手势展示的点状态
};
const offset = (width - height) / 2; // 计算偏移量
arr = []; // 九个点的坐标数组
// 计算九个点坐标
for (let i = 1; i <= 3; i++) {
for (let j = 1; j <= 3; j++) {
const lockCicle = {};
if (offset > 0) { // 横屏
lockCicle.x = (height / 4) * j + Math.abs(offset);
lockCicle.y = (height / 4) * i - height / 5;
lockCicle.state = 0;
lockCicle.stateShow = 0;
} else { // 竖屏
lockCicle.x = (width / 4) * j;
lockCicle.y = (width / 4) * i + Math.abs(offset) - height / 5;
lockCicle.state = 0;
lockCicle.stateShow = 0;
}
lockCicle.showX = (heightShow / 4) * j;
lockCicle.showY = (heightShow / 4) * i;
arr.push(lockCicle);
}
}
// 初始化界面
function init() {
ctx.clearRect(0, 0, width, height); // 清空画布
ctxshow.clearRect(0, 0, widthShow, heightShow);
pointerArr = []; // 清除绘制路径
for (let i = 0; i < arr.length; i++) {
arr[i].state = 0; // 清除绘制状态
arr[i].stateShow = 0;
drawPointer(i);
}
}
// 初始化界面
init();
// *****
// 绘制九宫格解锁界面
// *****
function drawPointer(i) {
ctx.save();
ctxshow.save();
const radius = width / 12;
let _fillStyle = "#ccc";
let _strokeStyle = "#ccc";
let _strokeStyleShow = "#ccc";
if (arr[i].state == 1) { // 不同状态显示不同颜色
_strokeStyle = "#0286fa";
_fillStyle = "#0286fa";
}
if(arr[i].stateShow == 1) {
_strokeStyleShow = "#0286fa";
}
// 绘制原点
ctx.beginPath(); // 起始一条路径
ctx.fillStyle = _fillStyle; // 填充颜色
ctx.arc(arr[i].x, arr[i].y, 6, 0, Math.PI * 2, false); // 创建曲线 false顺时针
ctx.fill();
ctx.closePath(); // 创建从当前点回到起始点的路径
// 绘制手势展示的原点
ctxshow.beginPath();
ctxshow.fillStyle = _strokeStyleShow;
ctxshow.arc(arr[i].showX, arr[i].showY, 4, 0, Math.PI * 2, false);
ctxshow.fill();
ctxshow.closePath();
// 绘制圆圈
ctx.beginPath();
ctx.strokeStyle = _strokeStyle;
ctx.lineWidth = 0.3;
ctx.arc(arr[i].x, arr[i].y, radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.closePath();
ctx.restore(); // 返回之前保存过的路径状态和属性
}
}
// 初始化界面
init = (flag) => { // flag:true表示需要格式化展示用的九宫格路径,false表示不需要
ctx.clearRect(0, 0, width, height); // 清空画布
if(flag){
ctxshow.clearRect(0, 0, widthShow, heightShow);
}
pointerArr = []; // 清除绘制路径
for (let i = 0; i < arr.length; i++) {
arr[i].state = 0; // 清除绘制状态
if(flag) {
arr[i].stateShow = 0;
}
this.drawPointer(i, flag);
}
}
drawPointer = (i, flag) => {
const radius = width / 12;
let _fillStyle = "#ccc";
let _strokeStyle = "#ccc";
let _strokeStyleShow = "#ccc";
if (arr[i].state == 1) { // 不同状态显示不同颜色
_strokeStyle = "#0286fa";
_fillStyle = "#0286fa";
}
if(arr[i].stateShow == 1) {
_strokeStyleShow = "#0286fa";
}
// 绘制原点
ctx.save();
ctx.beginPath(); // 起始一条路径
ctx.fillStyle = _fillStyle; // 填充颜色
ctx.arc(arr[i].x, arr[i].y, 6, 0, Math.PI * 2, false); // 创建曲线 false顺时针
ctx.fill();
ctx.closePath(); // 创建从当前点回到起始点的路径
// 绘制圆圈
ctx.beginPath();
ctx.strokeStyle = _strokeStyle;
ctx.lineWidth = 0.3;
ctx.arc(arr[i].x, arr[i].y, radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.closePath();
ctx.restore(); // 返回之前保存过的路径状态和属性
if(flag) {
// 绘制手势展示的原点
ctxshow.save();
ctxshow.beginPath();
ctxshow.fillStyle = _strokeStyleShow;
ctxshow.arc(arr[i].showX, arr[i].showY, 4, 0, Math.PI * 2, false);
ctxshow.fill();
ctxshow.closePath();
}
}
// *****
// 绘制连接线的方法,将坐标数组中的点绘制在canvas画布中
// *****
drawLinePointer = (x, y, flag) => {
// 绘制点到点的线
ctx.clearRect(0, 0, width, height); // 清空画布
ctx.save(); // 保存当前环境的状态
ctx.beginPath();
ctx.strokeStyle = "#0286fa";
ctx.lineWidth = 6;
ctx.lineCap = "round"; // 设置或返回线条的结束端点样式
ctx.lineJoin = "round"; // 相交时的拐角类型
for (var i = 0; i < pointerArr.length; i++) {
if (i == 0) {
ctx.moveTo(pointerArr[i].x, pointerArr[i].y);
} else {
ctx.lineTo(pointerArr[i].x, pointerArr[i].y);
}
}
ctx.stroke();
ctx.closePath();
ctx.restore();
// 保存经过的点
for (var i = 0; i < arr.length; i++) {
this.drawPointer(i,true); // 绘制圆圈和原点
// isPointInPath判断点(x, y)是否在路径中,有则返回true,否则返回false;同时判断该点是否已经经过
if (ctx.isPointInPath(x, y) && currentPointer != i && puts.indexOf(i + 1) < 0) {
pointerArr.push({
x: arr[i].x,
y: arr[i].y
});
currentPointer = i;
puts.push(i + 1); // 保存该坐标点到路径数组中
startX = arr[i].x;
startY = arr[i].y;
arr[i].state = 1;
if(!this.state.setAgain) { // 第二次设置手势,小九宫格不再做相应改变
arr[i].stateShow = 1;
}
}
}
// 绘制点到当前鼠标坐标的线
if (flag) {
ctx.save();
ctx.beginPath();
ctx.globalCompositeOperation = "destination-over"; // 在源图像上方显示目标图像
ctx.strokeStyle = "#e2e0e0";
ctx.lineWidth = 6;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.moveTo(startX, startY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.beginPath();
ctx.restore();
}
}
canvasTouchStart = (e) => {
isMouseDown = true;
const x1 = e.targetTouches[0].pageX;
const y1 = e.targetTouches[0].pageY - canvas.offsetTop;
this.drawLinePointer(x1, y1, false);
}
canvasTouchMove = (e) => {
if (isMouseDown) {
const x1 = e.targetTouches[0].pageX;
const y1 = e.targetTouches[0].pageY - canvas.offsetTop;
this.drawLinePointer(x1, y1, true);
}
}
canvasTouchEnd = (e) => {
this.drawLinePointer(0, 0, false);
isMouseDown = false;
pointerArr = [];
if(this.state.setOrCheck) { // 设置手势密码页面
if (puts.length >= 4) {
this.setState({
tooEasy: false
})
if(this.state.setAgain) { // 第二次设置手势
if(JSON.stringify(puts)==JSON.stringify(this.state.patternPassWord)) {
this.setState({
setAgain: false,
isFit: true
})
alert("手势密码设置成功")
} else {
console.log("两次绘制图案不一致,请重新绘制")
this.setState({
setAgain: false,
isFit: false
}, ()=>{
setTimeout(()=>{this.setState({isFit:true})},2000) // 两次绘制不一致,延时两秒后重新绘制
})
}
console.log(puts, this.state.patternPassWord,JSON.stringify(puts)==JSON.stringify(this.state.patternPassWord),'第二次设置手势')
this.init(true);
} else { // 第一次设置手势
this.setState({
setAgain: true,
patternPassWord: puts,
})
this.init(false);
}
console.log(`你的图案密码是: [ ${ puts.join(" > ") } ]`);
this.init(false);
} else if (puts.length >= 1) {
console.log("图案密码太简单了~~~");
this.setState({tooEasy:true})
if(this.state.setAgain) {
this.init(false);
} else {
this.init(true);
}
}
} else { // 验证手势页面
if (puts.length >= 4) {
this.setState({
tooEasy: false
})
if(JSON.stringify(puts)==JSON.stringify(pwd)) {
alert("密码解锁成功")
} else {
alert("密码错误!请重新输入")
}
console.log('你输入的密码:',JSON.stringify(puts),'实际密码:', JSON.stringify(pwd))
this.init(true);
} else if (puts.length >= 1) {
console.log("图案密码太简单了~~~");
this.setState({tooEasy:true})
this.init(true);
}
}
puts = [];
}
render() {
const { setOrCheck, setAgain, tooEasy, isFit } = this.state;
return (
<div className={styles.PLcontainer}>
<NavBar
mode="light"
icon={<Icon type="left" />}
onLeftClick={() => router.goBack()}
>{setOrCheck ? '设置手势密码': '验证手势密码'}
</NavBar>
<div className={styles.PatternLock}>
<div className={styles.showPattern} ref='showPattern'>
<canvas id="canvasShow" />
</div>
<div className={styles.tips}>
<div className={styles.inputTips}>
<p>{setAgain?'请再次绘制解锁图案':'请绘制解锁图案'}</p>
<div className={styles.guide} style={{ display: tooEasy||!isFit ? 'block' : 'none'}}>
<p className={styles.guideTips}>
{isFit?'至少连接4个点,请重新绘制':'两次绘制图案不一致,请重新绘制'}
</p>
</div>
</div>
</div>
<div className={styles.drawPattern} ref="drawPattern">
<canvas
id="canvas"
onTouchStart={this.canvasTouchStart}
onTouchMove={this.canvasTouchMove}
onTouchEnd={this.canvasTouchEnd}
/>
</div>
</div>
</div>
)
}
}
export default PatternLock
index.less
.PLcontainer {
width: 100%;
height: 100%;
background: #ffffff;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
min-height: 1%;
overflow-x: hidden;
overflow-y: hidden;
.PatternLock {
width: 100%;
height: 100%;
padding-top: 100px;
.showPattern {
width: 110px;
height: 110px;
margin: 0 auto;
}
.tips {
width: 100%;
.inputTips {
font-size: 28px;
text-align: center;
.guide {
height: 28px;
line-height: 28px;
margin-top: 26px;
.guideTips {
font-size: 26px;
color: #e91919;
&.hide {
display: none;
}
}
}
}
}
.drawPattern {
width: 100%;
height: 458px;
}
}
}