纯ArkUI实现7层拟物3D环形进度图:零依赖的视觉革命

本文是「食刻 (ShiKe)」技术系列第 2 篇,深入解析 Neumorphism 2.5D 环形进度图 的纯 ArkUI 实现方案 ------ 7 层组件叠加,零依赖,零 GPU 开销。

一、先看效果

食刻首页的核心视觉元素是一个 环形进度图(Ring Progress)

  • 显示今日热量摄入进度
  • 实时更新(添加食物后自动刷新)
  • 超标时自动变红
  • 看起来像 3D 的 ------ 有厚度、有光影、有材质感

这不是 Canvas 绘制的,不是图片,不是 3D 引擎渲染的。

这是用 7 个 ArkUI 原生组件叠加出来的。


二、为什么不用现成方案?

方案对比

方案 立体感 性能 依赖 可定制性
系统 Progress(Ring) ❌ 平面 ✅ 最优
Canvas 2D 手绘 ⚠️ 有限 ⚠️ 中等
ArkGraphics 3D ✅ 真 3D ❌ 重 引擎包
Lottie 动画 ✅ 好 ⚠️ 中等 json 文件
Neumorphism 7层叠加 ✅ 拟物 2.5D ✅ 零 GPU 开销 极高

我们的选择:第 5 种 ------ 纯 ArkUI 声明式组件叠加

理由:

  1. 鸿蒙 App 包体积敏感,不想引入任何第三方库
  2. 进度环需要实时响应状态变化(@State 驱动),Canvas 重绘逻辑复杂
  3. ArkGraphics 3D 引擎对于"一个圆环"来说杀鸡用牛刀
  4. 我们要的是拟物质感,不是真 3D

三、7 层结构拆解

完整层级清单

