Hướng Dẫn Xây Dựng Giao Diện Tùy Chỉnh Động Với Canvas
Mục Lục
- Giới thiệu
- Tại Sao Chọn Canvas?
- Xây Dựng: Đồng Hồ Cơ Động
- Thành Phần MechanicClock
- Ví Dụ Sử Dụng
- Kết Quả Kiểm Tra
- Giới Hạn
- Suy Nghĩ Cuối Cùng
- Câu Hỏi Thường Gặp
Giới Thiệu
Khi phát triển ứng dụng trên HarmonyOS, không phải lúc nào các thành phần giao diện mặc định cũng đáp ứng được nhu cầu của bạn. Có thể bạn cần một nút bấm được thiết kế đặc biệt, một thanh tiến độ độc đáo, hoặc thậm chí là một chiếc đồng hồ analog sống động. Mặc dù Canvas cho phép bạn vẽ bất kỳ thứ gì, nhưng việc biến những hình ảnh đó thành các thành phần động, dựa trên trạng thái, và phản hồi là điều mà nhiều lập trình viên gặp khó khăn.
Trong bài viết này, chúng ta sẽ khám phá cách sử dụng Canvas cùng với ArkTS và mô hình thành phần của ArkUI để tạo ra một chiếc đồng hồ cơ hoàn toàn tùy chỉnh, cập nhật mỗi giây mà không cần tải lại toàn bộ trang hoặc thành phần.
Tại Sao Chọn Canvas?
Canvas cho phép lập trình viên kiểm soát trực tiếp việc vẽ từng pixel. Điều này rất quan trọng khi xây dựng:
- Đồng hồ tùy chỉnh
- Biểu đồ và đồng hồ đo
- Giao diện trò chơi
- Công cụ vẽ tương tác
- Hoạt ảnh đặc biệt hoặc hình ảnh thương hiệu
Nhưng việc vẽ chỉ là một phần. Thách thức thực sự nằm ở việc làm cho những hình ảnh này phản hồi với sự thay đổi trạng thái, chẳng hạn như thời gian, tiến độ hoặc tương tác của người dùng.
Xây Dựng: Đồng Hồ Cơ Động
Dưới đây là một ví dụ đơn giản về một @Component
có thể tái sử dụng để vẽ một chiếc đồng hồ analog hoạt động hoàn toàn bằng Canvas. Nó là động, nhạy cảm với múi giờ, và hoàn toàn phản hồi.
Thành Phần MechanicClock
typescript
@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)
}
}
Ví Dụ Sử Dụng
Giả sử bạn muốn hiển thị nhiều đồng hồ trong một bố cục lưới, mỗi đồng hồ đại diện cho một thủ đô lớn trên thế giới.
Bạn chỉ cần nhập và sử dụng MechanicClock
:
typescript
@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%')
}
}
Kết Quả Kiểm Tra
Khi đã triển khai, mỗi chiếc đồng hồ cơ cập nhật độc lập dựa trên múi giờ của nó, và việc vẽ vẫn mượt mà và nhất quán nhờ vào việc sử dụng quản lý trạng thái của ArkUI và ngữ cảnh vẽ Canvas.
Giới Hạn
- Hỗ trợ Canvas bắt đầu từ API Level 8 trở lên.
- Việc vẽ quá nhiều thành phần Canvas cùng một lúc có thể ảnh hưởng đến hiệu suất trên các thiết bị yếu.
Suy Nghĩ Cuối Cùng
Nếu bạn từng cảm thấy bị giới hạn bởi các thành phần giao diện và muốn có toàn quyền kiểm soát hình ảnh của mình, Canvas là sân chơi của bạn. Kết hợp nó với reactivity của ArkUI và cấu trúc của ArkTS, bạn mở khóa khả năng tạo ra các giao diện thực sự động và đẹp mắt — giống như chiếc đồng hồ cơ đang tích tắc của chúng ta.
Có câu hỏi hay ý tưởng cải tiến nào không? Hãy để lại bình luận nhé!
Câu Hỏi Thường Gặp
Canvas là gì và tại sao nên sử dụng nó?
Canvas là một API cho phép bạn vẽ đồ họa 2D trực tiếp vào trang web, rất hữu ích cho các ứng dụng đòi hỏi sự tùy chỉnh cao về giao diện.
Tôi có thể sử dụng Canvas cho các thiết bị di động không?
Có, nhưng bạn cần đảm bảo rằng phiên bản API của thiết bị hỗ trợ Canvas.
Làm thế nào để tối ưu hóa hiệu suất khi sử dụng Canvas?
Tránh vẽ quá nhiều thành phần Canvas cùng một lúc và sử dụng các phương pháp quản lý trạng thái hiệu quả.