DEV Community

HarmonyOS
HarmonyOS

Posted on

How to Implement a Pattern Password Lock?

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)