复制代码
Stack (130×130, alignContent: Center)
 │
 ├── Layer 1: Circle strokeWidth(14) stroke(#C8D5CE)
 │           .offset(y: 4)          ← 底部厚度远层(最深的阴影)
 │
 ├── Layer 2: Circle strokeWidth(14) stroke(#B8C7BE)
 │           .offset(y: 2)          ← 底部厚度近层
 │
 ├── Layer 3: Circle strokeWidth(14) stroke(#E8F0EC)
 │                                  ← 轨道凹陷底色(浅灰绿)
 │
 ├── Layer 4: Progress Ring value(ratio)
 │           strokeWidth(14)        ← 绿色填充弧(主进度)
 │           .color(#52B788)
 │           .shadow(r=8, color=rgba(64,82,184,136))  ← 绿色投影辉光
 │
 ├── Layer 5: Progress Ring value(35%)
 │           strokeWidth(2.5)       ← 顶部白色高光弧
 │           .color(rgba(255,255,255,0.35))
 │
 ├── Layer 6: Circle strokeWidth(1.5)
 │           stroke(rgba(150,170,160,0.3))  ← 内圈暗面
 │
 └── Layer 7: Column center overlay
             Text: "还可摄入 1723 kcal"   ← 中心文字

每一层的作用

🎭 厚度模拟(Layer 1-2)
复制代码
Layer 1: offset(y:4) + #C8D5CE(深)
Layer 2: offset(y:2) + #B8C7BE(中)

向下偏移 + 深于轨道的颜色 = 圆柱管壁截面的视觉错觉

就像你从侧面看一根管子,能看到它的厚度。

💡 光源模拟(Layer 5-6)
复制代码
Layer 5: 白色 35% 弧(顶部高光)
Layer 6: 暗色内圈(底部阴影)

顶部亮 + 底部暗 = 光线从上方照射的直觉

这是 Neumorphism 的核心技巧。


四、关键代码实现

核心组件代码

typescript 复制代码
// HomePage.ets --- Neumorphism Ring 核心实现
@Builder
NeumoRing(totalKcal: number, dailyLimit: number) {
  Stack({ alignContent: Alignment.Center }) {
    // Layer 1: 底部厚度远层
    Circle()
      .width(130)
      .height(130)
      .fill(Color.Transparent)
      .strokeWidth(14)
      .stroke('#C8D5CE')
      .offset({ y: 4 })

    // Layer 2: 底部厚度近层
    Circle()
      .width(130)
      .height(130)
      .fill(Color.Transparent)
      .strokeWidth(14)
      .stroke('#B8C7BE')
      .offset({ y: 2 })

    // Layer 3: 轨道底色
    Circle()
      .width(130)
      .height(130)
      .fill(Color.Transparent)
      .strokeWidth(14)
      .stroke('#E8F0EC')

    // Layer 4: 主填充弧
    Progress({ 
      value: Math.min(100, (totalKcal / dailyLimit) * 100), 
      type: ProgressType.Ring 
    })
      .width(130)
      .height(130)
      .strokeWidth(14)
      .color(totalKcal > dailyLimit ? '#E07B5A' : '#52B788')
      .shadow({ radius: 8, color: 'rgba(82,183,136,0.5)' })
      .enableScan(false)

    // Layer 5: 顶部高光弧
    Progress({ value: 35, type: ProgressType.Ring })
      .width(130)
      .height(130)
      .strokeWidth(2.5)
      .color('rgba(255,255,255,0.35)')
      .enableScan(false)

    // Layer 6: 内圈暗面
    Circle()
      .width(130)
      .height(130)
      .fill(Color.Transparent)
      .strokeWidth(1.5)
      .stroke('rgba(150,170,160,0.3)')

    // Layer 7: 中心文字
    Column() {
      Text(`${Math.max(0, dailyLimit - totalKcal)}`)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Text('还可摄入 kcal')
        .fontSize(10)
        .fontColor('#888888')
        .margin({ top: 2 })
    }
  }
  .width(130)
  .height(130)
}

五、踩过的坑(血泪经验)

坑 1:Progress Ring 的"幽灵线"

现象 :使用 Progress(Ring) 作为底层轨道时,即使 value=0,也会显示一条极细的默认底轨线

原因Progress 组件自带默认底轨样式,无法完全透明化。

解决 :所有不需要进度的圆环层全部改用 Circle().stroke() 替代 Progress(Ring)Circle 是纯粹的描边圆,没有任何默认样式。

✅ 规则:静态装饰层用 Circle,动态进度层用 Progress

坑 2:抗锯齿黑边

现象 :圆环边缘出现黑色锯齿边,在浅色背景上特别明显。

原因.shadow() 使用纯黑半透明 rgba(0,0,0,x) 时,抗锯齿算法会产生黑边。

解决 :所有阴影颜色改用绿色系 rgba

typescript 复制代码
// ❌ 错误:黑色阴影 → 黑边
.shadow({ radius: 8, color: 'rgba(0,0,0,0.15)' })

// ✅ 正确:绿色系阴影 → 融合自然
.shadow({ radius: 8, color: 'rgba(82,183,136,0.5)' })

坑 3:超标变色

需求 :当 totalKcal > dailyLimit 时,填充色要从绿色变为红色/珊瑚色。

实现 :三元表达式直接控制 .color()

typescript 复制代码
.color(totalKcal > dailyLimit ? '#E07B5A' : '#52B788')
//  #E07B5A = 珊瑚色(警示但不刺眼)
//  #52B788 = 草绿色(健康自然)

同时阴影颜色也要跟随变化:

typescript 复制代码
.shadow({ 
  radius: 8, 
  color: totalKcal > dailyLimit 
    ? 'rgba(224,123,90,0.5)'   // 珊瑚色投影
    : 'rgba(82,183,136,0.5)'    // 绿色投影
})

六、视觉效果对比

维度 传统 Progress Ring 食刻 Neumorphism 环
立体感 平板 2D 7 层拟物 2.5D
材质感 管壁厚度 + 反光 + 阴影
状态反馈 单一颜色 0-100% 绿渐变 + 超标珊瑚色
性能 1 组件 7 组件(零 GPU 开销)
依赖 系统组件 纯 ArkUI,零外部依赖

七、可复用性

这个 7 层环形进度图的方案不限于饮食场景,可以复用于:

  • ✅ 健身 App:训练完成度
  • ✅ 财务 App:月度预算消耗
  • ✅ 学习 App:课程进度
  • ✅ 任务管理:项目完成率
  • ✅ 储水/电量类展示
  • ✅ 任何 0-100% 的环形指标
  • 只需替换颜色 Token 和中心文字

八、总结与下篇预告

核心要点回顾

  • 7 层 = 2 层厚度 + 1 层底轨 + 1 层进度 + 1 层高光 + 1 层暗面 + 1 层文字
  • 静态层用 Circle,动态层用 Progress,避免幽灵线
  • 阴影用同色系 rgba,避免抗锯齿黑边
  • .offset({y: 2,4}) 模拟厚度,.color(rgba白光) 模拟光源
  • 整个方案零依赖、零 GPU 开销、纯声明式

下一篇:《鸿蒙 Widget 开发实战:3 张卡片实现桌面-App 全链路同步》

我们将深入 Form Kit 的跨进程通信机制,揭秘 Widget 如何与主 App 实时同步数据,以及如何实现"点击桌面卡片一键直达 AI 记录面板"的丝滑体验。


📌 项目仓库:https://atomgit.com/VON-/cxs-demo1

相关推荐
mzhan0171 小时前
Linux: compare的直观性
java·linux·服务器
爱喝水的鱼丶1 小时前
SAP-ABAP:SAP 与 ABAP 关联逻辑与入门路径:业务×开发的协作指南
服务器·前端·数据库·学习·sap·abap
原来是猿1 小时前
TCP Server 业务扩展实战:从 Echo 到远程命令执行与词典翻译
linux·运维·服务器
原来是猿2 小时前
TCP Echo Server 深度解析:从单进程到线程池的演进之路(上)
服务器·网络·tcp/ip
躺不平的理查德2 小时前
Shell逻辑判断备忘录
运维·服务器·git
skywalk81632 小时前
Trae生成的中文编程语言关键字(如“定“、“函“、“印“等)需要和标识符之间用 空格 隔开,以确保正确识别
服务器·开发语言·编程
焦糖玛奇朵婷3 小时前
健身房预约小程序开发、设计
java·大数据·服务器·前端·小程序
阿斯加德D3 小时前
《霍格沃茨之遗》风灵月影修改器下载(已汉化)2026最新版
人工智能·测试工具·游戏·3d·游戏程序
Liangwei Lin3 小时前
LeetCode 76. 最小覆盖子串
运维·服务器