前言
这篇文章是对华为HarmonyOS移动端手表的应用开发示例,用的是6.1.0的版本,给了直接的代码显示,方便一些。
实战机型

一、开发框架与语言优势
| 优势项 | 说明 |
|---|
| ArkTS声明式UI | 基于TS扩展,强类型校验,状态驱动UI,代码简洁;@State、@Builder组件化封装,非常适合表盘、卡片、流光动画开发,就是你当前表盘代码所用范式。 |
| 一次开发多端部署 | 一套ArkTS工程,可同时适配手机、手表;通过设备类型配置区分布局,圆形/方形手表屏幕自动适配,降低多端维护成本。 |
| 完整组件库 | 提供穿戴专属组件:ArcList弧形列表、Swiper、Circle圆形绘制,原生支持表盘环形进度、渐变、模糊、发光特效,直接实现霓虹流光UI效果。 |
| 完整组件生命周期 | aboutToAppear / aboutToDisappear,页面销毁可清理定时器,规避手表端内存泄漏、后台无效动画耗电问题。 |
| DevEco Studio工具链 | 手表模拟器、实时预览、性能剖析,可直接调试动画帧率、内存占用,快速验证表盘动效表现。 |
二、图形动画渲染能力(适配你表盘项目)
| 优势项 | 说明 |
|---|
| 方舟引擎硬件加速 | GPU加速线性渐变、径向渐变、blur模糊、shadow阴影,多层Text叠加发光、粒子光斑动画可以稳定跑高帧率。 |
| 双动画体系 | 属性animation:持续循环呼吸/旋转动画;animateTo:状态平滑过渡,适合心率、数值变化的柔和动画,和现有代码完全匹配。 |
| renderGroup渲染分组 | 支持渲染组隔离,局部状态变更只刷新局部节点,50ms高频角度更新不会全页面重绘,降低手表CPU开销。 |
| 圆形屏幕原生支持 | 弧形组件、表冠旋转事件,圆形表盘坐标计算、环形进度strokeDashArray原生支持,无需自己封装基础能力。 |
三、系统能力:分布式、健康、传感器
| 优势项 | 说明 |
|---|
| 分布式软总线 | 手表‑手机数据无缝同步;手表采集心率步数,手机保存详细记录;支持跨设备调用能力,手表触发手机拍照、告警等场景。 |
| 开放健康服务API | 原生获取心率、步数、睡眠、血氧等体征数据,不用自己对接底层传感器;可直接替换你代码里模拟的heartRate、steps模拟数据。 |
| 传感器能力 | 高精度时钟、加速度、陀螺仪;支持抬腕亮屏、手势、表冠旋转交互。 |
| 端侧AI推理 | 手表本地轻量AI运算,健康风险评估、实时数据滤波,不需要频繁回传手机。 |
四、功耗与稳定性(手表最重要指标)
| 优势项 | 说明 |
|---|
| 轻量化调度内核 | 穿戴版鸿蒙做内核裁剪,后台任务合并唤醒,区分前台动画、后台休眠,减少CPU频繁唤醒,延长续航。 |
| 生命周期管控 | 页面退到后台,组件aboutToDisappear触发,开发者主动销毁定时器、动画,避免后台持续跑50ms高频定时器耗电。 |
| AOD熄屏显示支持 | 表盘应用可适配熄屏常显模式,低功耗绘制极简表盘。 |
| 内存管控 | 框架对应用内存做限制,ArkCompiler编译优化减少内存占用,复杂UI也不容易OOM崩溃。 |
五、业务与生态优势
| 优势项 | 说明 |
|---|
| 原子化服务 | 无需安装完整App,卡片、表盘服务直达,降低用户使用门槛。 |
| 统一HMS Core | 账号、推送、数据存储统一,手表手机账号互通。 |
| 双开发范式 | 全量ArkTS智能手表;Lite Wearable轻量范式,面向GT系列长续航设备,两套方案覆盖几乎全部华为穿戴机型。 |
六、对比开发痛点(开发注意点)
- 手表算力、内存有限,50ms高频定时器要谨慎使用,优先框架动画API,不要大量依靠JS定时器循环驱动画面。
- 小屏圆形布局,需要大量百分比、相对尺寸,禁止硬编码px像素。
- 后台挂起后JS定时器会被系统冻结,持续计时、采集业务需要使用系统专用Timer,不建议原生setInterval。
虚拟机说明
由于我真机的是6.1.0版本,也就是API23,所以对应匹配的虚拟机肯定要一致的。

