react中用canvas实现手势图案解锁设置

1,402 阅读6分钟

前言

该组件的ui参照了招商银行app的手势登录设置和手势登录验证页面,用canvas实现了设置手势密码的功能和验证手势密码的功能,绘制手势时按顺序记录所有经过的九宫格的点,作为手势密码。

其中,绘制手势时,连接不能少于4个点,每个点最多经过一次,不限制设置密码的长度:

  • 设置手势密码时,第一次绘制时,上方小九宫格会记录密码经过的点,作为再次绘制解锁图案时的提示,第二次绘制图案与第一次手势相符时,则绘制成功,否则重新绘制;
  • 验证手势密码时,绘制手势密码后验证通过,则解锁成功,否则提示密码错误;

1、绘制九宫格界面

由于首次进入页面就应该展示九宫格界面,所以将绘制图案的初始化写在componentDidMount()生命周期函数中,建两个canvas对象:用于提示的手势图案ctxhow和用于绘制手势的ctx,并且初始化九宫格解锁界面;

 //初始化界面
function init() {
    ctx.clearRect(0, 0, width, height); //清空画布
    ctxshow.clearRect(0, 0, widthShow, heightShow);
    pointerArr = []; //清除绘制路径
    for (var i = 0; i < arr.length; i++) {
        arr[i].state = 0; //清除绘制状态
        arr[i].stateShow = 0; 
        drawPointer(i);
    }
}

初始化9个点的坐标对象,再循环赋值每个点的坐标、颜色和连接状态,由于展示手势的九宫格需要在第二次绘制时做提示作用,所以连接状态不能与绘制九宫格共用变量;

//九宫格中9个点的坐标对象
let lockCicle = {
    x: 0, //x坐标
    y: 0, //y坐标
    showX: 0,   // 手势展示的点坐标
    showY: 0,
    color: "#999999",
    state: "1", // 当前点状态,是否已经被链接过
    stateShow: "1", // 手势展示的点状态
};
//计算九个点坐标
for (let i = 1; i <= 3; i++) {
    for (let j = 1; j <= 3; j++) {
        let 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);
    }
}

2、绘制路径

  • 在九宫格界面所在的dom元素上绑定点击滑动等事件,绘制路径;
canvasTouchMove = (e) => {
    if (isMouseDown) {
        let x1 = e.targetTouches[0].pageX;
        let y1 = e.targetTouches[0].pageY - canvas.offsetTop;
        this.drawLinePointer(x1, y1, true);
    }
}
  • 用isPointInPath判断九宫格的点(x, y)是否在当前滑动的路径中,如过滑到了坐标点且该点未被选中过,则保存该点至路径数组中,并且改变该坐标点的样式;
// 保存经过的点
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();
}

3、结束绘制后的事件

验证手势密码时:

  • (1)密码小于4位。提示'至少连接4个点,请重新绘制',且清空所有九宫格;
  • (2)密码大于4位。判断密码是否正确,正确则提示"密码解锁成功",错误则提示"密码错误!请重新输入"。清空所有九宫格;

设置手势密码:

  • (1)密码小于4位。提示'至少连接4个点,请重新绘制',判断是否第二次绘制,是则清空所有九宫格;否则是第一次绘制,只清空绘制九宫格,上部分的展示九宫格留着;
  • (2)密码大于4位。判断是否第二次绘制: ① 是则判断第二次绘制是否与第一次绘制图案一致,一致的话,提示"手势密码设置成功"后清空九宫格;不一致的话提示'两次绘制图案不一致,请重新绘制'后清空九宫格,延时两秒后回到第一次绘制; ② 否则是第一次绘制,保存绘制图案,只清空绘制九宫格,上部分的展示九宫格留着; 具体代码实现如下:

PatternLock.jsx

import React, { Component } from "react";
import { observer, inject } from "mobx-react";
import { withRouter } from "react-router";
import MainPage from "../EmptyContainer/EmptyContainer";
import { SwipeAction, List } from 'antd-mobile';
import Navbar from '../Components/Navbar/Navbar'

