HarmonyOS 鸿蒙Next中如何绘制手势密码?

HarmonyOS 鸿蒙Next中如何绘制手势密码? **问题描述:**用户需要通过绘制“九宫格”手势密码进行身份验证(如解锁、支付确认)。

3 回复

详细回答:

我们可以使用以下技术组合实现:

1、Canvas:绘制 9 个圆点(节点)

2、TouchEvent:监听手指滑动路径

3、数学计算:判断手指是否经过某个点(基于距离阈值)

4、状态管理:记录已选中的点序列

5、回调通知:当用户抬起手指时,返回最终密码路径

✅ 支持:

1、自由绘制连接

2、自动吸附到最近点

3、防止重复选择

4、可视化绘制轨迹

✅ 正确做法

首先自定义一个PatternLockView控件

/**
 * [@author](/user/author) J.query
 * [@date](/user/date) 2025/12/26 13:27
 * [@email](/user/email) j-query@foxmail.com
 * Description: 手势密码组件
 */

/**
 * 手势密码节点信息
 */
interface PatternPoint {
  id: number;
  x: number;
  y: number;
  isSelected: boolean;
}

/**
 * 手势密码事件回调
 */
interface PatternLockCallbacks {
  onComplete?: (pattern: number[]) => void;     // 密码完成,返回数字数组
  onPatternStart?: () => void;                 // 开始绘制
  onPatternChange?: (pattern: number[]) => void; // 密码变化
  onPatternReset?: () => void;                 // 密码重置
}

/**
 * 手势密码配置
 */
interface PatternLockConfig {
  nodeRadius?: number;      // 节点半径
  nodeSpacing?: number;     // 节点间距
  lineWidth?: number;       // 连线宽度
  touchThreshold?: number;  // 触摸阈值
  minNodes?: number;        // 最小节点数
  maxNodes?: number;        // 最大节点数
}

[@Component](/user/Component)
export struct PatternLockView {
  @Prop config: PatternLockConfig = {
    nodeRadius: 12,
    nodeSpacing: 60,
    lineWidth: 3,
    touchThreshold: 25,
    minNodes: 4,
    maxNodes: 9
  };
  
  @Prop callbacks: PatternLockCallbacks = {};
  
  [@State](/user/State) private points: PatternPoint[] = [];
  [@State](/user/State) private selectedPattern: number[] = [];
  [@State](/user/State) private isDrawing: boolean = false;
  [@State](/user/State) private currentTouchX: number = 0;
  [@State](/user/State) private currentTouchY: number = 0;
  
  private canvasWidth: number = 300;  // 增大画布宽度
  private canvasHeight: number = 300; // 增大画布高度
  private startX: number = 0;         // 起始X坐标调整为0
  private startY: number = 0;         // 起始Y坐标调整为0
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D();

  aboutToAppear() {
    this.initializePoints();
    console.log('PatternLock 初始化:');
    console.log('画布尺寸:', this.canvasWidth, 'x', this.canvasHeight);
    console.log('节点数量:', this.points.length);
    console.log('节点间距:', this.config.nodeSpacing || 80);
    console.log('节点半径:', this.config.nodeRadius || 15);
  }

  /**
   * 初始化九宫格节点
   */
  private initializePoints() {
    this.points = [];
    const nodeRadius = this.config.nodeRadius || 15;  // 增大默认半径
    const nodeSpacing = this.config.nodeSpacing || 80; // 增大默认间距
    
    // 计算画布尺寸,确保足够大
    this.canvasWidth = nodeSpacing * 2 + nodeRadius * 2 + 60;
    this.canvasHeight = nodeSpacing * 2 + nodeRadius * 2 + 60;
    
    // 计算起始位置(居中)
    this.startX = nodeRadius + 30;
    this.startY = nodeRadius + 30;
    
    // 创建3x3的九宫格节点
    for (let row = 0; row < 3; row++) {
      for (let col = 0; col < 3; col++) {
        const point: PatternPoint = {
          id: row * 3 + col + 1,
          x: this.startX + col * nodeSpacing,
          y: this.startY + row * nodeSpacing,
          isSelected: false
        };
        this.points.push(point);
      }
    }
  }