常量string.json设置
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
},
{
"name": "tab_clock",
"value": "时钟"
},
{
"name": "tab_date_weather",
"value": "日期天气"
},
{
"name": "tab_health",
"value": "步数心率"
},
{
"name": "label_steps",
"value": "步数"
},
{
"name": "label_steps_target",
"value": "目标"
},
{
"name": "label_heart_rate",
"value": "心率"
},
{
"name": "label_heart_unit",
"value": "BPM"
},
{
"name": "weather_sunny",
"value": "晴"
},
{
"name": "weather_temp",
"value": "26°"
},
{
"name": "weather_city",
"value": "深圳"
},
{
"name": "week_monday",
"value": "星期一"
},
{
"name": "week_tuesday",
"value": "星期二"
},
{
"name": "week_wednesday",
"value": "星期三"
},
{
"name": "week_thursday",
"value": "星期四"
},
{
"name": "week_friday",
"value": "星期五"
},
{
"name": "week_saturday",
"value": "星期六"
},
{
"name": "week_sunday",
"value": "星期日"
}
]
}
颜色常量
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "bg_deep_blue",
"value": "#0A192F"
},
{
"name": "accent_gold",
"value": "#FFD700"
},
{
"name": "accent_cyan_blue",
"value": "#4D9DE0"
},
{
"name": "accent_purple",
"value": "#C77DFF"
},
{
"name": "accent_pink",
"value": "#FF6B9D"
},
{
"name": "accent_mint",
"value": "#00F5D4"
},
{
"name": "card_bg",
"value": "#112240"
},
{
"name": "text_primary",
"value": "#E6F1FF"
},
{
"name": "text_secondary",
"value": "#8892B0"
}
]
}
index.ets
页面都写在了index.ets中。
@Entry
@Component
struct Index {
@State currentIndex: number = 0;
@State hours: string = '00';
@State minutes: string = '00';
@State seconds: string = '00';
@State gradientAngle1: number = 0;
@State gradientAngle2: number = 120;
@State gradientAngle3: number = 240;
@State spot1Angle: number = 0;
@State spot2Angle: number = 90;
@State spot3Angle: number = 210;
@State breathScale: number = 1.0;
@State heartRate: number = 72;
@State steps: number = 6428;
@State stepsTarget: number = 10000;
@State dateStr: string = '08月28日';
@State weekdayIdx: number = 4;
private timeTimerId: number = -1;
private animTimerId: number = -1;
private heartTimerId: number = -1;
aboutToAppear(): void {
this.updateTime();
this.updateDate();
this.timeTimerId = setInterval(() => {
this.updateTime();
}, 1000);
this.animTimerId = setInterval(() => {
this.gradientAngle1 = (this.gradientAngle1 + 1.2) % 360;
this.gradientAngle2 = (this.gradientAngle2 + 0.9) % 360;
this.gradientAngle3 = (this.gradientAngle3 + 0.6) % 360;
this.spot1Angle = (this.spot1Angle + 0.35) % 360;
this.spot2Angle = (this.spot2Angle - 0.25 + 360) % 360;
this.spot3Angle = (this.spot3Angle + 0.2) % 360;
}, 50);
this.heartTimerId = setInterval(() => {
let delta: number = Math.floor(Math.random() * 7) - 3;
let next: number = this.heartRate + delta;
if (next < 62) {
next = 62;
}
if (next > 88) {
next = 88;
}
animateTo({ duration: 600, curve: Curve.EaseInOut }, () => {
this.heartRate = next;
this.breathScale = 0.98 + Math.random() * 0.04;
});
}, 1500);
}
aboutToDisappear(): void {
if (this.timeTimerId !== -1) {
clearInterval(this.timeTimerId);
}
if (this.animTimerId !== -1) {
clearInterval(this.animTimerId);
}
if (this.heartTimerId !== -1) {
clearInterval(this.heartTimerId);
}
}
private updateTime(): void {
let now: Date = new Date();
let h: number = now.getHours();
let m: number = now.getMinutes();
let s: number = now.getSeconds();
this.hours = h < 10 ? '0' + h.toString() : h.toString();
this.minutes = m < 10 ? '0' + m.toString() : m.toString();
this.seconds = s < 10 ? '0' + s.toString() : s.toString();
}
private updateDate(): void {
let now: Date = new Date();
let mo: number = now.getMonth() + 1;
let da: number = now.getDate();
let moStr: string = mo < 10 ? '0' + mo.toString() : mo.toString();
let daStr: string = da < 10 ? '0' + da.toString() : da.toString();
this.dateStr = moStr + '月' + daStr + '日';
let w: number = now.getDay();
this.weekdayIdx = w === 0 ? 6 : w - 1;
}
private getWeekdayString(idx: number): string {
if (idx === 0) { return '星期一'; }
if (idx === 1) { return '星期二'; }
if (idx === 2) { return '星期三'; }
if (idx === 3) { return '星期四'; }
if (idx === 4) { return '星期五'; }
if (idx === 5) { return '星期六'; }
return '星期日';
}
private getSpotTranslationX(angle: number, radius: number, center: number): number {
let rad: number = angle * Math.PI / 180;
return center + Math.cos(rad) * radius;
}
private getSpotTranslationY(angle: number, radius: number, center: number): number {
let rad: number = angle * Math.PI / 180;
return center + Math.sin(rad) * radius;
}
@Builder
private cardBackdrop(pageIndex: number) {
Column() {
}
.width('92%')
.height('92%')
.borderRadius(28)
.linearGradient({
angle: (this.gradientAngle1 + pageIndex * 60) % 360,
colors: [
['rgba(17,34,64,0.68)', 0],
['rgba(10,25,47,0.58)', 0.5],
['rgba(26,52,94,0.62)', 1]
]
})
.border({
width: 1,
color: 'rgba(255,215,0,0.25)'
})
.shadow({
radius: 24,
color: 'rgba(77,157,224,0.22)',
offsetX: 0,
offsetY: 4
})
}
@Builder
private backgroundBuilder() {
Stack({ alignContent: Alignment.Center }) {
Column() {
}
.width('100%')
.height('100%')
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#050E1F', 0], ['#0A192F', 0.5], ['#0F2847', 1]]
})
Circle({ width: 180, height: 180 })
.fill(Color.White)
.radialGradient({
center: [50, 50],
radius: 80,
colors: [['rgba(77,157,224,0.75)', 0], ['rgba(77,157,224,0.15)', 0.55], ['rgba(77,157,224,0)', 1]]
})
.translate({
x: this.getSpotTranslationX(this.spot1Angle, 80, 0),
y: this.getSpotTranslationY(this.spot1Angle, 60, 0)
})
.blur(18)
.animation({
duration: 50,
curve: Curve.Linear,
iterations: 1,
playMode: PlayMode.Normal
})
Circle({ width: 200, height: 200 })
.fill(Color.White)
.radialGradient({
center: [50, 50],
radius: 90,
colors: [['rgba(199,125,255,0.65)', 0], ['rgba(199,125,255,0.12)', 0.55], ['rgba(199,125,255,0)', 1]]
})
.translate({
x: this.getSpotTranslationX(this.spot2Angle, 70, 10),
y: this.getSpotTranslationY(this.spot2Angle, 70, -10)
})
.blur(22)
.animation({
duration: 50,
curve: Curve.Linear,
iterations: 1,
playMode: PlayMode.Normal
})
Circle({ width: 160, height: 160 })
.fill(Color.White)
.radialGradient({
center: [50, 50],
radius: 70,
colors: [['rgba(255,107,157,0.6)', 0], ['rgba(0,245,212,0.25)', 0.5], ['rgba(0,245,212,0)', 1]]
})
.translate({
x: this.getSpotTranslationX(this.spot3Angle, 90, -15),
y: this.getSpotTranslationY(this.spot3Angle, 50, 20)
})
.blur(20)
.animation({
duration: 50,
curve: Curve.Linear,
iterations: 1,
playMode: PlayMode.Normal
})
Circle({ width: 60, height: 60 })
.fill(Color.White)
.radialGradient({
center: [50, 50],
radius: 28,
colors: [['rgba(255,215,0,0.55)', 0], ['rgba(255,215,0,0.08)', 0.6], ['rgba(255,215,0,0)', 1]]
})
.translate({ x: 20, y: -30 })
.blur(8)
}
.width('100%')
.height('100%')
.renderGroup(true)
}
@Builder
private fluidDigitBuilder(digit: string, angle1: number, angle2: number, angle3: number, fSize: number) {
Stack({ alignContent: Alignment.Center }) {
Text(digit)
.fontSize(fSize)
.fontWeight(FontWeight.Bolder)
.fontColor('#00FFFFFF')
.linearGradient({
angle: angle1,
colors: [['#4D9DE0', 0], ['#C77DFF', 0.5], ['#FF6B9D', 1]]
})
.opacity(0.55)
.blur(6)
.scale({ x: 1.08, y: 1.08 })
Text(digit)
.fontSize(fSize)
.fontWeight(FontWeight.Bolder)
.linearGradient({
angle: angle2,
colors: [
['#00F5D4', 0],
['#4D9DE0', 0.25],
['#FFD700', 0.5],
['#C77DFF', 0.75],
['#00F5D4', 1]
]
})
.opacity(0.85)
Text(digit)
.fontSize(fSize)
.fontWeight(FontWeight.Bolder)
.linearGradient({
angle: angle3,
colors: [['#FFD700', 0], ['#FFFFFF', 0.4], ['#FF6B9D', 0.75], ['#C77DFF', 1]]
})
.opacity(0.95)
}
.renderGroup(true)
}
@Builder
private colonBuilder(fSize: number) {
Column() {
Circle({ width: fSize * 0.12, height: fSize * 0.12 })
.fill('#FFD700')
.shadow({ radius: 12, color: '#80FFD700', offsetX: 0, offsetY: 0 })
Blank()
.height(fSize * 0.22)
Circle({ width: fSize * 0.12, height: fSize * 0.12 })
.fill('#FFD700')
.shadow({ radius: 12, color: '#80FFD700', offsetX: 0, offsetY: 0 })
}
.width(fSize * 0.25)
.justifyContent(FlexAlign.Center)
}
@Builder
private clockCardBuilder() {
Stack({ alignContent: Alignment.Center }) {
this.cardBackdrop(0)
Stack({ alignContent: Alignment.Center }) {
Circle({ width: 230, height: 230 })
.fill(Color.White)
.radialGradient({
center: [50, 50],
radius: 115,
colors: [['rgba(255,215,0,0.18)', 0], ['rgba(77,157,224,0.08)', 0.5], ['rgba(0,0,0,0)', 1]]
})
.scale({ x: this.breathScale, y: this.breathScale })
.animation({ duration: 1500, curve: Curve.EaseInOut, iterations: -1, playMode: PlayMode.Alternate })
Column({ space: 0 }) {
Row() {
this.fluidDigitBuilder(
this.hours.charAt(0),
this.gradientAngle1,
this.gradientAngle2,
this.gradientAngle3,
68
)
this.fluidDigitBuilder(
this.hours.charAt(1),
(this.gradientAngle1 + 60) % 360,
(this.gradientAngle2 + 90) % 360,
(this.gradientAngle3 + 30) % 360,
68
)
this.colonBuilder(68)
this.fluidDigitBuilder(
this.minutes.charAt(0),
(this.gradientAngle1 + 180) % 360,
(this.gradientAngle2 + 200) % 360,
(this.gradientAngle3 + 150) % 360,
68
)
this.fluidDigitBuilder(
this.minutes.charAt(1),
(this.gradientAngle1 + 240) % 360,
(this.gradientAngle2 + 270) % 360,
(this.gradientAngle3 + 210) % 360,
68
)
}
Row() {
Text(this.seconds)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.linearGradient({
angle: this.gradientAngle1,
colors: [['#00F5D4', 0], ['#4D9DE0', 0.5], ['#FFD700', 1]]
})
.margin({ right: 6 })
Text('秒')
.fontSize(12)
.fontColor('#8892B0')
}
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Center)
}
.width('92%')
.height('92%')
}
.width('100%')
.height('100%')
.renderGroup(true)
}
@Builder
private weatherIconBuilder() {
Stack({ alignContent: Alignment.Center }) {
Circle({ width: 36, height: 36 })
.fill(Color.White)
.radialGradient({
center: [50, 50],
radius: 18,
colors: [['#FFD700', 0], ['#FFA500', 0.7], ['#FF8C00', 1]]
})
.shadow({ radius: 12, color: '#80FFD700', offsetX: 0, offsetY: 0 })
.translate({ x: -10, y: -10 })
Column() {
}
.width(50)
.height(22)
.borderRadius(11)
.linearGradient({
angle: 0,
colors: [['#4D9DE0', 0], ['#C77DFF', 1]]
})
.translate({ x: 8, y: 4 })
.opacity(0.95)
Circle({ width: 20, height: 20 })
.fill('#C77DFF')
.translate({ x: -2, y: -4 })
Circle({ width: 26, height: 26 })
.fill('#4D9DE0')
.translate({ x: 12, y: -2 })
}
.width(90)
.height(70)
}
@Builder
private dateWeatherCardBuilder() {
Stack({ alignContent: Alignment.Center }) {
this.cardBackdrop(1)
Column() {
Text(this.dateStr)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#E6F1FF')
.margin({ top: 14 })
Text(this.getWeekdayString(this.weekdayIdx))
.fontSize(12)
.fontColor('#8892B0')
.margin({ top: 2 })
Blank()
.height(10)
this.weatherIconBuilder()
Blank()
.height(4)
Row() {
Text('26°')
.fontSize(30)
.fontWeight(FontWeight.Bolder)
.linearGradient({
angle: this.gradientAngle2,
colors: [['#FFD700', 0], ['#FF6B9D', 0.5], ['#C77DFF', 1]]
})
Text('晴')
.fontSize(14)
.fontColor('#E6F1FF')
.margin({ left: 8, top: 12 })
}
Text('深圳')
.fontSize(11)
.fontColor('#8892B0')
.margin({ top: 2 })
}
.width('92%')
.height('92%')
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
.renderGroup(true)
}
@Builder
private stepsRingBuilder() {
Stack({ alignContent: Alignment.Center }) {
Circle({ width: 110, height: 110 })
.fill('rgba(255,255,255,0.04)')
.stroke('#1A2A4A')
.strokeWidth(8)
Circle({ width: 110, height: 110 })
.fill('rgba(0,0,0,0)')
.stroke(Color.White)
.strokeWidth(8)
.strokeDashArray([
345.6 * (this.steps / this.stepsTarget),
345.6
])
.strokeDashOffset(-86.4)
.linearGradient({
angle: this.gradientAngle1,
colors: [['#00F5D4', 0], ['#4D9DE0', 0.5], ['#FFD700', 1]]
})
.animation({ duration: 800, curve: Curve.EaseOut })
Column({ space: 0 }) {
Text(this.steps.toString())
.fontSize(22)
.fontWeight(FontWeight.Bolder)
.linearGradient({
angle: this.gradientAngle3,
colors: [['#FFD700', 0], ['#FF6B9D', 1]]
})
Text('步数')
.fontSize(10)
.fontColor('#8892B0')
.margin({ top: 1 })
Text('目标 ' + this.stepsTarget.toString())
.fontSize(9)
.fontColor('#8892B0')
.margin({ top: 1 })
}
.alignItems(HorizontalAlign.Center)
}
.width(120)
.height(120)
}
@Builder
private heartSectionBuilder() {
Column({ space: 0 }) {
Row() {
Column() {
Text('♥')
.fontSize(18)
.fontColor('#FF6B9D')
.scale({ x: this.breathScale, y: this.breathScale })
.animation({
duration: 900,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
}
.margin({ right: 6 })
Column({ space: 0 }) {
Text(this.heartRate.toString())
.fontSize(26)
.fontWeight(FontWeight.Bolder)
.linearGradient({
angle: this.gradientAngle2,
colors: [['#FF6B9D', 0], ['#C77DFF', 0.6], ['#FFD700', 1]]
})
Text('BPM')
.fontSize(9)
.fontColor('#8892B0')
}
}
Text('心率')
.fontSize(10)
.fontColor('#8892B0')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
}
@Builder
private healthCardBuilder() {
Stack({ alignContent: Alignment.Center }) {
this.cardBackdrop(2)
Column() {
this.stepsRingBuilder()
Blank()
.height(10)
this.heartSectionBuilder()
}
.width('92%')
.height('92%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
.renderGroup(true)
}
@Builder
private pageIndicatorBuilder() {
Row() {
ForEach([0, 1, 2], (idx: number) => {
if (idx === this.currentIndex) {
Column() {
}
.width(16)
.height(4)
.borderRadius(2)
.linearGradient({
angle: 0,
colors: [['#FFD700', 0], ['#FF6B9D', 1]]
})
.animation({ duration: 260, curve: Curve.EaseOut })
.margin({ left: 3, right: 3 })
} else {
Column() {
}
.width(4)
.height(4)
.borderRadius(2)
.backgroundColor('rgba(136,146,176,0.55)')
.animation({ duration: 260, curve: Curve.EaseOut })
.margin({ left: 3, right: 3 })
}
}, (idx: number) => idx.toString())
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
build() {
Stack({ alignContent: Alignment.Center }) {
this.backgroundBuilder()
Column() {
Swiper() {
this.clockCardBuilder()
this.dateWeatherCardBuilder()
this.healthCardBuilder()
}
.width('100%')
.height('84%')
.index(this.currentIndex)
.autoPlay(false)
.indicator(false)
.loop(true)
.curve(Curve.Smooth)
.duration(420)
.itemSpace(0)
.displayMode(SwiperDisplayMode.Stretch)
.cachedCount(2)
.onChange((index: number) => {
animateTo({ duration: 260, curve: Curve.EaseOut }, () => {
this.currentIndex = index;
});
})
Blank()
.height(4)
this.pageIndicatorBuilder()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.padding({ top: 18, bottom: 12 })
}
.width('100%')
.height('100%')
.renderGroup(true)
}
}
初始页面效果



简洁化处理一下

简单多了。

鸿蒙ArkTS智能表盘页面技术说明
代码:@Entry 装饰器Index组件,Swiper三卡片滑动页面,包含时钟、日期天气、健康运动,大量渐变、粒子光点、呼吸动画、定时器驱动动态效果。
组件整体概览
| 项目 | 说明 |
|---|
| 开发框架 | HarmonyOS ArkTS,基于ArkUI声明式UI |
| 入口装饰 | @Entry @Component,页面根组件 |
| 页面模式 | Swiper轮播3个子卡片:时钟卡片 / 日期天气卡片 / 健康运动卡片 |
| 状态驱动 | @State管理全部UI可变状态;3组定时器做时间更新、背景粒子动画、心率模拟 |
| 生命周期 | aboutToAppear初始化定时器;aboutToDisappear销毁全部定时器,防止内存泄漏 |
| 渲染优化 | 多处.renderGroup(true),减少子树重绘开销 |
@State状态变量清单
| 状态变量 | 类型 | 作用 |
|---|
| currentIndex | number | Swiper当前页面下标0/1/2,联动分页指示器 |
| hours/minutes/seconds | string | 格式化时分秒,双位字符串00‑23 |
| gradientAngle1/2/3 | number | 3组渐变旋转角度,驱动文字、卡片渐变流动效果 |
| spot1Angle/2/3 | number | 背景光斑粒子旋转角度,计算坐标做圆周运动 |
| breathScale | number | 呼吸缩放系数,圆环、心跳图标呼吸动效 |
| heartRate | number | 模拟心率数值,62‑88区间随机波动 |
| steps | number | 当前步数 |
| stepsTarget | number | 步数目标值,用于进度环计算 |
| dateStr | string | 格式化日期 MM月DD日 |
| weekdayIdx | number | 星期索引0~6,对应周一至周日 |
私有定时器成员
| 变量 | 周期 | 职责 | 销毁时机 |
|---|
| timeTimerId | 1000ms | 每秒调用updateTime刷新系统时间 | aboutToDisappear清除 |
| animTimerId | 50ms | 高频更新渐变角度、光斑角度,实现流光粒子动画 | aboutToDisappear清除 |
| heartTimerId | 1500ms | 模拟心率随机波动,配合animateTo做平滑过渡 | aboutToDisappear清除 |
核心业务方法
| 方法 | 功能描述 |
|---|
| updateTime() | 获取系统Date对象,格式化补零得到时分秒字符串 |
| updateDate() | 获取系统日期,生成MM月DD日,转换星期索引 |
| getWeekdayString(idx) | 索引转中文星期文本 |
| getSpotTranslationX/Y | 角度→弧度计算,算出圆周运动粒子偏移坐标,供translate使用 |
@Builder自定义构建器
| Builder名称 | 用途 |
|---|
| cardBackdrop(pageIndex) | 卡片通用背景:半透明深色线性渐变、金色细边框、蓝调阴影,每张卡片独立偏移渐变角度 |
| backgroundBuilder() | 全局底层背景:垂直渐变底色 + 三组模糊彩色光斑做圆周漂浮 + 金色小光晕粒子 |
| fluidDigitBuilder() | 流光数字组件:三层叠加Text,分别做模糊外发光、主流光、高光层,实现立体霓虹数字效果 |
| colonBuilder() | 时钟冒号,两个金色发光圆点 |
| clockCardBuilder() | 第0页‑时钟卡片:呼吸径向渐变圆环 + fluidDigit渲染时分秒 + 秒数显示 |
| weatherIconBuilder() | 简易天气图标:太阳+云朵组合图形 |
| dateWeatherCardBuilder() | 第1页‑日期天气卡片:日期、星期、天气图标、模拟温度、城市 |
| stepsRingBuilder() | 步数进度环:双层Circle,strokeDashArray绘制环形进度,根据steps/stepsTarget计算弧长 |
| heartSectionBuilder() | 心率模块:爱心图标呼吸缩放 + 心率数值展示 |
| healthCardBuilder() | 第2页‑健康卡片:步数环形进度 + 心率模块 |
| pageIndicatorBuilder() | Swiper自定义分页指示器,激活态渐变长条,非激活小圆点 |
UI交互与动画机制
| 模块 | 实现要点 |
|---|
| Swiper滑动 | loop循环开启,关闭内置indicator;onChange更新currentIndex,驱动自定义指示器 |
| 流光渐变动画 | setInterval持续累加angle角度,linearGradient绑定angle,实现颜色流动;取模360防止溢出 |
| 光斑圆周运动 | 三角函数cos/sin计算translate偏移,不断修改角度实现粒子绕圈漂浮 |
| 呼吸缩放 | 组件.animation配置无限循环Alternate,scale在区间往复缩放 |
| 心率平滑变化 | 定时器生成随机值做边界钳位,animateTo实现状态平滑过渡 |
| 环形进度 | strokeDashArray绘制圆弧;strokeDashOffset设置起始偏移,实现从顶部开始的进度环 |
关键技术点与风险
| 问题点 | 说明 |
|---|
| 高频setInterval | animTimerId间隔50ms,大量状态频繁更新,依赖renderGroup做渲染隔离;页面销毁必须clearInterval,否则后台持续运行内存泄露 |
| 角度取模 | 所有角度计算使用%360,防止数值无限累加变大 |
| 进度环计算 | 圆环周长 = 2πradius;代码硬编码周长345.6,strokeDashOffset偏移‑86.4,让进度起点位于圆环顶部 |
| 文字多层叠加 | fluidDigit三层Text叠加:模糊辉光层、主流光层、高光层,模拟硬件霓虹发光效果 |
| animateTo vs 属性animation | 属性animation用于组件固有持续动画;animateTo用于状态变量的过程式平滑变更 |
| 日期星期转换 | JS Date.getDay():0=周日,代码做映射转为索引0=周一 |
所有评论(0)