VOOZH about

URL: https://dev.to/harmonyos/how-to-implement-a-pattern-password-lock-26k5

⇱ How to Implement a Pattern Password Lock? - DEV Community


Read the original article:How to Implement a Pattern Password Lock?

How to Implement a Pattern Password Lock?

Requirement Description

How to implement the function of setting a pattern password lock?

Background Knowledge

  • PatternLock is a component that allows users to input passwords using a 3x3 grid pattern, often used in password verification scenarios.
  • Canvas is a drawing component for custom graphics rendering.
  • Grid is a grid container component where each item corresponds to a GridItem, supporting various layouts.

Implementation Steps

  • Option 1: Use the built-in PatternLock component to implement password lock drawing. (Refer to the official documentation: Create a Pattern Password Lock).
  • Option 2: Use a Stack component that contains both Canvas (for drawing lines) and Grid (for rendering the circle nodes) to build a custom pattern lock.

Main Logic:

  1. Create Canvas and Grid inside Stack to render the pattern interface.
  2. Add the handleTouch method to the Canvas component. This is the core of the code:
    • While the finger moves across the screen, record each number passed through and store it in an array.
    • When TouchType.Up is triggered, call validatePassword to check whether the input pattern is correct, and show appropriate text or toast messages.

Code Snippet / Configuration

import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct GestureLock {
 @State points: number[][] = [
 [50, 50], [150, 50], [250, 50],
 [50, 150], [150, 150], [250, 150],
 [50, 250], [150, 250], [250, 250]
 ];
 @State path: number[] = [];
 @State isDrawing: boolean = false;
 @State isError: boolean = false;
 @State shakeOffset: number = 0;
 @State lineColor: string = '#0A59F7';
 @State circleColor: string = '#CCCCCC';
 @State currentX: number = 0;
 @State currentY: number = 0;

 // Default preset password
 private presetPassword: number[] = [1, 2, 5, 8, 9];
 private settings: RenderingContextSettings = new RenderingContextSettings(true);
 private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

 build() {
 Stack() {
 Column() {
 // Hint text
 Text(this.getHintText())
 .fontSize(20)
 .fontColor(Color.Black)
 .margin({ top: 20, bottom: 20 });

 // Stack layout
 Stack() {
 // Canvas component
 Canvas(this.canvasContext)
 .width(300)
 .height(300)
 .backgroundColor(Color.Transparent)
 .hitTestBehavior(HitTestMode.Transparent)
 .onTouch((event: TouchEvent) => {
 this.handleTouch(event);
 })
 .onReady(() => {
 this.drawPath();
 });

 // Grid for password lock
 Grid() {
 ForEach(this.points, (point: number[], index: number) => {
 // Render each circle node
 GridItem() {
 Column() {
 Text()
 .fontSize(20)
 .fontColor(Color.Black)
 }
 .width(80)
 .height(80)
 .justifyContent(FlexAlign.Center)
 .alignItems(HorizontalAlign.Center)
 .borderRadius(40)
 .backgroundColor(this.getCircleColor(index))
 .border({ width: 2, color: this.getCircleBorderColor(index) })
 .animation({ duration: 300, curve: Curve.EaseInOut });
 }
 });
 }
 .columnsTemplate('1fr 1fr 1fr')
 .rowsTemplate('1fr 1fr 1fr')
 .width(300)
 .height(300)
 .hitTestBehavior(HitTestMode.Transparent)
 }
 .translate({ x: this.shakeOffset, y: 0 })
 .animation({ duration: 50, curve: Curve.EaseInOut })
 }
 .width('100%')
 .height('100%')
 .justifyContent(FlexAlign.Center)
 .alignItems(HorizontalAlign.Center)
 }
 .width('100%')
 .height('100%')
 .backgroundColor(Color.White)
 .zIndex(200)
 .position({ x: 0, y: 0 })
 }

 // Change circle color while drawing
 getCircleColor(index: number): string {
 return this.path.includes(index) ? '#0A59F7' : '#CCCCCC';
 }

 // Circle border color
 getCircleBorderColor(index: number): Color {
 return Color.Black;
 }

 // Hint text
 getHintText(): string {
 if (this.isError) {
 return 'Password incorrect, please try again';
 } else {
 return 'Please draw your gesture password';
 }
 }

 // Draw path
 drawPath() {
 const context = this.canvasContext;
 context.clearRect(0, 0, 300, 300);
 context.strokeStyle = this.lineColor;
 context.lineWidth = 3;
 context.lineCap = 'round';
 context.lineJoin = 'round';
 if (this.path.length > 0) {
 context.beginPath();
 const start = this.points[this.path[0]];
 context.moveTo(start[0], start[1]);
 for (let i = 1; i < this.path.length; i++) {
 const end = this.points[this.path[i]];
 context.lineTo(end[0], end[1]);
 }
 if (this.isDrawing) {
 context.lineTo(this.currentX, this.currentY);
 }
 context.stroke();
 }
 }

 // Record path while finger moves
 handleTouch(event: TouchEvent) {
 const touch = event.touches[0];
 const x = touch.x;
 const y = touch.y;
 if (event.type === TouchType.Down) {
 this.isDrawing = true;
 this.path = [];
 this.isError = false;
 this.lineColor = '#0A59F7';
 this.drawPath();
 }
 if (this.isDrawing) {
 this.currentX = x;
 this.currentY = y;
 this.points.forEach((point: number[], index: number) => {
 const centerX = point[0];
 const centerY = point[1];
 const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
 if (distance <= 40 && !this.path.includes(index)) {
 this.path.push(index);
 }
 });
 this.drawPath();
 }

 if (event.type === TouchType.Up) {
 this.isDrawing = false;
 this.validatePassword();
 }
 }

 // Validate input pattern
 validatePassword() {
 if (this.path.length === this.presetPassword.length &&
 this.path.every((value: number, index: number) => value + 1 === this.presetPassword[index])) {
 promptAction.openToast({ message: 'Password correct' });
 setTimeout(() => {
 this.reset();
 }, 1000);
 } else {
 this.isError = true;
 this.shake();
 setTimeout(() => {
 this.reset();
 }, 1000);
 promptAction.openToast({ message: 'Password incorrect, please try again' });
 }
 }

 // Shake effect on error
 shake() {
 const shakeDistance = 10;
 const shakeDuration = 50;
 const shakeCount = 4;
 let direction = 1;
 for (let i = 0; i < shakeCount; i++) {
 setTimeout(() => {
 this.shakeOffset = direction * shakeDistance;
 direction *= -1;
 }, i * shakeDuration);
 }
 setTimeout(() => {
 this.shakeOffset = 0;
 }, shakeCount * shakeDuration);
 }

 // Reset path
 reset() {
 this.path = [];
 this.isError = false;
 this.lineColor = '#0A59F7';
 this.drawPath();
 }
}

Test Results

  • When the user draws the correct pattern (1 → 2 → 5 → 8 → 9), a toast appears: "Password correct".
  • When the user draws the wrong pattern, a toast appears: "Password incorrect, please try again", and the interface shakes.

👁 image.png

Limitations or Considerations

  • Security considerations: Gesture-based passwords are less secure compared to alphanumeric ones.
  • Need to handle edge cases like partial drawings or unintended touches.

Related Documents or Links

https://developer.huawei.com/consumer/en/doc/harmonyos-references/scroll-and-swipe

Written by Mehmet Algul