HarmonyOS NEXT Development Case: Ruler Application

The following code demonstrates how to create an interactive ruler application using HarmonyOS NEXT. This implementation features dynamic scale generation, gesture-based interaction, and real-time UI adaptation. Implementation Code import { window } from '@kit.ArkUI'; // Import window management APIs import { deviceInfo } from '@kit.BasicServicesKit'; // Import device information APIs // Ruler scale line definition class RulerLine { index: number; // Index of the scale line height: number; // Height of the scale line constructor(index: number, height: number) { this.index = index; // Initialize index this.height = height; // Initialize height } // Display scale numbering showNumber(): string { return this.index % 10 === 0 ? `${Math.floor(this.index / 10)}` : ''; // Show number every 10 units } } // Custom text styling extension @Extend(Text) function fancy() { .fontColor("#019dfe") // Set font color .fontSize(20); // Set font size } // Main ruler component @Entry @Component struct RulerComponent { @State maxRulerHeight: number = 0; // Maximum ruler height @State @Watch('onCellWidthChanged') cellWidthInPixels: number = 17.28; // Pixels per millimeter @State textWidth: number = 80; // Text label width @State rulerLines: RulerLine[] = []; // Array of scale lines @State leftOffsetX: number = -300; // Left offset position @State currentPositionX: number = -300; // Current X position @State @Watch('onContainerHeightChanged') containerHeight: number = 53; // Container height @State originalContainerHeight: number = 53; // Original container height @State @Watch('onCellWidthChanged') containerWidth: number = 0; // Container width // Handle cell width changes onCellWidthChanged() { this.maxRulerHeight = vp2px(this.containerWidth) / this.cellWidthInPixels / 10; } // Handle container height changes onContainerHeightChanged() { this.containerHeight = Math.max(this.containerHeight, 53); // Ensure minimum height } // Lifecycle: Component initialization aboutToAppear(): void { // Set landscape orientation window.getLastWindow(getContext()).then((windowClass) => { windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE); }); // Initialize scale lines for (let i = 0; i { Line() .width(1) .height(`${line.height}px`) .backgroundColor(Color.White) .margin({ left: `${this.cellWidthInPixels * index}px` }); Text(line.showNumber()) .fontColor(Color.White) .fontSize(18) .width(`${this.textWidth}px`) .height(`${this.textWidth}px`) .textAlign(TextAlign.Center) .margin({ left: `${this.cellWidthInPixels * index - this.textWidth / 2}px`, top: `${line.height}px` }); }); }.width('100%').height('100%').align(Alignment.TopStart); // Control panel Column({ space: 15 }) { Text(`Current Device: ${deviceInfo.marketName}`).fancy(); Counter() { Text(`Measurement: ${this.maxRulerHeight.toFixed(2)} cm`).fancy(); } .foregroundColor(Color.White) .width(300) .onInc(() => { this.containerHeight = px2vp(vp2px(this.containerHeight) + this.cellWidthInPixels / 10; }) .onDec(() => { this.containerHeight = px2vp(vp2px(this.containerHeight) - this.cellWidthInPixels / 10; }); Counter() { Text(`mm Scale: ${this.cellWidthInPixels.toFixed(2)}px`).fancy(); } .foregroundColor(Color.White) .width(300) .onInc(() => { this.cellWidthInPixels += 0.01; }) .onDec(() => { this.cellWidthInPixels = Math.max(0.01, this.cellWidthInPixels - 0.01); }); } // Interactive measurement area RelativeContainer() { Rect() .fill("#80019dfe") .borderColor("#019dfe") .borderWidth({ left: 1, right: 1 }) .clip(true) .width("100%") .height("100%") .onAreaChange((oldArea: Area, newArea: Area) => { this.containerWidth = newArea.width as number; }); // Left draggable handle Stack() { Circle({ height: 30, width: 30 }) .fill("#019dfe") .stroke(Color.Transparent) .strokeWidth(3); Circle({ height: 40, width: 40 }) .fill(Color.Transparent) .stroke("#019dfe") .strokeWidth(3); } .hitTestBehavior(HitTestMode.Block) .padding(20) .alignRules({ center: { anchor: "__container__", align: VerticalAlign.Center }, middle: { anchor: "__container__", align: HorizontalAlig

