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组件,设置其尺寸和样式。通过onGestureComplete回调获取用户绘制的手势路径,与预设密码进行比对验证。密码存储应使用@ohos.security.huks进行加密处理。

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

1. 布局与状态定义

使用Column布局,通过Canvas组件作为绘制区域,并定义关键状态变量:

@State private points: Point[] = []; // 九宫格点坐标
@State private selectedPoints: number[] = []; // 被选中的点索引
@State private currentPath: Point[] = []; // 当前绘制路径
private canvasSize: number = 300; // 画布尺寸

2. 初始化九宫格点坐标

aboutToAppear生命周期中计算九个点的位置:

aboutToAppear() {
  const step = this.canvasSize / 4;
  for (let row = 0; row < 3; row++) {
    for (let col = 0; col < 3; col++) {
      this.points.push({
        x: step * (col + 1),
        y: step * (row + 1)
      });
    }
  }
}

3. 绘制逻辑实现

CanvasonReady回调中使用RenderingContext2D进行绘制:

Canvas(this.canvasContext)
  .size({ width: this.canvasSize, height: this.canvasSize })
  .onReady(() => {
    const ctx = this.canvasContext.getContext('2d');
    
    // 绘制九宫格点
    this.points.forEach((point, index) => {
      ctx.beginPath();
      ctx.arc(point.x, point.y, 10, 0, Math.PI * 2);
      ctx.fillStyle = this.selectedPoints.includes(index) ? '#007DFF' : '#E5E5E5';
      ctx.fill();
    });
    
    // 绘制连接线
    if (this.currentPath.length > 1) {
      ctx.beginPath();
      ctx.moveTo(this.currentPath[0].x, this.currentPath[0].y);
      this.currentPath.forEach(point => {
        ctx.lineTo(point.x, point.y);
      });
      ctx.strokeStyle = '#007DFF';
      ctx.lineWidth = 3;
      ctx.stroke();
    }
  })

4. 手势事件处理

通过触摸事件监听用户绘制:

.gesture(
  GestureGroup(
    GestureMask.Normal,
    PanGesture({ distance: 1 })
      .onActionStart((event: GestureEvent) => {
        this.handleTouchStart(event);
      })
      .onActionUpdate((event: GestureEvent) => {
        this.handleTouchMove(event);
      })
      .onActionEnd(() => {
        this.handleTouchEnd();
      })
  )
)

5. 触摸处理函数

private handleTouchStart(event: GestureEvent) {
  const touchX = event.offsetX;
  const touchY = event.offsetY;
  const index = this.getSelectedPointIndex(touchX, touchY);
  
  if (index !== -1) {
    this.selectedPoints.push(index);
    this.currentPath.push(this.points[index]);
  }
}

private handleTouchMove(event: GestureEvent) {
  if (this.selectedPoints.length === 0) return;
  
  const touchX = event.offsetX;
  const touchY = event.offsetY;
  const index = this.getSelectedPointIndex(touchX, touchY);
  
  if (index !== -1 && !this.selectedPoints.includes(index)) {
    this.selectedPoints.push(index);
    this.currentPath.push(this.points[index]);
  } else {
    this.currentPath.push({ x: touchX, y: touchY });
  }
}

private handleTouchEnd() {
  // 验证手势密码逻辑
  const password = this.selectedPoints.join('');
  console.log('绘制的手势密码:', password);
  
  // 清空绘制状态
  setTimeout(() => {
    this.selectedPoints = [];
    this.currentPath = [];
  }, 500);
}

6. 辅助函数

private getSelectedPointIndex(x: number, y: number): number {
  const radius = 20; // 触摸检测半径
  for (let i = 0; i < this.points.length; i++) {
    const point = this.points[i];
    const distance = Math.sqrt(Math.pow(point.x - x, 2) + Math.pow(point.y - y, 2));
    if (distance <= radius) {
      return i;
    }
  }
  return -1;
}

关键注意事项:

  1. 性能优化:频繁绘制时建议使用CanvasRenderingContext2D的离屏渲染
  2. 安全存储:手势密码应使用加密存储(如@ohos.security.huks
  3. 验证逻辑:服务端应限制连续失败尝试次数
  4. 用户体验:可添加视觉反馈(如点高亮、路径动画)

此实现提供了完整的手势密码绘制功能,可根据实际需求调整样式和验证逻辑。

回到顶部