Read the original article:Pulse Ring with ArkUI Animator
Requirement Description
Implement a minimal Pulse Ring animation for HarmonyOS wearable:
- One-tap play/pause.
- Smooth ring animation (grow/shrink).
- Quick theme & ring color selection.
Background Knowledge
- Prefer
this.getUIContext().createAnimator()(UI context-bound). -
onFrameprovides interpolated value; write it to@Stateand use in UI. -
@Statemust have default values in ArkTS.
Implementation Steps
- Define themes and a small palette for ring color.
- Create a component with
@Statefields:running,progress,themeIdx,ringColor. - Build an Animator with
direction: 'alternate',iterations: -1. - In
onFrame, updateprogress(0..1) and compute ring size. - UI: a centered
Stack()as the ring; aRow()of swatches; a small “Theme” button. - Tap the ring to toggle play/pause.
Code Snippet / Configuration
import { AnimatorResult, AnimatorOptions } from '@kit.ArkUI'
const MIN = 84
const MAX = 140
interface ThemeConf { bg: Color; fg: Color; ring: Color; border: number }
const THEMES: ThemeConf[] = [
{ bg: Color.Black, fg: Color.White, ring: Color.White, border: 3 },
{ bg: Color.White, fg: Color.Black, ring: Color.Black, border: 3 },
{ bg: Color.Gray, fg: Color.Black, ring: Color.White, border: 4 },
]
const RING_PALETTE: Color[] = [ Color.White, Color.Black, Color.Red, Color.Green, Color.Blue, Color.Yellow ]
@Entry
@Component
struct Index {
@State running: boolean = false
@State progress: number = 0
@State themeIdx: number = 0
@State ringColor: Color | null = null
private anim?: AnimatorResult
aboutToDisappear() {
if (this.anim) { try { this.anim.cancel() } catch (_) {} this.anim = undefined }
}
private ensureAnimator() {
if (this.anim) return
const opts: AnimatorOptions = {
duration: 1800, easing: 'smooth', delay: 0, fill: 'forwards',
direction: 'alternate', iterations: -1, begin: 0, end: 1
}
this.anim = this.getUIContext().createAnimator(opts)
this.anim.onFrame = (v: number) => { this.progress = v } // 0..1
this.anim.onFinish = () => { this.running = false }
this.anim.onCancel = () => { this.running = false }
}
private playPause() {
this.ensureAnimator()
if (this.running) { this.anim!.pause(); this.running = false }
else { this.anim!.play(); this.running = true }
}
private ringSize(): number { return MIN + (MAX - MIN) * this.progress }
private theme(): ThemeConf { return THEMES[this.themeIdx % THEMES.length] }
private currentRingColor(): Color { return this.ringColor ?? this.theme().ring }
private nextTheme() { this.themeIdx = (this.themeIdx + 1) % THEMES.length }
build() {
Column() {
Text(this.running ? 'Running' : 'Paused')
.fontSize(12).fontColor(this.theme().fg).margin({ bottom: 6 })
Stack() {
if (!this.running) {
Text('Tap').fontSize(14).fontColor(this.theme().fg)
}
}
.width(this.ringSize()).height(this.ringSize())
.border({ width: this.theme().border, color: this.currentRingColor(), radius: this.ringSize() / 2 })
.backgroundColor(this.theme().bg)
.alignSelf(ItemAlign.Center)
.gesture(TapGesture().onAction(() => this.playPause()))
.margin({ bottom: 10 })
Row() {
ForEach(
RING_PALETTE,
(c: Color, idx: number) => {
Stack()
.width(18).height(18)
.border({ width: 1, color: this.theme().fg, radius: 9 })
.backgroundColor(c)
.margin({ left: 4, right: 4 })
.gesture(TapGesture().onAction(() => { this.ringColor = c }))
},
(c: Color, idx: number) => idx.toString() // trackBy must be string
)
}
.justifyContent(FlexAlign.Center)
.margin({ bottom: 8 })
Button('Theme')
.height(28).width(70)
.onClick(() => this.nextTheme())
.backgroundColor(this.currentRingColor())
.alignSelf(ItemAlign.Center)
}
.width('100%').height('100%')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.padding(8).backgroundColor(this.theme().bg)
}
}
Test Results
- Verified on wearable emulator (square & round) and one device.
- Start/stop latency < 50 ms perceived; animation smooth at default settings
Limitations or Consideration
- Don’t name methods like ArkUI attributes (
size,width,border, …). -
@Statemust have initial values (e.g.,ringColor: Color | null = null). - Inside
Column/Row/StackDSL blocks, only UI syntax (If,ForEach, components) — noconst/let. -
reverse()does not affect animations using interpolating-spring curve (per docs). - Keep logic in
onFramelightweight to save battery.
Top comments (0)