  /**
   * 绘制手势密码
   */
  private drawPattern(canvas: CanvasRenderingContext2D) {
    // 清空画布
    canvas.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
    
    // 绘制背景矩形(用于调试)
    canvas.fillStyle = '#FFFACD';
    canvas.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
    
    // 强制绘制一些测试点确保Canvas工作
    canvas.beginPath();
    canvas.arc(50, 50, 10, 0, Math.PI * 2);
    canvas.fillStyle = '#FF0000';
    canvas.fill();
    
    canvas.beginPath();
    canvas.arc(150, 50, 10, 0, Math.PI * 2);
    canvas.fillStyle = '#00FF00';
    canvas.fill();
    
    canvas.beginPath();
    canvas.arc(250, 50, 10, 0, Math.PI * 2);
    canvas.fillStyle = '#0000FF';
    canvas.fill();
    
    // 绘制节点
    console.log('开始绘制节点,数量:', this.points.length);
    this.points.forEach((point, index) => {
      console.log(`绘制节点${index + 1}: id=${point.id}, x=${point.x}, y=${point.y}`);
      this.drawPoint(canvas, point);
    });
    
    // 绘制连线
    if (this.selectedPattern.length > 0) {
      this.drawLines(canvas);
    }
    
    // 绘制当前手指到最近的点的连线
    if (this.isDrawing && this.selectedPattern.length > 0) {
      this.drawCurrentLine(canvas);
    }
  }

  /**
   * 绘制调试网格(可选)
   */
  private drawDebugGrid(canvas: CanvasRenderingContext2D) {
    canvas.strokeStyle = '#FFE0E0';
    canvas.lineWidth = 1;
    canvas.setLineDash([5, 5]);
    
    // 绘制垂直线
    for (let i = 0; i <= 3; i++) {
      const x = this.startX + i * (this.config.nodeSpacing || 80);
      canvas.beginPath();
      canvas.moveTo(x, this.startY);
      canvas.lineTo(x, this.startY + 2 * (this.config.nodeSpacing || 80));
      canvas.stroke();
    }
    
    // 绘制水平线
    for (let i = 0; i <= 3; i++) {
      const y = this.startY + i * (this.config.nodeSpacing || 80);
      canvas.beginPath();
      canvas.moveTo(this.startX, y);
      canvas.lineTo(this.startX + 2 * (this.config.nodeSpacing || 80), y);
      canvas.stroke();
    }
    
    canvas.setLineDash([]);
  }

  /**
   * 绘制单个节点
   */
  private drawPoint(canvas: CanvasRenderingContext2D, point: PatternPoint) {
    const nodeRadius = this.config.nodeRadius || 15;

    // 绘制外圆
    canvas.beginPath();
    canvas.arc(point.x, point.y, nodeRadius, 0, Math.PI * 2);

    if (point.isSelected) {
      // 选中状态:蓝色填充
      canvas.fillStyle = '#0A59F7';
      canvas.fill();
      canvas.strokeStyle = '#003D99';
      canvas.lineWidth = 3;
      canvas.stroke();
    } else {
      // 未选中状态:红色填充,黑色边框(增强对比度)
      canvas.fillStyle = '#FF6B6B';
      canvas.fill();
      canvas.strokeStyle = '#000000';
      canvas.lineWidth = 3;
      canvas.stroke();
    }

    // 绘制中心点(用于增强可见性)
    canvas.beginPath();
    canvas.arc(point.x, point.y, 5, 0, Math.PI * 2);
    canvas.fillStyle = point.isSelected ? '#0A59F7' : '#0A59F7';
    canvas.fill();

    // 绘制节点ID(调试用)
    canvas.fillStyle = '#000000';
    canvas.font = '40px Arial';
    canvas.textAlign = 'center';
    canvas.textBaseline = 'middle';
    canvas.fillText(point.id.toString(), point.x, point.y);
  }


  /**
   * 绘制已选中节点之间的连线
   */
  private drawLines(canvas: CanvasRenderingContext2D) {
    const selectedPoints = this.points.filter(p => p.isSelected);
    if (selectedPoints.length < 2) return;
    
    canvas.beginPath();
    canvas.moveTo(selectedPoints[0].x, selectedPoints[0].y);
    
    for (let i = 1; i < selectedPoints.length; i++) {
      canvas.lineTo(selectedPoints[i].x, selectedPoints[i].y);
    }
    
    canvas.strokeStyle = '#0A59F7';
    canvas.lineWidth = this.config.lineWidth || 3;
    canvas.stroke();
  }

  /**
   * 绘制当前手指位置到最新选中点的连线
   */
  private drawCurrentLine(canvas: CanvasRenderingContext2D) {
    const selectedPoints = this.points.filter(p => p.isSelected);
    if (selectedPoints.length === 0) return;
    
    const lastPoint = selectedPoints[selectedPoints.length - 1];
    
    canvas.beginPath();
    canvas.moveTo(lastPoint.x, lastPoint.y);
    canvas.lineTo(this.currentTouchX, this.currentTouchY);
    canvas.strokeStyle = '#0A59F7';
    canvas.lineWidth = this.config.lineWidth || 3;
    canvas.stroke();
  }

