Read the original article:Creating a Touch-Responsive Angle Measurement Tool Using Canvas
Creating a Touch-Responsive Angle Measurement Tool Using Canvas
Requirement Description
How to implement an interactive protractor (angle measurement tool) using the Canvas component, allowing users to measure angles through touch interaction with real-time visual feedback.
Background Knowledge
- Canvas provides a canvas component for custom graphics drawing. Developers use CanvasRenderingContext2D objects and OffscreenCanvasRenderingContext2D objects to draw on the Canvas component, supporting shapes, text, images, and complex animations. The CanvasRenderingContext2D.arc method can draw arcs.
- onTouch is a touch event where finger touch actions trigger the callback function.
Implementation Steps
Use the Canvas component to create a custom semicircular gauge with scale markings. The specific implementation steps are as follows:
- Draw the protractor panel: In the draw() method, clear the canvas. Use the arc() method to draw a semicircle with a radius of 120 to form the protractor panel.
- Draw scale lines: Loop from 90 degrees to 270 degrees, incrementing by 5 degrees each time. Use moveTo() and lineTo() methods to draw each scale line, with lines extending inward 3 units from radius 120.
- Draw the pointer: Use the arc() method to draw a sector area starting from 180 degrees (directly left), drawing to the corresponding angle based on the angel variable value. Use stroke() and fill() methods to complete the pointer drawing.
- Handle touch events: In the onTouch event, calculate the touch point coordinates relative to the center point. Use Math.atan() to calculate the angle, adjusting the angle range to 0-180 degrees as needed. Update the angel variable and call the draw() method to redraw the protractor, displaying the current angle.
- Display current angle: Use a Text component to display the current angle value, formatted to two decimal places.
Code Snippet / Configuration
Complete implementation:
@Entry
@Component
struct Index {
@State angel: number = 0
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
@State canvasH: number = 150
private radius: number = 0
private centerX: number = 0
private centerY: number = 0
private draw() {
this.context.clearRect(0, 0, this.context.width, this.context.height)
this.context.beginPath()
this.context.arc(this.centerX, this.centerY, this.radius, Math.PI, Math.PI * 2)
this.context.lineWidth = 1.5
this.context.strokeStyle = '#222222'
this.context.stroke()
for (let deg: number = 90; deg <= 270; deg += 5) {
const rad: number = deg * Math.PI / 180
const inner: number = this.radius - (deg % 10 === 0 ? 6 : 3)
const x1: number = this.centerX + this.radius * Math.sin(rad)
const y1: number = this.centerY + this.radius * Math.cos(rad)
const x2: number = this.centerX + inner * Math.sin(rad)
const y2: number = this.centerY + inner * Math.cos(rad)
this.context.beginPath()
this.context.lineWidth = deg % 10 === 0 ? 1.5 : 1
this.context.strokeStyle = deg % 10 === 0 ? '#FF4D4F' : '#D46B08'
this.context.moveTo(x1, y1)
this.context.lineTo(x2, y2)
this.context.stroke()
}
const endRad: number = Math.PI + (this.angel * Math.PI / 180)
this.context.beginPath()
this.context.strokeStyle = '#1677FF'
this.context.fillStyle = 'rgba(22,119,255,0.15)'
this.context.moveTo(this.centerX, this.centerY)
this.context.lineTo(this.centerX - this.radius, this.centerY)
this.context.arc(this.centerX, this.centerY, this.radius, Math.PI, endRad)
this.context.lineTo(this.centerX, this.centerY)
this.context.stroke()
this.context.fill()
}
build() {
Column() {
Canvas(this.context)
.width('100%')
.height(this.canvasH)
.backgroundColor(Color.Transparent)
.onReady(() => {
this.centerX = this.context.width / 2
this.centerY = this.context.height
this.radius = Math.min(this.centerX, this.centerY) - 8
this.draw()
})
.onTouch((event: TouchEvent) => {
if (!event.touches || event.touches.length === 0) {
return
}
const tx: number = event.touches[0].x
const ty: number = event.touches[0].y
const vx: number = this.centerX - tx
const vy: number = this.centerY - ty
let deg: number = Math.atan2(vy, vx) * 180 / Math.PI
if (deg < 0) {
deg += 180
}
if (deg > 180) {
deg = 180
}
if (deg < 0) {
deg = 0
}
this.angel = deg
this.draw()
})
Text('Angle: ' + this.angel.toFixed(1) + '°')
.fontSize(14)
.margin({ top: 6 })
}
.width('100%')
.height('100%')
.padding(8)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('#F7F8FA')
.onAreaChange((_old, cur) => {
const pad: number = 12
const w: number = Math.floor(Number(cur.width) - pad * 2)
const h: number = Math.floor(w / 2)
if (Math.abs(h - this.canvasH) >= 2) {
this.canvasH = h > 80 ? h : 80
this.centerX = Math.floor(w / 2)
this.centerY = this.canvasH
this.radius = Math.min(this.centerX, this.centerY) - 8
if (this.context.width && this.context.height) {
this.draw()
}
}
})
}
}
Test Results
The implementation successfully demonstrates:
- Interactive protractor with real-time angle measurement (0-180 degrees)
- Touch-responsive pointer that follows user interaction
- Visual feedback with colored scale markings (red) and pointer area (green)
- Accurate angle calculation using arctangent mathematics
- Dynamic text display showing angle to two decimal precision
- Smooth canvas redrawing on each touch event
Limitations or Considerations
- This example supports API Version 19 Release and above
- Compatible with HarmonyOS 5.1.1 Release SDK and above
- Requires DevEco Studio 5.1.1 Release or later for compilation and execution
- The protractor measures angles from 0 to 180 degrees (semicircle only)
- Touch interaction requires proper handling of coordinate system transformations
Related Documents or Links
- Canvas Component Guide
- CanvasRenderingContext2D API
- OffscreenCanvasRenderingContext2D API
- arc() Method Documentation
- onTouch Event Documentation

Top comments (0)