const navBarStyle = { background: '#fff', color: '#333' }
const leftIcon = require('../../static/images/SettingBack.png')

let pointerArr = []; // 绘制路径
let startX, startY; //线条起始点
let puts = []; //经过的九个点的数组
let currentPointer; //当前点是否已经连接
let pwd = [1,2,4,5,7]; //密码
let confirmPwd = []; //确认密码
let unlockFlag = false; //是否解锁的标志
let isMouseDown = false;
let arr = [];  //九个点的坐标数组
let canvas, ctx, width, height;
let canvasShow, ctxshow, widthShow, heightShow;

import "./PatternLock.less";
import deviceStore from "../../store/deviceStore";

@inject("doorStore", "deviceStore")
@withRouter
@observer
class PatternLock extends Component {
    constructor(props) {
        super(props);
        this.state = {
            patternPassWord: [],
            setOrCheck: true,  // 设置或验证手势密码页面 设置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个点的坐标对象
        let lockCicle = {
            x: 0, //x坐标
            y: 0, //y坐标
            showX: 0,   // 手势展示的点坐标
            showY: 0,
            color: "#999999",
            state: "1", // 当前点状态,是否已经被链接过
            stateShow: "1", // 手势展示的点状态
        };
        let offset = (width - height) / 2; //计算偏移量
        arr = []; //九个点的坐标数组
        //计算九个点坐标
        for (let i = 1; i <= 3; i++) {
            for (let j = 1; j <= 3; j++) {
                let 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 (var i = 0; i < arr.length; i++) {
                arr[i].state = 0; //清除绘制状态
                arr[i].stateShow = 0; 
                drawPointer(i);
            }
        }
        //初始化界面
        init();
        // *****
        // 绘制九宫格解锁界面
        // *****
        function drawPointer(i) {
            ctx.save();
            ctxshow.save();
            let 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 (var i = 0; i < arr.length; i++) {
            arr[i].state = 0; //清除绘制状态
            if(flag) {
                arr[i].stateShow = 0; 
            } 
            this.drawPointer(i, flag);
        }
    }
    drawPointer = (i, flag) => {
        let 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;
        let x1 = e.targetTouches[0].pageX;
        let y1 = e.targetTouches[0].pageY - canvas.offsetTop;
        this.drawLinePointer(x1, y1, false);
    }
    canvasTouchMove = (e) => {
        if (isMouseDown) {
            let x1 = e.targetTouches[0].pageX;
            let 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() {

        return (
            <div className="PLcontainer">
                <Navbar navBarStyle={navBarStyle} backKey={1} pageTitle={this.state.setOrCheck ? '设置手势密码' : '验证手势密码'} leftIcon={leftIcon} />
                <div className="PatternLock">
                    <div className="showPattern" ref="showPattern">
                        <canvas id="canvasShow"></canvas>
                    </div>
                    <div className="tips">
                        <div className="inputTips">
                            <p>{this.state.setAgain?'请再次绘制解锁图案':'请绘制解锁图案'}</p>
                            <div className="guide">
                                <p className={`guideTips${this.state.tooEasy||!this.state.isFit?'':' hide'}`}>
                                    {this.state.isFit?'至少连接4个点,请重新绘制':'两次绘制图案不一致,请重新绘制'}
                                </p>
                            </div>                           
                        </div>
                    </div>
                    <div className="drawPattern" ref="drawPattern">
                        <canvas
                            id="canvas"
                            onTouchStart={this.canvasTouchStart}
                            onTouchMove={this.canvasTouchMove}
                            onTouchEnd={this.canvasTouchEnd}
                        ></canvas>
                    </div>
                </div>
            </div>
        );
    }
}
export default PatternLock;

PatternLock.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 {
        margin-top: 50px;
        margin-bottom: 40px;
        font-size: 36px;
        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: 958px;
    }
  }
}