鸿蒙APP开发-带你走进节流战的Canvas图表

节流战的Canvas图表:消费趋势图与预算进度

如果你想控制消费、养成节约习惯,推荐去鸿蒙应用市场搜一下**「节流战」**,下载体验体验。设置月度预算、记录每笔消费、参与预算挑战,一套走下来对消费习惯会有更清晰的认识。体验完再回来看这篇文章,你会更清楚消费趋势图和预算进度背后是怎么实现的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。

很多人觉得"前端转鸿蒙"应该很容易------都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂

比如:

  • Canvas绘图 :Web的Canvas API和鸿蒙的CanvasRenderingContext2D几乎一模一样,但创建方式不同。
  • 动画效果 :Web用requestAnimationFrame,鸿蒙用animator模块。
  • 数据存储 :Web的localStorage到了ArkTS变成了@ohos.data.preferences

接下来这篇文章,我会用"节流战"的实际开发经历,带你看看鸿蒙的Canvas怎么画消费趋势图------从数据聚合到图表绘制。


这篇文章聊什么

节流战的Canvas图表功能,核心要解决两个问题:

  1. 消费趋势图:展示最近30天的消费走势
  2. 预算进度:展示本月预算的使用情况

第一步:消费数据结构

typescript 复制代码
interface Expense {
  id: string;
  amount: number;
  category: string;
  description: string;
  date: string;
  timestamp: number;
  isImpulse: boolean; // 是否冲动消费
}

interface Budget {
  monthlyLimit: number;
  categoryLimits: Record<string, number>;
}

// 8个消费分类
const EXPENSE_CATEGORIES = [
  { id: 'food', name: '餐饮', icon: '🍜', color: '#F59E0B' },
  { id: 'transport', name: '交通', icon: '🚌', color: '#3B82F6' },
  { id: 'shopping', name: '购物', icon: '🛍️', color: '#EC4899' },
  { id: 'entertainment', name: '娱乐', icon: '🎮', color: '#8B5CF6' },
  { id: 'living', name: '生活', icon: '🏠', color: '#10B981' },
  { id: 'health', name: '医疗', icon: '💊', color: '#EF4444' },
  { id: 'education', name: '教育', icon: '📚', color: '#14B8A6' },
  { id: 'other', name: '其他', icon: '📦', color: '#6B7280' }
];

第二步:30天消费趋势图

typescript 复制代码
@Entry
@Component
struct ExpenseTrendPage {
  @State expenses: Expense[] = []
  @State dailyTotals: number[] = []

  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

  async aboutToAppear() {
    await this.loadExpenses()
    this.calculateDailyTotals()
  }

  async loadExpenses() {
    const store = await preferences.getPreferences(getContext(), 'jieji_data');
    const stored = await store.get('expenses', '[]') as string;
    this.expenses = JSON.parse(stored);
  }

  calculateDailyTotals() {
    const today = new Date();
    this.dailyTotals = [];

    for (let i = 29; i >= 0; i--) {
      const date = new Date(today);
      date.setDate(date.getDate() - i);
      const dateStr = date.toISOString().slice(0, 10);

      const dayTotal = this.expenses
        .filter(e => e.date === dateStr)
        .reduce((sum, e) => sum + e.amount, 0);

      this.dailyTotals.push(dayTotal);
    }
  }

  build() {
    Column() {
      Text('30天消费趋势')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      Canvas(this.ctx)
        .width('100%')
        .height(220)
        .backgroundColor('#1F2937')
        .borderRadius(12)
        .onReady(() => {
          this.drawTrendChart()
        })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#111827')
  }

  private drawTrendChart() {
    const ctx = this.ctx;
    const width = 350;
    const height = 220;
    const padding = { top: 20, right: 20, bottom: 30, left: 50 };

    ctx.clearRect(0, 0, width, height);

    const maxVal = Math.max(...this.dailyTotals, 1);

    // 画柱状图
    const barWidth = (width - padding.left - padding.right) / 30 - 2;

    this.dailyTotals.forEach((total, index) => {
      const x = padding.left + (width - padding.left - padding.right) * (index / 30) + 1;
      const barHeight = (total / maxVal) * (height - padding.top - padding.bottom);
      const y = height - padding.bottom - barHeight;

      // 渐变色
      const gradient = ctx.createLinearGradient(x, y, x, height - padding.bottom);
      gradient.addColorStop(0, '#F59E0B');
      gradient.addColorStop(1, '#F59E0B33');

      ctx.fillStyle = gradient;
      ctx.fillRect(x, y, barWidth, barHeight);
    });

    // 画Y轴标签
    ctx.fillStyle = '#6B7280';
    ctx.font = '10px sans-serif';
    ctx.textAlign = 'right';
    for (let i = 0; i <= 4; i++) {
      const value = maxVal * (i / 4);
      const y = height - padding.bottom - (height - padding.top - padding.bottom) * (i / 4);
      ctx.fillText(`¥${Math.round(value)}`, padding.left - 5, y + 4);
    }
  }
}

第三步:预算进度环

typescript 复制代码
// 画预算进度环
private drawBudgetRing(budgetUsed: number, budgetTotal: number) {
  const ctx = this.ctx;
  const centerX = 100;
  const centerY = 100;
  const radius = 80;
  const lineWidth = 12;

  const progress = Math.min(budgetUsed / budgetTotal, 1);
  const startAngle = -Math.PI / 2;
  const endAngle = startAngle + progress * Math.PI * 2;

  // 背景环
  ctx.strokeStyle = '#374151';
  ctx.lineWidth = lineWidth;
  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
  ctx.stroke();

  // 进度环
  const color = progress > 0.9 ? '#EF4444' : progress > 0.7 ? '#F59E0B' : '#10B981';
  ctx.strokeStyle = color;
  ctx.lineWidth = lineWidth;
  ctx.lineCap = 'round';
  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, startAngle, endAngle);
  ctx.stroke();

  // 中心文字
  ctx.fillStyle = '#FFFFFF';
  ctx.font = 'bold 24px sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(`${Math.round(progress * 100)}%`, centerX, centerY);
}

总结

这篇文章围绕"节流战"的Canvas图表功能,讲解了两个核心主题:

  1. 消费趋势图:30天柱状图的数据聚合和绘制
  2. 预算进度环:圆弧进度条的绘制

Canvas绘图的核心技巧是坐标映射和渐变色运用。柱状图用渐变色可以让图表更有层次感。


如果你想控制消费,希望这篇文章能帮你理解节流战背后的图表实现。去鸿蒙应用市场下载体验一下吧,有问题欢迎交流。

相关推荐
陈_杨1 小时前
鸿蒙APP开发-带你走进光绘记的拍摄规划
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进光绘记的长曝光模拟
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进节拍器的声音怎么这么准
前端·javascript
搬砖的阿wei1 小时前
Pinia 与 Vuex 区别
前端·vue.js
KaMeidebaby1 小时前
卡梅德生物技术快报|原核表达系统工艺优化:包涵体重折叠 + 分子筛纯化实现功能 RBD 高效制备,附全参数配置
前端·人工智能·算法·数据挖掘·数据分析
最爱睡觉睡觉睡觉1 小时前
代碼案例:CSS 屬性對照
前端·app
VitoChang2 小时前
开发体验超赞的SolidJS2.0来了
前端
CoCo的编程之路2 小时前
2026全栈演进:使用前端开发助手进行项目重构的最佳工具
大数据·前端·人工智能·ai编程·comate
@PHARAOH3 小时前
WHAT - NextAuth 权限认证机制
前端·微服务·服务端