This content originally appeared on DEV Community and was authored by HarmonyOS
Customizing a UI with pure Canvas is cool — but making it reactive, up-to-date, and ArkTS-friendly is next level. Let’s dive into a real-world example: a stateful mechanical clock that ticks with elegance.
Introduction
When working on HarmonyOS apps, there are times when default UI components just don’t cut it. Maybe you need a stylized button, a unique progress bar, or even an analog clock that feels alive. While Canvas gives you the freedom to draw anything, turning those visuals into dynamic, state-driven, and responsive components is where most developers hesitate.
In this article, we’ll explore how to use Canvas with ArkTS and ArkUI’s component-driven model to create a fully custom mechanical clock that updates every second — without reloading the entire page or component.
Why Canvas?
Canvas allows developers to directly control pixel-level rendering. This is crucial when building:
- Custom Clocks
- Charts and meters
- Game UIs
- Interactive drawing tools
- Special animations or branded visals
But drawing is just one part. The real challenge lies in making these drawings react to state changes, such as time, progress, or user interaction.
Let’s Build: A Stateful Mechanical Clock
Below is a simplified example of a reusable @Component
that renders a fully working analog clock using Canvas. It’s dynamic, timezone-sensitive, and fully reactive.
MechanicClock Component
@Component
export struct MechanicClock {
@Prop timezone: string = 'Europe/Istanbul';
@State private currentTime: Date = new Date();
private timerID: number = 0;
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private updateTime() {
const options: Intl.DateTimeFormatOptions = {
timeZone: this.timezone,
hour12: false
};
const timeParts = new Date().toLocaleTimeString('tr-TR', options).split(':');
this.currentTime.setHours(Number(timeParts[0]), Number(timeParts[1]), Number(timeParts[2]));
}
private drawHand(angle: number, length: number, width: number, color: string) {
const centerX = 95;
const centerY = 95;
this.context.beginPath();
this.context.moveTo(centerX, centerY);
this.context.lineTo(
centerX + length * Math.sin(angle),
centerY - length * Math.cos(angle)
);
this.context.lineWidth = width;
this.context.strokeStyle = color;
this.context.lineCap = 'round';
this.context.stroke();
}
private drawClockFace() {
this.context.clearRect(0, 0, 190, 190);
this.context.beginPath();
this.context.arc(95, 95, 85.5, 0, 2 * Math.PI);
this.context.fillStyle = '#fff';
this.context.fill();
this.context.lineWidth = 3.8;
this.context.strokeStyle = '#333';
this.context.stroke();
for (let i = 0; i < 60; i++) {
const angle = (i * 6) * Math.PI / 180;
const isHourMark = i % 5 === 0;
const length = isHourMark ? 11.4 : 5.7;
const startX = 95 + (85.5 - length) * Math.sin(angle);
const startY = 95 - (85.5 - length) * Math.cos(angle);
const endX = 95 + 85.5 * Math.sin(angle);
const endY = 95 - 85.5 * Math.cos(angle);
this.context.beginPath();
this.context.moveTo(startX, startY);
this.context.lineTo(endX, endY);
this.context.lineWidth = isHourMark ? 2.85 : 0.95;
this.context.strokeStyle = isHourMark ? '#333' : '#999';
this.context.stroke();
}
this.context.font = 'bold 38px Arial';
this.context.textAlign = 'center';
this.context.textBaseline = 'middle';
this.context.fillStyle = '#333';
for (let i = 1; i <= 12; i++) {
const angle = (i * 30) * Math.PI / 180;
const x = 95 + 66.5 * Math.sin(angle);
const y = 95 - 66.5 * Math.cos(angle);
this.context.fillText(i.toString(), x, y);
}
}
private drawFullClock() {
this.drawClockFace();
const hours = this.currentTime.getHours();
const minutes = this.currentTime.getMinutes();
const seconds = this.currentTime.getSeconds();
const hourAngle = (hours % 12 * 30 + minutes * 0.5) * Math.PI / 180;
const minuteAngle = minutes * 6 * Math.PI / 180;
const secondAngle = seconds * 6 * Math.PI / 180;
this.drawHand(hourAngle, 47.5, 5.7, '#222');
this.drawHand(minuteAngle, 66.5, 2.85, '#444');
this.drawHand(secondAngle, 76, 0.95, '#e74c3c');
this.context.beginPath();
this.context.arc(95, 95, 3.8, 0, 2 * Math.PI);
this.context.fillStyle = '#e74c3c';
this.context.fill();
}
aboutToAppear() {
this.updateTime();
this.timerID = setInterval(() => {
this.updateTime();
this.drawFullClock();
}, 1000);
}
aboutToDisappear() {
clearInterval(this.timerID);
}
build() {
Column() {
Canvas(this.context)
.width(190)
.height(190)
.onReady(() => {
this.drawFullClock();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
Usage Example
Let’s say you want to display multiple clocks in a grid layout, each representing a major capital in the world.
You can simply import and use MechanicClock
:
@Component
export struct CapitalClocks {
@State cities: City[] = topCapitals
@State clockTypes: boolean[] = Array(topCapitals.length).fill(false)
build() {
Column() {
Grid() {
ForEach(this.cities, (item: City, index?: number) => {
GridItem() {
Column() {
Text(item.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
MechanicClock({ timezone: item.timezone })
}
.padding(8)
}
.backgroundColor(Color.White)
.border({
width: 2,
color: '#e0e0e0',
radius: 12
})
.shadow({
radius: 4,
color: '#20000000',
offsetX: 1,
offsetY: 1
})
})
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.width('100%')
.height('100%')
.layoutWeight(1)
}
.height('100%')
.width('100%')
}
}
Test Results
Once implemented, each mechanical clock updates independently based on its timezone, and the rendering remains smooth and consistent thanks to the use of ArkUI state management and Canvas drawing context.
Limitations
- Canvas support starts from API Level 8 and above.
- Rendering too many Canvas components at once might affect performance on low-end devices.
Final Thoughts
If you ever felt limited by UI components and wanted complete control over your visuals, Canvas is your playground. Combine it with ArkUI’s reactivity and ArkTS’s structure, and you unlock the ability to create truly dynamic and beautiful UIs — just like our ticking mechanical clock.
Got questions or ideas for improvements? Drop a comment!
References
Drawing Custom UI by Using Canvas
State Management Overview
Written by Mehmet Algul
This content originally appeared on DEV Community and was authored by HarmonyOS