欢迎光临
我们一直在努力

测速仪表盘案例新特性接入

从零打造跑车仪表盘:Canvas绘制 + V2状态管理 + Animator动画驱动

摘要:本文基于 HarmonyOS ArkTS 开发框架,使用 Canvas 画布组件 从零绘制一个跑车风格的汽车速度仪表盘,结合 状态管理V2(@ComponentV2、@Local、@Param、@Monitor、@Computed)实现组件间数据流管理,并通过 Animator 动画 驱动指针平滑过渡。同时实现了长按按钮持续加速/刹车的交互效果。文章涵盖完整实现链路、V1与V2对比、Canvas分层绘制技巧及踩坑记录。


📋 目录

  • 一、效果展示
  • 二、技术栈与环境
  • 三、项目结构
  • 四、V2 状态管理设计
  • 五、Canvas 绘制详解
  • 六、动画系统
  • 七、交互设计
  • 八、完整代码
  • 九、踩坑记录
  • 十、总结与扩展

一、效果展示

功能概览

功能说明
仪表盘表盘 0-240 km/h 跑车风格深色表盘,三色分区弧线(绿/黄/红)
菱形指针 红色发光指针,随速度平滑旋转
数字速度 表盘中央大字号显示当前速度 + km/h 单位
加油加速 长按右按钮,速度持续增加(每50ms +3 km/h)
刹车减速 长按左按钮,速度持续减少(每50ms -5 km/h)
区间提示 根据速度显示"安全驾驶"/“注意速度”/“超速警告!”

速度分区配色

绿色安全区 (0-80 km/h) ██ #00E676
黄色警告区 (80-160 km/h) ██ #FFB300
红色危险区 (160-240 km/h) ██ #FF1744


二、技术栈与环境

项目版本/说明
开发工具 DevEco Studio 6.1+
API Version API 23+
编程语言 ArkTS
状态管理 V2(@ComponentV2 + @Local + @Param + @Monitor + @Computed)
绘图组件 Canvas + CanvasRenderingContext2D
动画 @ohos.animator(AnimatorResult)
交互 onTouch + setInterval

