React移动端使用canvas实现手势密码登录

本文详细介绍了如何使用JavaScript和Ant Design Mobile在移动端实现一个基于九宫格的手势密码解锁组件。通过Canvas API绘制路径,并处理用户触碰事件,实现设置或验证密码的功能。
摘要由CSDN通过智能技术生成

 先上效果图:

 

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;
    }
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值