HarmonyOS 鸿蒙Next中如何绘制手势密码?
HarmonyOS 鸿蒙Next中如何绘制手势密码? **问题描述:**用户需要通过绘制“九宫格”手势密码进行身份验证(如解锁、支付确认)。
详细回答:
我们可以使用以下技术组合实现:
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组件进行自定义绘制,并结合手势事件处理。以下是关键步骤和代码思路:
-
界面布局:使用
Canvas组件作为画布,通常需要设置固定宽高(如300x300)。九宫格的九个点坐标可通过计算均匀分布。 -
绘制九宫格:在
Canvas的onReady回调中,通过CanvasRenderingContext2DAPI绘制静态圆点(如空心圆)。示例:// 计算点坐标 let points = []; // 存储9个点的坐标数组 // 绘制每个点 context.beginPath(); context.arc(x, y, radius, 0, Math.PI * 2); context.stroke(); -
手势交互:为
Canvas绑定触摸事件:onTouchStart:记录起始点,判断触摸位置是否在圆点范围内。onTouchMove:实时绘制用户手指移动的路径(连线),并检查经过的圆点,将其加入已选点序列。onTouchEnd:完成手势,验证密码逻辑(如比对预存密码)。
-
路径绘制:在
onTouchMove中,用lineTo连接已选点,并通过stroke绘制线条。需注意线条应跟随手指实时更新。 -
密码验证:将已选点序列(按顺序)与存储的密码对比。可结合
@StorageLink持久化存储密码配置。
注意事项:
- 性能优化:避免在频繁触发的
onTouchMove中执行重计算,可考虑节流。 - 安全性:手势密码存储需加密,验证逻辑应在本地完成。
- 视觉反馈:为选中的圆点添加填充色、连线高亮等效果提升体验。
完整实现需约100-150行代码,重点在于坐标计算与手势事件协同。

