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组件,设置其尺寸和样式。通过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. 绘制逻辑实现
在Canvas的onReady回调中使用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;
}
关键注意事项:
- 性能优化:频繁绘制时建议使用
CanvasRenderingContext2D的离屏渲染 - 安全存储:手势密码应使用加密存储(如
@ohos.security.huks) - 验证逻辑:服务端应限制连续失败尝试次数
- 用户体验:可添加视觉反馈(如点高亮、路径动画)
此实现提供了完整的手势密码绘制功能,可根据实际需求调整样式和验证逻辑。