May 11, 2025 - 02:46
 0
HarmonyOS NEXT Development Case: Ruler Application

Image description

The following code demonstrates how to create an interactive ruler application using HarmonyOS NEXT. This implementation features dynamic scale generation, gesture-based interaction, and real-time UI adaptation.

Implementation Code

import { window } from '@kit.ArkUI'; // Import window management APIs
import { deviceInfo } from '@kit.BasicServicesKit'; // Import device information APIs

// Ruler scale line definition
class RulerLine {
  index: number; // Index of the scale line
  height: number; // Height of the scale line

  constructor(index: number, height: number) {
    this.index = index; // Initialize index
    this.height = height; // Initialize height
  }

  // Display scale numbering
  showNumber(): string {
    return this.index % 10 === 0 ? `${Math.floor(this.index / 10)}` : ''; // Show number every 10 units
  }
}

// Custom text styling extension
@Extend(Text)
function fancy() {
  .fontColor("#019dfe") // Set font color
  .fontSize(20); // Set font size
}

// Main ruler component
@Entry
@Component
struct RulerComponent {
  @State maxRulerHeight: number = 0; // Maximum ruler height
  @State @Watch('onCellWidthChanged') cellWidthInPixels: number = 17.28; // Pixels per millimeter
  @State textWidth: number = 80; // Text label width
  @State rulerLines: RulerLine[] = []; // Array of scale lines
  @State leftOffsetX: number = -300; // Left offset position
  @State currentPositionX: number = -300; // Current X position
  @State @Watch('onContainerHeightChanged') containerHeight: number = 53; // Container height
  @State originalContainerHeight: number = 53; // Original container height
  @State @Watch('onCellWidthChanged') containerWidth: number = 0; // Container width

  // Handle cell width changes
  onCellWidthChanged() {
    this.maxRulerHeight = vp2px(this.containerWidth) / this.cellWidthInPixels / 10;
  }

  // Handle container height changes
  onContainerHeightChanged() {
    this.containerHeight = Math.max(this.containerHeight, 53); // Ensure minimum height
  }

  // Lifecycle: Component initialization
  aboutToAppear(): void {
    // Set landscape orientation
    window.getLastWindow(getContext()).then((windowClass) => {
      windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE);
    });