  /**
   * 获取触摸位置对应的节点
   */
  private getPointAt(x: number, y: number): PatternPoint | null {
    const threshold = this.config.touchThreshold || 25;
    
    for (const point of this.points) {
      const distance = Math.sqrt(
        Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)
      );
      
      if (distance <= threshold && !point.isSelected) {
        return point;
      }
    }
    
    return null;
  }

  /**
   * 处理触摸开始
   */
  private onTouchStart(event: TouchEvent) {
    // 重置选中状态和绘制状态,但不清空节点数组
    this.selectedPattern = [];
    this.isDrawing = false;
    this.points.forEach(point => {
      point.isSelected = false;
    });
    
    const touch = event.touches[0];
    const x = touch.x;
    const y = touch.y;
    
    const point = this.getPointAt(x, y);
    if (point) {
      this.isDrawing = true;
      this.selectedPattern = [point.id - 1]; // 转换为0-based数组
      point.isSelected = true;
      
      this.callbacks.onPatternStart?.();
      this.callbacks.onPatternChange?.(this.selectedPattern);
    }
  }

  /**
   * 处理触摸移动
   */
  private onTouchMove(event: TouchEvent) {
    if (!this.isDrawing) return;
    
    const touch = event.touches[0];
    const x = touch.x;
    const y = touch.y;
    
    this.currentTouchX = x;
    this.currentTouchY = y;
    
    const point = this.getPointAt(x, y);
    if (point) {
      this.selectedPattern.push(point.id - 1); // 添加0-based节点ID
      point.isSelected = true;
      
      this.callbacks.onPatternChange?.(this.selectedPattern);
    }
  }

  /**
   * 处理触摸结束
   */
  private onTouchEnd() {
    if (!this.isDrawing) return;
    
    this.isDrawing = false;
    
    // 检查密码长度是否符合要求
    const selectedCount = this.selectedPattern.length;
    const minNodes = this.config.minNodes || 4;
    const maxNodes = this.config.maxNodes || 9;
    
    if (selectedCount >= minNodes && selectedCount <= maxNodes) {
      // 密码有效,输出数组格式如 [0,1,2,5,8]
      this.callbacks.onComplete?.(this.selectedPattern);
    } else {
      // 密码太短,0.5秒后清空提示重绘
      setTimeout(() => {
        this.resetPattern();
      }, 500);
    }
  }

  /**
   * 重置手势密码
   */
  public resetPattern() {
    this.selectedPattern = [];
    this.isDrawing = false;
    this.points.forEach(point => {
      point.isSelected = false;
    });
    
    this.callbacks.onPatternReset?.();
  }

  build() {
    Column() {
      Canvas(this.context)
        .width(this.canvasWidth)
        .height(this.canvasHeight)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .shadow({
          radius: 8,
          color: '#00000020',
          offsetX: 0,
          offsetY: 2
        })
        .onReady(() => {
          this.context = this.context;
          this.drawPattern(this.context);
        })
        .onTouch((event: TouchEvent) => {
          if (event.type === TouchType.Down) {
            this.onTouchStart(event);
            this.drawPattern(this.context); // 重绘
          } else if (event.type === TouchType.Move) {
            this.onTouchMove(event);
            this.drawPattern(this.context); // 重绘
          } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
            this.onTouchEnd();
            this.drawPattern(this.context); // 重绘
          }
        })
      
      // 操作按钮
      Row() {
        Button('重置')
          .fontSize(14)
          .fontColor('#666666')
          .backgroundColor('#F5F5F5')
          .padding({ left: 20, right: 20, top: 8, bottom: 8 })
          .borderRadius(6)
          .onClick(() => {
            this.resetPattern();
            this.drawPattern(this.context); // 重绘
          })
      }
      .margin({ top: 20 })
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .padding(20)
    .width('100%')
  }
}

示例page代码:

import { PatternLockView } from '../components/PatternLock';


/**
 * [@author](/user/author) J.query
 * [@date](/user/date) 2025/12/26 13:27
 * [@email](/user/email) j-query@foxmail.com
 * Description: 手势密码示例页面
 */

[@Entry](/user/Entry)
[@Component](/user/Component)
struct PatternLockDemo {
  [@State](/user/State) savedPattern: number[] = [];
  [@State](/user/State) isSetting: boolean = true;
  [@State](/user/State) confirmPattern: number[] = [];
  [@State](/user/State) message: string = '请设置手势密码';

