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

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
Dynamic Scale Generation
Creates ruler scales programmatically with varying heights for different scale marks (main, medium, and small divisions).Adaptive Layout
Utilizes HarmonyOS's responsive layout system to maintain proper scaling across different screen sizes.Interactive Measurement
Implements dual draggable handles with smooth animation for precise measurement operations.Real-time Unit Conversion
Provides pixel-to-centimeter conversion with adjustable scale density.Gesture Support
Implements multi-directional pan gestures for both ruler adjustment and measurement operations.
Usage
- Drag the blue handles to perform measurements
- Use the "+"/"-" buttons to adjust scale density
- The ruler automatically adapts to device orientation
- Displays real-time measurement results in centimeters
This implementation demonstrates core HarmonyOS NEXT capabilities including declarative UI, reactive state management, and advanced gesture handling.