    // Initialize scale lines
    for (let i = 0; i <= 15 * 10; i++) {
      const lineHeight: number = (i % 10 === 0) ? 90 : (i % 5 === 0) ? 60 : 45;
      this.rulerLines.push(new RulerLine(i, lineHeight));
    }
  }

  // UI construction
  build() {
    Column() {
      Stack() {
        // Scale lines rendering
        Stack() {
          ForEach(this.rulerLines, (line: RulerLine, index: number) => {
            Line()
              .width(1)
              .height(`${line.height}px`)
              .backgroundColor(Color.White)
              .margin({ left: `${this.cellWidthInPixels * index}px` });
            Text(line.showNumber())
              .fontColor(Color.White)
              .fontSize(18)
              .width(`${this.textWidth}px`)
              .height(`${this.textWidth}px`)
              .textAlign(TextAlign.Center)
              .margin({
                left: `${this.cellWidthInPixels * index - this.textWidth / 2}px`,
                top: `${line.height}px`
              });
          });
        }.width('100%').height('100%').align(Alignment.TopStart);

        // Control panel
        Column({ space: 15 }) {
          Text(`Current Device: ${deviceInfo.marketName}`).fancy();
          Counter() {
            Text(`Measurement: ${this.maxRulerHeight.toFixed(2)} cm`).fancy();
          }
          .foregroundColor(Color.White)
          .width(300)
          .onInc(() => {
            this.containerHeight = px2vp(vp2px(this.containerHeight) + this.cellWidthInPixels / 10;
          })
          .onDec(() => {
            this.containerHeight = px2vp(vp2px(this.containerHeight) - this.cellWidthInPixels / 10;
          });

          Counter() {
            Text(`mm Scale: ${this.cellWidthInPixels.toFixed(2)}px`).fancy();
          }
          .foregroundColor(Color.White)
          .width(300)
          .onInc(() => {
            this.cellWidthInPixels += 0.01;
          })
          .onDec(() => {
            this.cellWidthInPixels = Math.max(0.01, this.cellWidthInPixels - 0.01);
          });
        }

        // Interactive measurement area
        RelativeContainer() {
          Rect()
            .fill("#80019dfe")
            .borderColor("#019dfe")
            .borderWidth({ left: 1, right: 1 })
            .clip(true)
            .width("100%")
            .height("100%")
            .onAreaChange((oldArea: Area, newArea: Area) => {
              this.containerWidth = newArea.width as number;
            });

          // Left draggable handle
          Stack() {
            Circle({ height: 30, width: 30 })
              .fill("#019dfe")
              .stroke(Color.Transparent)
              .strokeWidth(3);
            Circle({ height: 40, width: 40 })
              .fill(Color.Transparent)
              .stroke("#019dfe")
              .strokeWidth(3);
          }
          .hitTestBehavior(HitTestMode.Block)
          .padding(20)
          .alignRules({
            center: { anchor: "__container__", align: VerticalAlign.Center },
            middle: { anchor: "__container__", align: HorizontalAlign.Start }
          })
          .gesture(PanGesture({
            fingers: 1,
            direction: PanDirection.Horizontal,
            distance: 1
          }).onActionUpdate((event: GestureEvent) => {
            this.leftOffsetX = this.currentPositionX + event.offsetX / 2;
            this.containerHeight = this.originalContainerHeight - event.offsetX;
          }).onActionEnd(() => {
            this.currentPositionX = this.leftOffsetX;
            this.originalContainerHeight = this.containerHeight;
          }));

          // Right draggable handle
          Stack() {
            Circle({ height: 30, width: 30 })
              .fill("#019dfe")
              .stroke(Color.Transparent)
              .strokeWidth(3);
            Circle({ height: 40, width: 40 })
              .fill(Color.Transparent)
              .stroke("#019dfe")
              .strokeWidth(3);
          }
          .hitTestBehavior(HitTestMode.Block)
          .padding(20)
          .alignRules({
            center: { anchor: "__container__", align: VerticalAlign.Center },
            middle: { anchor: "__container__", align: HorizontalAlign.End }
          })
          .gesture(PanGesture({
            fingers: 1,
            direction: PanDirection.Horizontal,
            distance: 1
          }).onActionUpdate((event: GestureEvent) => {
            this.leftOffsetX = this.currentPositionX + event.offsetX / 2;
            this.containerHeight = this.originalContainerHeight + event.offsetX;
          }).onActionEnd(() => {
            this.currentPositionX = this.leftOffsetX;
            this.originalContainerHeight = this.containerHeight;
          }));
        }
        .width(this.containerHeight)
        .height("100%")
        .translate({ x: this.leftOffsetX })
        .gesture(PanGesture({
          fingers: 1,
          direction: PanDirection.Horizontal,
          distance: 1
        }).onActionUpdate((event: GestureEvent) => {
          if (event) {
            this.leftOffsetX = this.currentPositionX + event.offsetX;
          }
        }).onActionEnd(() => {
          this.currentPositionX = this.leftOffsetX;
        }));
      }
    }.height('100%').width('100%')
    .padding({ left: 30, right: 10 })
    .backgroundColor("#181b22");
  }
}

Key Features

  1. Dynamic Scale Generation

    Creates ruler scales programmatically with varying heights for different scale marks (main, medium, and small divisions).

  2. Adaptive Layout

    Utilizes HarmonyOS's responsive layout system to maintain proper scaling across different screen sizes.

  3. Interactive Measurement

    Implements dual draggable handles with smooth animation for precise measurement operations.

  4. Real-time Unit Conversion

    Provides pixel-to-centimeter conversion with adjustable scale density.

  5. Gesture Support

    Implements multi-directional pan gestures for both ruler adjustment and measurement operations.

Usage

  1. Drag the blue handles to perform measurements
  2. Use the "+"/"-" buttons to adjust scale density
  3. The ruler automatically adapts to device orientation
  4. Displays real-time measurement results in centimeters

This implementation demonstrates core HarmonyOS NEXT capabilities including declarative UI, reactive state management, and advanced gesture handling.