  /**
   * 手势密码完成回调
   */
  private onPatternComplete = (pattern: number[]) => {
    const patternStr = JSON.stringify(pattern);
    
    if (this.isSetting) {
      // 设置模式
      if (this.confirmPattern.length === 0) {
        // 第一次输入
        this.confirmPattern = pattern;
        this.message = '请再次确认手势密码';
      } else {
        // 第二次输入,验证是否一致
        if (JSON.stringify(this.confirmPattern) === patternStr) {
          this.savedPattern = pattern;
          this.isSetting = false;
          this.message = '手势密码设置成功!';
        } else {
          this.message = '两次输入不一致,请重新设置';
          this.confirmPattern = [];
        }
      }
    } else {
      // 验证模式
      if (JSON.stringify(this.savedPattern) === patternStr) {
        this.message = '验证成功!';
      } else {
        this.message = '密码错误,请重试';
      }
    }
  };

  /**
   * 手势密码开始回调
   */
  private onPatternStart = () => {
    if (!this.isSetting) {
      this.message = '请输入手势密码';
    }
  };

  /**
   * 手势密码重置回调
   */
  private onPatternReset = () => {
    if (this.isSetting && this.confirmPattern.length === 0) {
      this.message = '请设置手势密码';
    } else if (this.isSetting) {
      this.message = '请再次确认手势密码';
    }
  };

  /**
   * 重置所有设置
   */
  private resetAll = () => {
    this.savedPattern = [];
    this.isSetting = true;
    this.confirmPattern = [];
    this.message = '请设置手势密码';
  };

  build() {
    Column() {
      // 标题
      Text('手势密码演示')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 10 })

      // 状态消息
      Text(this.message)
        .fontSize(16)
        .fontColor(this.message.includes('成功') ? '#00AA00' : 
                  this.message.includes('错误') || this.message.includes('不一致') ? '#FF0000' : '#666666')
        .margin({ bottom: 20 })

      // 手势密码组件
      PatternLockView({
        config: {
          nodeRadius: 20,        // 增大节点半径
          nodeSpacing: 90,       // 增大节点间距
          lineWidth: 5,          // 增粗连线
          touchThreshold: 35,    // 增大触摸范围
          minNodes: 4,
          maxNodes: 9
        },
        callbacks: {
          onComplete: this.onPatternComplete,
          onPatternStart: this.onPatternStart,
          onPatternReset: this.onPatternReset
        }
      })

      // 当前模式指示
      Row() {
        Text('当前模式: ')
          .fontSize(14)
          .fontColor('#666666')
        
        Text(this.isSetting ? '设置密码' : '验证密码')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.isSetting ? '#0A59F7' : '#00AA00')
      }
      .

更多关于HarmonyOS 鸿蒙Next中如何绘制手势密码?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,绘制手势密码主要使用@ohos.graphics.gesturePassword模块。通过GesturePasswordView组件创建手势密码界面,使用GesturePasswordController管理手势路径和验证逻辑。开发者需监听手势绘制事件,处理路径点数据,并调用验证接口完成密码设置与校验。

在HarmonyOS Next中绘制手势密码,核心是使用Canvas组件进行自定义绘制,并结合手势事件处理。以下是关键步骤和代码思路:

  1. 界面布局:使用Canvas组件作为画布,通常需要设置固定宽高(如300x300)。九宫格的九个点坐标可通过计算均匀分布。

  2. 绘制九宫格:在CanvasonReady回调中,通过CanvasRenderingContext2DAPI绘制静态圆点(如空心圆)。示例:

    // 计算点坐标
    let points = []; // 存储9个点的坐标数组
    // 绘制每个点
    context.beginPath();
    context.arc(x, y, radius, 0, Math.PI * 2);
    context.stroke();
    
  3. 手势交互:为Canvas绑定触摸事件:

    • onTouchStart:记录起始点,判断触摸位置是否在圆点范围内。
    • onTouchMove:实时绘制用户手指移动的路径(连线),并检查经过的圆点,将其加入已选点序列。
    • onTouchEnd:完成手势,验证密码逻辑(如比对预存密码)。
  4. 路径绘制:在onTouchMove中,用lineTo连接已选点,并通过stroke绘制线条。需注意线条应跟随手指实时更新。

  5. 密码验证:将已选点序列(按顺序)与存储的密码对比。可结合@StorageLink持久化存储密码配置。

注意事项

  • 性能优化:避免在频繁触发的onTouchMove中执行重计算,可考虑节流。
  • 安全性:手势密码存储需加密,验证逻辑应在本地完成。
  • 视觉反馈:为选中的圆点添加填充色、连线高亮等效果提升体验。

完整实现需约100-150行代码,重点在于坐标计算与手势事件协同。

回到顶部