在“轻规划”的人生管理模型中,用户通过 AI 导师“阿轻”五感引导写下的年度愿景信,最终要平铺落盘到“曼陀罗九宫格”愿景海报。九宫格是 3x3 矩阵,包含 8 大核心象限:健康身体、工作事业、财务理财、人际社会、家庭生活、学习成长、体验突破、心灵修养;中心坐标 [1,1] 留给年度主标题或核心旗帜。本文围绕这一场景,拆解纯 ArkTS 自研 High-Fidelity Markdown AST 合并渲染组件、ArkUI 自适应 Grid 布局,以及中心金箔卡片的微光动效与性能调优。
一、背景与技术挑战
HarmonyOS NEXT 目前尚未提供成熟的富文本 Markdown 抽象语法树(AST)原生解析组件。如果直接用 Web 组件承载 HTML,会带来首帧白屏、内存占用高等性能开销,也难以实现原生级别的精细排版和自适应滚动。同时,九宫格需要在高维度单元格中动态合并表格结构,并为年度主题位渲染带微光扫过效果的金箔卡片。因此,项目采用纯 ArkTS 自研 High-Fidelity Markdown AST 渲染组件,并用 ArkUI 声明式布局完成九宫格渲染。
二、架构与数据流
整体遵循单向数据流:
1. 数据解析层:MarkdownTableMerger 负责词法扫描与合法性校验,防范 ReDoS,将线性 Markdown 文本转换为含元数据的 AST 树。
2. 数据映射层:基于坐标避空映射算法,把 8 大维度元数据映射到 3x3 矩阵,并规避 [1,1] 中心位置。
3. 视图表现层:NineGridBalanceMatrix 作为容器管理自适应 Grid 排版,GoldLusterCard 绘制中心金箔微光动效。
数据流向可以概括为:AI 生成的 Markdown 文本 → MarkdownTableMerger 合并引擎 → Markdown AST Token 树 → 坐标避空算法映射 → MatrixCell 数据矩阵 → @State 状态绑定 NineGridBalanceMatrix 组件;年度主题通过 @StorageProp 进入 GoldLusterCard,再通过 translate 动效参与图层混合。
三、核心数据结构
为了让数据在类型安全环境中传递,先定义九宫格单元格结构:- export interface MatrixCell {
- x: number;
- y: number;
- title: string;
- actions: string[];
- color: string;
- }
复制代码
四、中心避空映射算法
标准九宫格有 9 个单元格,而人生维度数据为 8 个,中心坐标 [1,1] 必须强制留白,用于放置年度主题卡片。映射时不能简单用 quadrantIndex 换算行列,否则第 5 个数据会落到中心。项目设计“中心避空映射算法”:当 quadrantIndex 大于等于 4 时,物理索引加 1,把中心位置腾出来,让数据源按顺序环绕中心。核心换算如下:- const actualIndex = quadrantIndex >= 4 ? quadrantIndex + 1 : quadrantIndex;
- const targetRow = Math.floor(actualIndex / 3);
- const targetCol = actualIndex % 3;
复制代码 解析时还会过滤表头、表头分割线,并把“具体微行动”按中文分号、英文分号、中文逗号、英文逗号切分,最终形成 title 为“类别: 目标”的 MatrixCell。
五、ArkUI 九宫格自适应渲染
在 ArkUI 中采用声明式 Grid 容器结合响应式设计,避免多层嵌套 Row/Column,降低组件树深度,优化绘制性能。columnsTemplate 和 rowsTemplate 使用“1fr 1fr 1fr”均分三列三行,配合 columnsGap、rowsGap 和 aspectRatio(1),可适配手机、折叠屏与平板等鸿蒙终端。核心组件结构如下:- @Component
- export struct NineGridBalanceMatrix {
- @State cells: MatrixCell[] = [];
- @StorageProp('annualTheme') annualTheme: string = '觉醒之年';
- build() {
- Grid() {
- ForEach(this.cells, (cell: MatrixCell) => {
- GridItem() {
- Column() {
- Text(cell.title)
- .fontSize(14)
- .fontWeight(FontWeight.Bold)
- .fontColor('#333333')
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- List() {
- ForEach(cell.actions, (action: string) => {
- ListItem() {
- Text(`• ${action}`)
- .fontSize(11)
- .fontColor('#666666')
- .lineHeight(16)
- }
- })
- }
- .margin({ top: 6 })
- .width('100%')
- .edgeEffect(EdgeEffect.None)
- }
- .padding(10)
- .backgroundColor(cell.color)
- .borderRadius(12)
- .height('100%')
- .alignItems(HorizontalAlign.Start)
- .justifyContent(FlexAlign.Start)
- }
- .rowStart(cell.x)
- .columnStart(cell.y)
- }, (item: MatrixCell) => `${item.x}_${item.y}_${item.title}`)
- GridItem() {
- GoldLusterCard({ title: this.annualTheme })
- }
- .rowStart(1)
- .columnStart(1)
- }
- .columnsTemplate('1fr 1fr 1fr')
- .rowsTemplate('1fr 1fr 1fr')
- .columnsGap(8)
- .rowsGap(8)
- .width('100%')
- .aspectRatio(1)
- }
- }
复制代码 这里 ForEach 的 key 使用 `${item.x}_${item.y}_${item.title}`,便于优化重绘。中心 [1,1] 使用独立的 GoldLusterCard,而不是普通数据单元格。
六、[1,1] 年度主题位金箔微光卡片
中心年度旗帜位需要足够醒目。实现依赖多图层混合:
背景金箔层用 LinearGradient 填充,模拟金属质感折射;
微光动画层绘制倾斜且带高斯模糊的高亮白色条带,通过修改 translate 的 x 属性往复位移;
内容阻隔层放置高反差深棕色文字,整体容器设置 .clip(true),裁剪溢出边界的微光带。
动效采用 animateTo,duration 为 3500ms,tempo 为 1.0,curve 为 Curve.EaseInOut,iterations 为 -1,playMode 为 PlayMode.Normal。初始 shimmerOffsetX 为 -200,动画目标为 200。核心代码如下:- @Component
- export struct GoldLusterCard {
- @Prop title: string = '';
- @State private shimmerOffsetX: number = -200;
- aboutToAppear() {
- this.startShimmerAnimation();
- }
- private startShimmerAnimation() {
- this.shimmerOffsetX = -200;
- animateTo({
- duration: 3500,
- tempo: 1.0,
- curve: Curve.EaseInOut,
- iterations: -1,
- playMode: PlayMode.Normal,
- onFinish: () => {
- }
- }, () => {
- this.shimmerOffsetX = 200;
- });
- }
- build() {
- Stack() {
- Row()
- .width('100%')
- .height('100%')
- .borderRadius(12)
- .linearGradient({
- angle: 135,
- colors: [
- ['#FFE082', 0.0],
- ['#FFD54F', 0.25],
- ['#FFB300', 0.6],
- ['#FFA000', 1.0]
- ]
- })
- .shadow({
- radius: 12,
- color: 'rgba(218,165,32,0.4)',
- offsetY: 6,
- offsetX: 0
- })
- Row()
- .width(60)
- .height('160%')
- .backgroundColor('rgba(255, 255, 255, 0.35)')
- .rotate({ angle: 25 })
- .blur(20)
- .translate({ x: this.shimmerOffsetX })
- .hitTestBehavior(HitTestMode.None)
- Column() {
- Text('年度主题')
- .fontSize(10)
- .fontColor('#7D5100')
- .fontWeight(FontWeight.Medium)
- .opacity(0.8)
- .letterSpacing(1.5)
- Text(this.title)
- .fontSize(16)
- .fontWeight(FontWeight.Bold)
- .fontColor('#5D3A00')
- .margin({ top: 6 })
- .textAlign(TextAlign.Center)
- .maxLines(2)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- }
- .width('100%')
- .justifyContent(FlexAlign.Center)
- .alignItems(HorizontalAlign.Center)
- }
- .clip(true)
- .width('100%')
- .height('100%')
- .borderRadius(12)
- }
- }
复制代码
七、避坑与性能调优
无限循环动画的内存与能耗问题:使用 iterations: -1 后,即使页面销毁或返回上一页,渲染引擎仍可能在后台执行插值计算,导致 CPU 占用偏高。应在 aboutToDisappear 中重置或置空组件内动画状态,停止后台渲染消耗。
renderGroup 提升绘制帧率:Grid 单元格较多且中心卡片持续刷新时,系统可能反复重绘整个 Grid。在 GoldLusterCard 整体组件上添加 .renderGroup(true),让底座层和静态文字层离屏渲染并缓存为纹理,扫光动画运行时只需将白色光带与纹理在 GPU 中做图层混合,减少 CPU 排版损耗。
防范正则灾难性回溯:解析 Markdown 时,如果表格格式混乱或行特别长,正则可能严重回溯,导致主线程卡死。进入 parseToMatrix 前可预先限制单行长度,例如单行不超过 500 字符,规避长文本带来的正则假死。
八、总结
通过自研 Markdown 表格合并算法,项目打通了 AI 结构化数据源向声明式 Grid 九宫格的数据落盘;借助 ArkUI 的线性渐变、clip 裁剪、高斯模糊与显式属性动画,实现了中心金箔旗帜卡片。年度目标搭建完成后,下一步是打卡执行。原文预告下一篇文章将进入 AR Engine Kit 的面部追踪开发,主题为“微笑能量打卡与面部骨骼点高频推理避坑指南”。 |