鸿蒙原生应用 ArkUI 实战:手写柱状图 + 双层进度条 —— 订阅统计页的纯 ArkUI 数据可视化架构

App 54「订阅服务管理」统计页(ProfileTab),主题色 #9B51E0 梦幻紫,4 个 Tab 分别为首页(📺)、订阅(📋)、日历(📅)、统计(📊)。本篇基于 54-sub-mgr/entry/src/main/ets/pages/ProfileTab.ets(共 108 行)逐段拆解,附 4 张实机截图。

统计页整体布局

一、整体结构:三区"年度卡 + 柱状图 + 占比图"布局

统计页是"年度总览 + 月度趋势 + 分类结构"的分析布局——3 个 @Builder 块:

build() {
  Column() {
    this.Header()
    Scroll() {
      Column({ space: 14 }) {
        this.YearCard()
        this.BarChart()
        this.CatChart()
      }
      .width('100%')
      .padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad + this.safeBottom + 20 })
    }
    .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
  }
  .width('100%').height('100%').backgroundColor(C.bg)
}

3 块结构:YearCard(紫色渐变年度卡)→ BarChart(月度柱状图)→ CatChart(分类占比进度条)。统计页是"分析页"——先看年度总结(640 元、日均 1.75、同比 -8%),再看月度趋势(6 月最高 61),最后看分类结构(视频占 47%)。总览 → 趋势 → 结构三层递进,是系列中少见的"纯数据可视化页"(无用户卡/菜单)。

二、数据模型 + 动态最大值

interface CatStat {
  id: number;
  name: string;
  amount: number;
  percent: number;
  color: string;
}
private months: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
private monthVals: number[] = [46, 53, 53, 61, 53, 53];

private cats: CatStat[] = [
  { id: 1, name: '视频娱乐', amount: 300, percent: 47, color: '#E63946' },
  { id: 2, name: '软件工具', amount: 200, percent: 31, color: '#9B51E0' },
  { id: 3, name: '云服务', amount: 140, percent: 22, color: '#4A90D9' }
];

months 6 个月 + monthVals 对应支出(6 月最高 61);cats 3 分类(视频 47%/软件 31%/云服务 22%),各带品牌色。47+31+22 = 100%,300+200+140 = 640——分类结构完全对账。

private maxVal(): number {
  let m = 1;
  this.monthVals.forEach((v: number) => { if (v > m) { m = v; } });
  return m;
}

maxVal() 动态求最大值(61)——柱高相对最大值缩放,默认 m = 1 避免除零。即使后续月份数据变化,图表自动适配最高柱,无需改代码。

三、Header + YearCard:年度渐变卡

Header() {
  Row() { Text('支出统计').fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text) }
  .width('100%').height(this.safeTop + 56).padding({ top: this.safeTop, left: D.pad, right: D.pad })
  .backgroundColor(C.card).alignItems(VerticalAlign.Bottom)
}
YearCard() {
  Column({ space: 8 }) {
    Text('¥640').fontSize(36).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
    Text('本年度订阅总支出').fontSize(13).fontColor('#FFFFFF').opacity(0.9)
    Text('日均 ¥1.75 · 相比去年 -8%').fontSize(12).fontColor('#FFFFFF').opacity(0.85)
  }
  .width('100%').padding(24).borderRadius(D.rLg)
  .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
}

逐点拆解:

  1. 主金额 36 号:¥640 用 36 号 Bold 白字——比首页的 32 号更大(统计页是全屏分析,年度总额是唯一焦点)。
  2. 副文案两级透明度:"本年度订阅总支出"13 号 90% + "日均 ¥1.75 · 相比去年 -8%"12 号 85%——两级递减(0.9 → 0.85),日均(640÷365≈1.75)与同比是"平均 + 对比"两个分析维度。
  3. 数据自洽:640 = 首页"年度支出 ¥640" = 分类合计 300+200+140 = 640——三个数据源完全一致。

统计页顶部年度卡

四、BarChart:手写月度柱状图

BarChart() {
  Column({ space: 14 }) {
    Text('月度支出趋势').fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
    Row() {
      ForEach(this.months, (m: string, idx: number) => {
        Column({ space: 6 }) {
          Text('¥' + this.monthVals[idx]).fontSize(9).fontColor(C.textDim)
          Column()
            .width(24).height(this.monthVals[idx] * 110 / this.maxVal())
            .backgroundColor(C.primary).borderRadius({ topLeft: 4, topRight: 4 })
          Text(m).fontSize(10).fontColor(C.textSub)
        }.layoutWeight(1).justifyContent(FlexAlign.End)
      }, (m: string) => m)
    }.width('100%').height(160).alignItems(VerticalAlign.Bottom)
  }
  .width('100%').padding(16).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}

逐点拆解:

  1. 纯 ArkUI 柱状图:Column().width(24).height(val * 110 / maxVal())——柱高 = 数值 × 110 ÷ 最大值(6 月 61 元最高占满 110,其余按比例),不依赖第三方图表库,用基础组件手绘。
  2. 柱体细节:24 宽紫色柱 + borderRadius({ topLeft: 4, topRight: 4 })——只圆顶部两角(柱底贴基线不需要圆角)。
  3. 柱顶金额 + 底部月份:9 号淡灰金额(¥46)+ 10 号月份(3月)——每根柱上下两个标签,金额在柱顶、月份在柱底。
  4. 底部对齐:height(160).alignItems(VerticalAlign.Bottom) + 每列 justifyContent(FlexAlign.End)——所有柱底部对齐,形成标准柱状图形态。
  5. 趋势解读:3 月最低 46、6 月最高 61、其余稳定 53——8 月 53 ≈ 首页月付 53.30(四舍五入),趋势图与当月数据对账。