核心技术点

  • @ComponentV2:新一代组件装饰器,提供更细粒度的响应式更新
  • Canvas 分层绘制:7层独立绘制逻辑,每层职责清晰
  • Animator 动画:createAnimator() 创建动画实例,onFrame 回调驱动每帧重绘
  • onTouch 长按交互:通过触摸事件 + 定时器实现持续加速/刹车

  • 三、项目结构

    entry/src/main/ets/
    ├── common/
    │ └── Constants.ets ← 全局常量(新增汽车仪表盘相关常量)
    ├── components/
    │ └── CarDashboard.ets ← 🆕 汽车仪表盘组件(V2)
    ├── pages/
    │ └── CarSpeedTest.ets ← 🆕 汽车测速页面
    ├── entryability/
    │ └── EntryAbility.ets ← 入口Ability(更新为加载新页面)
    └── …

    文件职责说明

    文件职责状态管理版本
    Constants.ets 集中管理速度范围、角度、颜色、字体、动画参数等常量 纯静态类
    CarDashboard.ets Canvas 绘制仪表盘 + Animator 驱动指针动画 V2
    CarSpeedTest.ets 页面布局 + 按钮交互 + 状态管理 V1(@Entry)
    EntryAbility.ets 应用入口,加载 CarSpeedTest 页面

    四、V2 状态管理设计

    4.1 V1 与 V2 装饰器对比

    V1 装饰器V2 装饰器方向说明
    @State @Local 自身 组件内部响应式状态
    @Prop @Param 父→子 父组件向子组件单向传递
    @Link @Param + @Event 父↔子 双向绑定(通过事件回调)
    @Watch @Monitor 自身 监听属性变化触发回调
    @Provide @Provider 子树 跨组件状态共享
    @Consume @Consumer 子树 消费共享状态
    计算属性 @Computed 自身 派生计算值(自动缓存)
    @Once 父→子 仅首次初始化

    4.2 本项目的状态管理架构

    CarSpeedTest (@Entry @Component) ← V1 页面组件

    ├── @State currentSpeed: number ← 页面级速度状态
    ├── @State isAccelerating: boolean ← 加速按钮状态
    ├── @State isBraking: boolean ← 刹车按钮状态

    └── CarDashboard (@ComponentV2) ← V2 仪表盘组件

    ├── @Local currentSpeed: number ← 内部动画速度(驱动Canvas重绘)
    ├── @Local canvasSize: number ← 画布尺寸
    ├── @Param targetSpeed: number ← 接收外部目标速度
    ├── @Param isAccelerating ← 接收加速状态
    ├── @Param isBraking ← 接收刹车状态
    ├── @Event onSpeedChange ← 速度变化回调
    ├── @Monitor('targetSpeed') ← 监听目标速度→触发动画
    └── @Computed speedZone ← 派生速度区间

    4.3 为什么页面用 V1,子组件用 V2?

    关键决策:@Entry 装饰器与 @ComponentV2 在某些 API 版本下存在兼容性问题。为了确保稳定性:

    • 页面级使用 @Entry @Component(V1),通过 @State 管理状态
    • 子组件使用 @ComponentV2(V2),通过 @Param 接收数据

    V1 页面的 @State 可以传递给 V2 子组件的 @Param,两者可以混用在同一页面中。

    4.4 @Monitor 替代 @Watch

    在 V2 组件中,@Monitor 是 @Watch 的替代方案。当 targetSpeed(来自父组件的速度值)变化时,自动触发动画过渡:

    @Monitor('targetSpeed')
    onTargetSpeedChange() {
    if (this.currentSpeed === this.targetSpeed) {
    return;
    }
    // 停止旧动画,创建新动画
    if (this.speedAnimator) {
    this.speedAnimator.finish();
    }
    this.startAnimation(this.currentSpeed, this.targetSpeed);
    }


    五、Canvas 绘制详解

    5.1 七层分层绘制策略

    仪表盘采用分层绘制,每层职责独立,从底到顶依次绘制:

    ┌─────────────────────────────────────────┐
    │ Layer 7: drawDigitalDisplay() │ 数字速度 + km/h 单位
    │ Layer 6: drawNeedle() │ 菱形指针 + 中心圆 + 发光
    │ Layer 5: drawProgressArc() │ 当前速度进度弧 + 发光效果
    │ Layer 4: drawScaleNumbers() │ 刻度数字 0,20,40,…,240
    │ Layer 3: drawScaleMarks() │ 主刻度线 + 副刻度线
    │ Layer 2: drawColorZones() │ 三色弧线区段(绿/黄/红)
    │ Layer 1: drawDialBackground() │ 表盘深色背景 + 外环边框
    └─────────────────────────────────────────┘

    5.2 角度坐标系

    仪表盘采用底部开口式设计(类似跑车仪表),角度范围 270°:

    起始角度: 135° (左下方)
    结束角度: 45° (右下方)
    跨越角度: 270° (从底部经过左侧、顶部到右侧)

    角度映射公式:
    当前角度 = 起始角度 + 270° × (当前速度 / 最大速度)

    示例:
    0 km/h → 135°
    120 km/h → 135° + 270° × 0.5 = 270°
    240 km/h → 135° + 270° = 405° (即 45°)

    5.3 三色分区弧线实现

    三色分区是跑车仪表盘的核心视觉特征。每段弧线独立绘制:

    private drawColorZones(cx: number, cy: number) {
    const totalAngleRange = this.getEffectiveAngleRange(); // 270°
    const startRad = this.degToRad(Constants.CAR_START_ANGLE);

    // 计算分区角度
    const greenEndFrac = 80 / 240; // 绿色段占比
    const yellowEndFrac = 160 / 240; // 黄色段占比

    // 绿色段 (0-80 km/h)
    this.ctx.beginPath();
    this.ctx.arc(cx, cy, this.radius, startRad, greenEndRad);
    this.ctx.strokeStyle = Constants.CAR_COLOR_GREEN; // #00E676
    this.ctx.globalAlpha = 0.25;
    this.ctx.stroke();

    // 黄色段、红色段类似…
    }

    5.4 刻度线与数字

    刻度线分为主刻度(每20km/h)和副刻度(每10km/h),已越过的刻度线高亮显示:

    for (let i = 0; i <= totalSteps; i++) {
    const speed = i * 10;
    const angle = startAngle + totalDegrees * speed / maxSpeed;
    const rad = this.degToRad(angle);
    const isMajor = speed % 20 === 0;
    const isActive = speed <= this.currentSpeed; // 已越过

    // 计算刻度线两端坐标(三角函数)
    const outerR = this.radius this.progressRingWidth * 0.5;
    const innerR = outerR tickLength;
    const x1 = cx + Math.cos(rad) * outerR;
    const y1 = cy + Math.sin(rad) * outerR;
    const x2 = cx + Math.cos(rad) * innerR;
    const y2 = cy + Math.sin(rad) * innerR;

    // 高亮已越过的刻度
    this.ctx.strokeStyle = isActive
    ? Constants.CAR_COLOR_SCALE_ACTIVE // 白色
    : Constants.CAR_COLOR_SCALE; // 灰蓝色
    }

    5.5 菱形指针设计

    指针使用坐标系变换(save → translate → rotate)绘制菱形:

    this.ctx.save();
    this.ctx.translate(cx, cy); // 移动原点到表盘中心
    this.ctx.rotate(rad); // 旋转到目标角度

    // 菱形指针四个顶点
    this.ctx.beginPath();
    this.ctx.moveTo(0, needleLength); // 针尖
    this.ctx.lineTo(needleWidth, 0); // 左腰
    this.ctx.lineTo(0, needleLength * 0.25); // 底部中点
    this.ctx.lineTo(needleWidth, 0); // 右腰
    this.ctx.closePath();

    // 红色填充 + 发光阴影
    this.ctx.shadowBlur = 12;
    this.ctx.shadowColor = '#FF5252';
    this.ctx.fillStyle = '#FF1744';
    this.ctx.fill();

    this.ctx.restore();


    六、动画系统

    6.1 Animator 配置

    使用 @kit.ArkUI 中的 AnimatorResult 实现指针平滑过渡:

    private animatorOptions: AnimatorOptions = {
    duration: 200, // 200ms 快速响应
    easing: 'ease-out', // 减速曲线,模拟惯性
    delay: 0,
    fill: 'forwards', // 动画结束保持最终状态
    direction: 'normal',
    iterations: 1,
    begin: 0,
    end: 0
    };

    6.2 动画创建与触发

    动画实例在组件 aboutToAppear 生命周期中创建,在 aboutToDisappear 中销毁:

    aboutToAppear(): void {
    this.speedAnimator = this.getUIContext().createAnimator(this.animatorOptions);
    }

    aboutToDisappear(): void {
    if (this.speedAnimator) {
    this.speedAnimator.finish();
    this.speedAnimator = undefined;
    }
    }

    当 @Monitor 监听到 targetSpeed 变化时,重置并播放动画:

    private startAnimation(from: number, to: number) {
    this.animatorOptions.begin = from;
    this.animatorOptions.end = to;
    this.speedAnimator.reset(this.animatorOptions);
    this.speedAnimator.onFrame = (value: number) => {
    this.currentSpeed = value; // 更新内部速度
    this.drawDashboard(); // 每帧重绘Canvas
    };
    this.speedAnimator.play();
    }

    6.3 性能优化要点

    • 短动画时长:200ms 确保快速响应,避免动画堆积
    • ease-out 缓动:模拟物理惯性,速度变化更自然
    • 及时 finish:新动画开始前先 finish() 旧动画,防止冲突
    • 条件绘制:canvasSize <= 0 时跳过绘制,避免无效计算

    七、交互设计

    7.1 长按持续加速/刹车

    使用 .onTouch() 事件监听按钮触摸状态,配合 setInterval 定时器实现持续变速:

    // 加速按钮
    Button('加油加速')
    .onTouch((event: TouchEvent) => {
    if (event.type === TouchType.Down) {
    this.startAccelerate(); // 开始定时加速
    } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
    this.stopAccelerate(); // 停止加速
    }
    });

    // 加速逻辑
    private startAccelerate() {
    this.isAccelerating = true;
    this.accelerateTimer = setInterval(() => {
    if (this.currentSpeed < Constants.CAR_SPEED_MAX) {
    this.currentSpeed = Math.min(
    this.currentSpeed + Constants.CAR_ACCELERATE_RATE, // +3 km/h
    Constants.CAR_SPEED_MAX
    );
    } else {
    this.stopAccelerate(); // 到达最大值自动停止
    }
    }, Constants.CAR_TIMER_INTERVAL); // 每50ms执行一次
    }

    7.2 速度边界处理

    场景处理方式
    速度达到最大值(240) 自动停止加速定时器
    速度降到最小值(0) 自动停止刹车定时器
    组件销毁(aboutToDisappear) 清除所有定时器,防止内存泄漏
    触摸取消(TouchType.Cancel) 等同于松手,停止定时器

    7.3 按钮视觉效果

    按钮在不同状态下呈现不同样式:

    .opacity(this.isAccelerating ? 0.7 : 1) // 按下时透明度降低
    .shadow({
    radius: this.isAccelerating ? 4 : 12, // 按下时阴影收缩
    color: Constants.CAR_COLOR_GREEN,
    offsetY: this.isAccelerating ? 1 : 4 // 模拟按钮下沉
    })


    八、完整代码

    8.1 CarDashboard.ets(仪表盘组件)

    import { AnimatorOptions, AnimatorResult } from '@kit.ArkUI';
    import { Constants } from '../common/Constants';

    @ComponentV2
    export struct CarDashboard {
    // V2 状态装饰器
    @Local currentSpeed: number = 0;
    @Local canvasSize: number = 0;

    @Param targetSpeed: number = 0;
    @Param isAccelerating: boolean = false;
    @Param isBraking: boolean = false;
    @Event onSpeedChange: (speed: number) => void = (speed: number) => {};

    // 计算属性
    @Computed get speedPercent(): number {
    return (this.currentSpeed Constants.CAR_SPEED_MIN) /
    (Constants.CAR_SPEED_MAX Constants.CAR_SPEED_MIN);
    }

    @Computed get speedZone(): string {
    if (this.currentSpeed <= Constants.CAR_GREEN_ZONE_END) return 'safe';
    if (this.currentSpeed <= Constants.CAR_YELLOW_ZONE_END) return 'moderate';
    return 'danger';
    }

    @Computed get zoneColor(): string {
    if (this.currentSpeed <= Constants.CAR_GREEN_ZONE_END) return Constants.CAR_COLOR_GREEN;
    if (this.currentSpeed <= Constants.CAR_YELLOW_ZONE_END) return Constants.CAR_COLOR_YELLOW;
    return Constants.CAR_COLOR_RED;
    }

    // 监听目标速度变化 → 触发平滑动画
    @Monitor('targetSpeed')
    onTargetSpeedChange() {
    if (this.currentSpeed === this.targetSpeed) return;
    if (this.speedAnimator) this.speedAnimator.finish();
    this.startAnimation(this.currentSpeed, this.targetSpeed);
    }

    // 监听画布尺寸变化 → 重算布局参数
    @Monitor('canvasSize')
    onCanvasSizeChange() {
    this.progressRingWidth = this.canvasSize * 0.04;
    this.radius = (this.canvasSize this.progressRingWidth) * 0.5 * 0.9;
    this.needleLength = this.radius * 0.72;
    this.needleWidth = this.canvasSize * 0.02;
    this.centerDotRadius = this.canvasSize * 0.04;
    }

    // 私有成员
    private setting: RenderingContextSettings = new RenderingContextSettings(true);
    private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.setting);
    private speedAnimator: AnimatorResult | undefined = undefined;
    private animatorOptions: AnimatorOptions = {
    duration: Constants.CAR_ANIMATION_DURATION,
    easing: 'ease-out',
    delay: 0, fill: 'forwards', direction: 'normal',
    iterations: 1, begin: 0, end: 0
    };
    private progressRingWidth: number = 0;
    private radius: number = 0;
    private needleLength: number = 0;
    private needleWidth: number = 0;
    private centerDotRadius: number = 0;

    // 动画驱动
    private startAnimation(from: number, to: number) {
    if (!this.speedAnimator) return;
    this.animatorOptions.begin = from;
    this.animatorOptions.end = to;
    this.speedAnimator.reset(this.animatorOptions);
    this.speedAnimator.onFrame = (value: number) => {
    this.currentSpeed = value;
    this.onSpeedChange(value);
    this.drawDashboard();
    };
    this.speedAnimator.play();
    }

    // 绘制入口(7层分层绘制)
    private drawDashboard() {
    if (this.canvasSize <= 0) return;
    const cx = this.canvasSize / 2, cy = this.canvasSize / 2;
    this.ctx.clearRect(0, 0, this.canvasSize, this.canvasSize);
    this.drawDialBackground(cx, cy);
    this.drawColorZones(cx, cy);
    this.drawScaleMarks(cx, cy);
    this.drawScaleNumbers(cx, cy);
    this.drawProgressArc(cx, cy);
    this.drawNeedle(cx, cy);
    this.drawDigitalDisplay(cx, cy);
    }

    // Layer 1: 表盘背景
    private drawDialBackground(cx: number, cy: number) {
    this.ctx.beginPath();
    this.ctx.arc(cx, cy, this.radius + this.progressRingWidth * 0.5, 0, 2 * Math.PI);
    this.ctx.lineWidth = 3;
    this.ctx.strokeStyle = Constants.CAR_COLOR_BORDER;
    this.ctx.stroke();
    this.ctx.beginPath();
    this.ctx.arc(cx, cy, this.radius + this.progressRingWidth * 0.5, 0, 2 * Math.PI);
    this.ctx.fillStyle = Constants.CAR_COLOR_DIAL_BG;
    this.ctx.fill();
    // 内阴影
    this.ctx.save();
    this.ctx.beginPath();
    this.ctx.arc(cx, cy, this.radius * 0.95, 0, 2 * Math.PI);
    this.ctx.fillStyle = '#0D1526';
    this.ctx.globalAlpha = 0.3;
    this.ctx.fill();
    this.ctx.globalAlpha = 1.0;
    this.ctx.restore();
    }

    // Layer 2: 三色分区弧线
    private drawColorZones(cx: number, cy: number) {
    const totalAngleRange = this.getEffectiveAngleRange();
    const startRad = this.degToRad(Constants.CAR_START_ANGLE);
    const greenEndFrac = Constants.CAR_GREEN_ZONE_END / Constants.CAR_SPEED_MAX;
    const yellowEndFrac = Constants.CAR_YELLOW_ZONE_END / Constants.CAR_SPEED_MAX;
    this.ctx.lineWidth = this.progressRingWidth;
    this.ctx.lineCap = 'butt';
    this.ctx.globalAlpha = 0.25;
    // 绿色段
    const greenEndRad = this.degToRad(Constants.CAR_START_ANGLE + totalAngleRange * greenEndFrac);
    this.ctx.beginPath();
    this.ctx.arc(cx, cy, this.radius, startRad, greenEndRad);
    this.ctx.strokeStyle = Constants.CAR_COLOR_GREEN;
    this.ctx.stroke();
    // 黄色段
    const yellowEndRad = this.degToRad(Constants.CAR_START_ANGLE + totalAngleRange * yellowEndFrac);
    this.ctx.beginPath();
    this.ctx.arc(cx, cy, this.radius, greenEndRad, yellowEndRad);
    this.ctx.strokeStyle = Constants.CAR_COLOR_YELLOW;
    this.ctx.stroke();
    // 红色段
    const redEndRad = this.degToRad(Constants.CAR_START_ANGLE + totalAngleRange);
    this.ctx.beginPath();
    this.ctx.arc(cx, cy, this.radius, yellowEndRad, redEndRad);
    this.ctx.strokeStyle = Constants.CAR_COLOR_RED;
    this.ctx.stroke();
    this.ctx.globalAlpha = 1.0;
    }

    // Layer 3-4: 刻度线和数字(代码见前文详解)
    // Layer 5: 进度弧(带发光效果)
    // Layer 6: 菱形指针(带发光阴影)
    // Layer 7: 数字速度显示

    // 工具方法
    private getEffectiveAngleRange(): number {
    let effectiveEnd = Constants.CAR_END_ANGLE;
    if (effectiveEnd < Constants.CAR_START_ANGLE) effectiveEnd += 360;
    return effectiveEnd Constants.CAR_START_ANGLE;
    }

    private degToRad(degrees: number): number {
    return degrees * Math.PI / 180;
    }

    // 生命周期
    aboutToAppear(): void {
    this.speedAnimator = this.getUIContext().createAnimator(this.animatorOptions);
    }

    aboutToDisappear(): void {
    this.speedAnimator?.finish();
    this.speedAnimator = undefined;
    }

    build() {
    Column() {
    Canvas(this.ctx)
    .width('100%')
    .height('100%')
    .onReady(() => {
    const w = this.getUIContext().getComponentUtils()
    .getRectangleById('car_canvas').size.width;
    this.canvasSize = this.getUIContext().px2vp(w);
    this.drawDashboard();
    })
    .id('car_canvas');
    }
    }
    }

    8.2 CarSpeedTest.ets(页面)

    import { Constants } from '../common/Constants';
    import { CarDashboard } from '../components/CarDashboard';

    @Entry
    @Component
    struct CarSpeedTest {
    @State currentSpeed: number = 0;
    @State isAccelerating: boolean = false;
    @State isBraking: boolean = false;
    @State maxSpeed: number = 0;
    @StorageProp('topRectHeight') topRectHeight: number = 0;
    @StorageProp('bottomRectHeight') bottomRectHeight: number = 0;

    private accelerateTimer: number = 1;
    private brakeTimer: number = 1;

    private getZoneText(): Resource {
    if (this.currentSpeed <= Constants.CAR_GREEN_ZONE_END) return $r('app.string.car_zone_safe');
    if (this.currentSpeed <= Constants.CAR_YELLOW_ZONE_END) return $r('app.string.car_zone_moderate');
    return $r('app.string.car_zone_danger');
    }

    private getZoneColor(): string {
    if (this.currentSpeed <= Constants.CAR_GREEN_ZONE_END) return Constants.CAR_COLOR_GREEN;
    if (this.currentSpeed <= Constants.CAR_YELLOW_ZONE_END) return Constants.CAR_COLOR_YELLOW;
    return Constants.CAR_COLOR_RED;
    }

    private startAccelerate() {
    this.isAccelerating = true;
    this.accelerateTimer = setInterval(() => {
    if (this.currentSpeed < Constants.CAR_SPEED_MAX) {
    this.currentSpeed = Math.min(this.currentSpeed + Constants.CAR_ACCELERATE_RATE, Constants.CAR_SPEED_MAX);
    if (this.currentSpeed > this.maxSpeed) this.maxSpeed = this.currentSpeed;
    } else { this.stopAccelerate(); }
    }, Constants.CAR_TIMER_INTERVAL);
    }

    private stopAccelerate() {
    this.isAccelerating = false;
    if (this.accelerateTimer !== 1) { clearInterval(this.accelerateTimer); this.accelerateTimer = 1; }
    }

    private startBrake() {
    this.isBraking = true;
    this.brakeTimer = setInterval(() => {
    if (this.currentSpeed > Constants.CAR_SPEED_MIN) {
    this.currentSpeed = Math.max(this.currentSpeed Constants.CAR_BRAKE_RATE, Constants.CAR_SPEED_MIN);
    } else { this.stopBrake(); }
    }, Constants.CAR_TIMER_INTERVAL);
    }

    private stopBrake() {
    this.isBraking = false;
    if (this.brakeTimer !== 1) { clearInterval(this.brakeTimer); this.brakeTimer = 1; }
    }

    aboutToDisappear(): void {
    this.stopAccelerate();
    this.stopBrake();
    }

    build() {
    Column() {
    // 标题栏 + 仪表盘 + 区间提示 + 按钮区域
    // (完整布局代码见源文件)
    }
    .padding({ top: this.getUIContext().px2vp(this.topRectHeight) })
    .backgroundColor(Constants.CAR_COLOR_BG)
    .width('100%')
    .height('100%');
    }
    }

    8.3 Constants.ets(新增部分)

    // ========== 汽车仪表盘常量 ==========
    static readonly CAR_SPEED_MIN: number = 0;
    static readonly CAR_SPEED_MAX: number = 240;
    static readonly CAR_START_ANGLE: number = 135;
    static readonly CAR_END_ANGLE: number = 45;
    static readonly CAR_GREEN_ZONE_END: number = 80;
    static readonly CAR_YELLOW_ZONE_END: number = 160;
    static readonly CAR_COLOR_GREEN: string = '#00E676';
    static readonly CAR_COLOR_YELLOW: string = '#FFB300';
    static readonly CAR_COLOR_RED: string = '#FF1744';
    static readonly CAR_COLOR_BG: string = '#1A1A2E';
    static readonly CAR_COLOR_DIAL_BG: string = '#16213E';
    static readonly CAR_COLOR_NEEDLE: string = '#FF1744';
    static readonly CAR_COLOR_NEEDLE_GLOW: string = '#FF5252';
    static readonly CAR_COLOR_TEXT_PRIMARY: string = '#FFFFFF';
    static readonly CAR_COLOR_TEXT_SECONDARY: string = '#B0BEC5';
    static readonly CAR_COLOR_SCALE: string = '#78909C';
    static readonly CAR_COLOR_SCALE_ACTIVE: string = '#FFFFFF';
    static readonly CAR_COLOR_BORDER: string = '#2A3A5C';
    static readonly CAR_SPEED_FONT: string = '700 56vp sans-serif';
    static readonly CAR_UNIT_FONT: string = '400 18vp sans-serif';
    static readonly CAR_SCALE_FONT: string = '500 13vp sans-serif';
    static readonly CAR_ANIMATION_DURATION: number = 200;
    static readonly CAR_ACCELERATE_RATE: number = 3;
    static readonly CAR_BRAKE_RATE: number = 5;
    static readonly CAR_TIMER_INTERVAL: number = 50;


    九、踩坑记录

    🕳️ 坑1:@ComponentV2 不能与 @Reusable 共用

    现象:给 @ComponentV2 组件添加 @Reusable 装饰器后编译报错。

    原因:@Reusable 仅支持 @Component(V1组件),V2组件不支持组件池复用。

    解决方案:V2组件使用 @ObservedV2 + @Trace 实现细粒度响应式,减少不必要的重渲染。

    // ❌ 错误:@Reusable 不兼容 @ComponentV2
    @Reusable
    @ComponentV2
    struct MyComponent { }

    // ✅ 正确:V2组件使用 @ObservedV2 + @Trace
    @ObservedV2
    class MyModel {
    @Trace value: number = 0;
    }

    🕳️ 坑2:ArkTS 泛型推断限制

    现象:使用 Array.from()、.map() 等泛型方法时编译报错 arkts-no-inferred-generic-params。

    解决方案:显式声明泛型参数,或改用 for 循环。

    // ❌ 错误:隐式泛型推断
    let arr = Array.from({ length: 5 }, (_, i) => i);

    // ✅ 正确:显式泛型或用 for 循环
    let arr: number[] = [];
    for (let i = 0; i < 5; i++) arr.push(i);

    🕳️ 坑3:Canvas onReady 时机

    现象:在 build() 中直接调用绘制方法,Canvas 显示空白。

    原因:Canvas 必须在 onReady() 回调触发后才能获取尺寸和进行绑制操作。

    解决方案:所有绘制逻辑放在 onReady 回调中,或通过尺寸变化监听触发。

    Canvas(this.ctx)
    .onReady(() => {
    // 这里才能安全获取尺寸并绘制
    this.canvasSize = this.getUIContext().px2vp(canvasWidth);
    this.drawDashboard();
    })

    🕳️ 坑4:Animator 生命周期管理

    现象:页面退出后仍然收到 onFrame 回调,导致空指针异常。

    解决方案:在 aboutToDisappear() 中调用 finish() 并置空引用。

    aboutToDisappear(): void {
    if (this.speedAnimator) {
    this.speedAnimator.finish();
    this.speedAnimator = undefined;
    }
    }

    🕳️ 坑5:setInterval 定时器泄漏

    现象:快速切换页面后,后台定时器仍在执行,修改已销毁组件的状态。

    解决方案:在 aboutToDisappear() 中清除所有定时器,并在 onTouch 的 Cancel 事件中也做清除处理。

    aboutToDisappear(): void {
    this.stopAccelerate(); // clearInterval
    this.stopBrake(); // clearInterval
    }

    .onTouch((event: TouchEvent) => {
    if (event.type === TouchType.Down) {
    this.startAccelerate();
    } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
    this.stopAccelerate(); // Cancel 也要清除!
    }
    })

    🕳️ 坑6:V1 与 V2 装饰器不能混用

    现象:在 @ComponentV2 中使用 @State、@Prop、@Watch 等 V1 装饰器,编译报错。

    解决方案:V2 组件必须使用 V2 装饰器体系:

    V1V2
    @State @Local
    @Prop @Param
    @Watch @Monitor

    十、总结与扩展

    核心收获

  • V2 状态管理的优势:@Monitor + @Computed 让数据流更清晰,细粒度更新减少不必要的重绘
  • Canvas 分层绘制:7层独立绘制逻辑,每层职责清晰,易于维护和扩展
  • Animator 动画:配合 @Monitor 实现声明式动画触发,代码更优雅
  • onTouch 交互:长按持续操作 + 松手停止的模式在很多场景下都适用
  • 可扩展方向

    方向实现思路
    陀螺仪联动 监听设备传感器,倾斜手机时指针偏移
    多表盘联动 转速表 + 速度表 + 油量表,共享状态
    驾驶模式切换 运动模式(红色主题)/ 经济模式(绿色主题)
    音效反馈 加速时播放引擎声,刹车时播放刹车声
    速度历史记录 绘制速度-时间曲线图

    参考文档

    • Canvas 画布组件
    • @ohos.animator 动画
    • 测速仪表盘自定义案例
    • ArkTS 状态管理 V2

    粒度更新减少不必要的重绘 2. Canvas 分层绘制:7层独立绘制逻辑,每层职责清晰,易于维护和扩展 3. Animator 动画:配合 @Monitor 实现声明式动画触发,代码更优雅 4. onTouch 交互:长按持续操作 + 松手停止的模式在很多场景下都适用

    可扩展方向

    方向实现思路
    陀螺仪联动 监听设备传感器,倾斜手机时指针偏移
    多表盘联动 转速表 + 速度表 + 油量表,共享状态
    驾驶模式切换 运动模式(红色主题)/ 经济模式(绿色主题)
    音效反馈 加速时播放引擎声,刹车时播放刹车声
    速度历史记录 绘制速度-时间曲线图

    参考文档

    • Canvas 画布组件
    • @ohos.animator 动画
    • 测速仪表盘自定义案例
    • ArkTS 状态管理 V2

    赞(0)
    未经允许不得转载:171主机测评 » 测速仪表盘案例新特性接入
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址