月度支出趋势柱状图

五、CatChart:双层 Stack 进度条

CatChart() {
  Column({ space: 14 }) {
    Text('分类占比').fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
    ForEach(this.cats, (c: CatStat) => {
      Column({ space: 6 }) {
        Row() {
          Text(c.name).fontSize(13).fontColor(C.text).layoutWeight(1)
          Text('¥' + c.amount + ' · ' + c.percent + '%').fontSize(12).fontColor(C.textDim)
        }.width('100%')
        Stack({ alignContent: Alignment.Start }) {
          Row().width('100%').height(8).backgroundColor(C.cardSoft).borderRadius(4)
          Row().width(c.percent + '%').height(8).backgroundColor(c.color).borderRadius(4)
        }.width('100%')
      }.width('100%')
    }, (c: CatStat) => c.id.toString())
  }
  .width('100%').padding(16).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}

逐点拆解:

  1. 双层 Stack 进度条:Stack({ alignContent: Alignment.Start }) 叠放灰底条(100%)+ 彩色条(percent%)——底条 width('100%') 淡灰圆角 8 高,彩色条 width(c.percent + '%') 按占比拉长(47% 红条最长)。
  2. 彩色分类条:视频红/软件紫/云服务蓝——颜色编码分类(与首页订阅品牌色同体系),视频娱乐 47% 红条最长(占近一半)。
  3. 名称 + 金额百分比:13 号分类名 + 12 号淡灰"¥300 · 47%"——金额与占比同行,信息紧凑。
  4. 百分比字符串:width(c.percent + '%') 是字符串拼接——ArkUI 宽度支持百分比字符串('47%'),若直接写 width(c.percent) 数字会被当 vp 值(47vp)而非 47%。

分类占比进度条

六、跨页数据自洽:统计分析的全局闭环

    Text('¥640').fontSize(36).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        Text('¥640').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')

统计页"¥640 本年度"与首页"年度支出 ¥640"完全一致——首页给结论、统计页给分解(月度趋势 + 分类结构)。统计页"8月 ¥53"≈ 首页"本月 53.30"(四舍五入),柱状图最后一个柱就是首页当月支出。分类 300+200+140 = 640 = 年度总额——分类结构完全对账。

七、系列横向对比:数据可视化页面的两种形态

统计页代表 App图表类型核心特色主题色
趋势 + 结构双图App 54 订阅柱状图 + 进度条三图组合(总览/趋势/结构)#9B51E0 梦幻紫
手写柱状图App 47 班费柱状图 + 进度条收支分析#00A862 翡翠绿
环形进度App 39 兴趣环形 95%单环比例#FF6B9D 粉

差异点:App 54 的"趋势 + 结构双图组合"——柱状图看"月度趋势"(时间维度:6 月最高)、进度条看"分类结构"(空间维度:视频占 47%),时间与结构两个分析维度一页给全,是系列中分析维度最完整的统计页。同时它是系列中极少数没有用户卡/菜单的纯数据页(其他 App 的第 4 Tab 多为个人中心)。

八、开发者视角:调试与验证技巧

  1. 柱高比例公式:height(val * 110 / maxVal())——固定系数 110 是"最高柱的像素高度",若图表高度 160 而最高柱 110,剩余 50 留给金额标签和月份标签;用"系数 = 图表高 - 标签预留"的公式推导。
  2. 百分比字符串:width(c.percent + '%') 拼接——ArkUI 宽度支持百分比字符串,但 percent 是数字(47),拼接 '47%' 才生效;直接写 width(47) 会被当 47vp。
  3. 进度条圆角:双层条都是 borderRadius(4)——高度 8 配圆角 4 是标准比例(4 = 8÷2 形成半圆头),圆角大于高度一半会出现怪异形状。
  4. 空数据防护:maxVal() 默认 m = 1(避免除零)——数据全为 0 时柱高 0、maxVal 返回 1,防止除以零崩溃;产品化应展示空状态。

九、演进方向

方向当前状态产品化演进
月度动态硬编码 6 个月从扣款记录动态聚合月度支出
分类动态硬编码 3 分类从订阅分类自动计算占比
预算对比仅同比 -8%设定年度预算 + 进度条 + 预测
环比箭头无柱状图叠加环比变化箭头
导出报表无导出图片/表格 + 年度订阅报告

十、一句话总结

统计页以"手写柱状图 + 双层 Stack 进度条"完成了订阅支出的全面分析。height(val * 110 / maxVal()) 纯 ArkUI 柱状图(不引图表库 + 底部对齐 + 顶部圆角)、Stack 双层进度条(灰底 100% + 彩色 percent% + width(c.percent + '%') 百分比字符串)、36 号年度总额 + 两级透明度副文案——108 行代码,一个"年度总览 → 月度趋势 → 分类结构"的纯数据可视化页,也是"手绘图表"型统计页的标准范式。

项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