ArkUI UI重设计实践:从600行规范到代码落地
大多数技术文章讲的是“怎么实现一个功能”,但本文换个角度:如何把一份600行的设计规范文档,转化成可运行的ArkUI代码。项目“本地扫描仪”包含文件扫描、证件照、OCR三个模块。最初版本在UI上问题明显——证件照页面同时铺了8到10个交互元素,字号从11sp到22sp出现了8种,按钮主次不分。于是我们编写了完整的设计规范文档,逐页重构,并记录下这个过程:设计规范怎么写、代码怎么落地、踩了哪些坑。一、问题诊断:先量化再动手
重构前,我们先做了两件事:统计每个页面的交互元素数量,以及统计代码中所有fontSize取值。证件照页面最严重——背景色选择、尺寸选择、自定义开关、RGB输入框、导出按钮全部展开在同一屏内。字号上,同级元素(区块标题)同时出现了13、15、16三种取值,典型的“边做边加”导致的不一致。
二、建立字体系统:常量统一管理
解决字号混乱的第一步是建立统一的Typography Scale。我们定义了7级字体层级:H1 Display 24sp Bold(页面唯一大标题)、H2 Section 18sp SemiBold(功能区块标题)、H3 Card 16sp Medium(卡片内标题)、Body Large 15sp Regular(按钮文字/重要正文)、Body 14sp Regular(副标题/说明文字)、Caption 12sp Regular(辅助提示)、Tab Label 10sp Regular(底部Tab文字)。比原来的8种取值少一种,但覆盖所有场景。关键变更:区块标题统一为18sp(取代13/15/16混用),极小提示从11sp提升到12sp。
代码落地使用常量统一管理。创建AppTypography.ets文件,定义Typography对象:
export const Typography = {
H1: { size: 24, weight: FontWeight.Bold },
H2: { size: 18, weight: FontWeight.SemiBold },
H3: { size: 16, weight: FontWeight.Medium },
BodyLg: { size: 15, weight: FontWeight.Normal },
Body: { size: 14, weight: FontWeight.Normal },
Caption: { size: 12, weight: FontWeight.Normal },
Tab: { size: 10, weight: FontWeight.Normal }
};
使用时直接引用常量,不再手写数字:
Text('文件扫描')
.fontSize(Typography.H1.size)
.fontWeight(Typography.H1.weight)
好处是改字号只需改一处常量,坏处是初期迁移工作量大,每个页面的Text组件都要改。
三、证件照页面重构:渐进式披露
证件照页面是重构重点,8到10个交互元素压缩到3个。核心思路是渐进式披露:默认只展示最常用选项,高级选项折叠收起。阶段划分:阶段1(无图片时)只展示选图入口;阶段2(有图片后)展示预览+背景色+尺寸+导出;阶段3(自定义展开)用户主动展开高级选项。
背景色选择器从3个文字chip改为圆形色块选择器:
Row({ space: 12 }) {
ForEach(BgColors, (c: BgColorItem) => {
Column() {
Circle()
.width(44).height(44)
.fill(c.color)
.stroke(this.selectedBg === c.id ? '#007DFF' : 'transparent')
.strokeWidth(3)
.scale({ x: this.selectedBg === c.id ? 1.08 : 1.0 })
Text(c.label)
.fontSize(Typography.Caption.size)
.fontColor('#999')
}
})
}
尺寸选择器从4个平铺chip改为下拉选择器:
Column() {
Row() {
Text(`当前:${this.selectedSize.label}`)
.fontSize(Typography.Body.size)
Image($r('app.media.ic_arrow_down'))
.rotate({ angle: this.showSizeDropdown ? 180 : 0 })
}
if (this.showSizeDropdown) {
ForEach(SizeOptions, (s: SizeOption) => {
Text(`${s.label} ${s.width}×${s.height}mm`)
.onClick(() => {
this.selectedSize = s;
this.showSizeDropdown = false;
})
})
}
}
四、扫描结果页重构:按钮层级
扫描结果页原图/增强切换、导出PDF、导出图片、分享四个操作平铺,视觉权重相同。解决方案是分层+ActionSheet合并。模式切换采用Segmented Control样式:
Row() {
ForEach(['原图', '增强'], (mode: string) => {
Text(mode)
.fontSize(14)
.fontColor(this.currentMode === mode ? '#fff' : '#999')
.backgroundColor(this.currentMode === mode ? '#007DFF' : 'transparent')
.borderRadius(18)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.onClick(() => { this.currentMode = mode; })
})
}
导出按钮用主操作+ActionSheet:
Button('导出', { type: ButtonType.Capsule })
.height(52)
.width('100%')
.backgroundColor('#007DFF')
.onClick(() => {
ActionSheet.show({
items: [
{ title: '导出为 PDF' },
{ title: '导出高清图片' },
{ title: '分享给好友' }
]
})
})
主操作按钮用实心填充52vp高度,次要操作用描边样式44vp。三种导出方式收进一个弹窗,首页只保留1个导出按钮。
五、响应式布局:断点系统
我们定义了四个断点:SM ≤600vp(手机竖屏,底部Tab)、MD 600~840vp(手机横屏,底部Tab)、LG 840~1200vp(平板竖屏,左侧Tab)、XL ≥1200vp(平板横屏,左侧Tab)。关键组件在不同断点下布局差异:
if (this.breakpoint >= Breakpoint.LG) {
// 平板:左右分栏
Row() {
Column() { this.SwiperPreview() }.layoutWeight(3)
Column() { this.OperationPanel() }.layoutWeight(2)
}
} else {
// 手机:上下堆叠
Scroll() {
Column() {
this.SwiperPreview()
this.OperationPanel()
}
}
}
断点检测使用系统API:
import { display } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@StorageProp('breakpoint') breakpoint: string = 'sm';
aboutToAppear() {
const w = display.getDefaultDisplaySync().width;
this.updateBreakpoint(w);
display.on('displayChange', () => {
const nw = display.getDefaultDisplaySync().width;
this.updateBreakpoint(nw);
});
}
}
六、动画规范
全局动画参数:证件照选图成功后的进场动画是预览图淡入+上滑:
Column() {
Image(this.selectedImage)
.opacity(this.showPreview ? 1 : 0)
.translate({ y: this.showPreview ? 0 : 10 })
.animation({ duration: 300, curve: Curve.EaseOut })
}
Tab切换的弹性缩放:
Image(tab.icon)
.scale({
x: this.activeTab === tab.id ? 1.08 : 1.0,
y: this.activeTab === tab.id ? 1.08 : 1.0
})
.animation({
duration: 200,
curve: springMotion(0.5, { damping: 0.7 })
})
springMotion参数response 0.5秒,damping 0.7,产生轻微弹跳后迅速收敛的效果。
七、踩坑记录
坑1:fontSize常量的类型问题。ArkUI的fontSize接受number|string|Resource。如果常量定义为{size:24},类型推断为number,但某些组件(如Button)的fontSize需要string或Resource。修复:常量定义为number,使用时转string或定义为Resource:.fontSize(Typography.H1.size.toString())或H1: { size: $r('app.integer.font_h1') }。Resource方式更符合规范,但需要额外资源文件。
坑2:ForEach的keyGenerator。证件照背景色选择器用ForEach渲染色块,如果不提供keyGenerator,列表项身份识别不准确,切换选中态时动画会闪烁。必须提供keyGenerator:ForEach(BgColors, (c) => {...}, (c) => c.id)。
坑3:ActionSheet在不同设备上的表现。手机上ActionSheet从底部滑入,高度自适应;平板上ActionSheet宽度会撑满屏幕,因为平板默认使用Dialog样式。修复:平板端改用自定义Popup或DropdownMenu。
坑4:动画时长与帧率的关系。100ms微交互在60fps设备上约6帧,在120fps上约12帧,更平滑但可能感觉“太慢”。HarmonyOS的动画曲线在不同帧率下会自动插值,无需手动处理。
坑5:设计规范的执行一致性。最难不是写规范,而是保证每个开发者都遵守。代码审查时发现有人在新页面里又手写fontSize(16)而不是引用Typography.H3。修复:ESLint规则禁止直接使用数字字号:"harmonyos/no-hardcoded-font-size": "error"。
八、性能数据
代码行数反而减少了约15%,因为消除了大量重复的字号、颜色、间距硬编码。
九、核心思想
回顾整个过程,最有价值的三个决策:先量化问题再动手,不是“我觉得字号乱”而是“统计出8种取值”,数字比直觉更有说服力;渐进式披露解决信息过载,证件照页面从10个元素压缩到3个,不是删除功能而是折叠;常量化消除不一致,Typography常量+ESLint规则从源头杜绝字号混乱。核心差异:传统重构靠开发者的审美直觉,本方案靠600行设计文档+常量体系,设计文档是代码的上游输入,代码是设计文档的下游实现。
Re: ArkUI UI重设计实践:从600行规范到代码落地
很干货的一篇实践总结,从量化问题到建立设计规范,再到ArkUI代码落地的每一步都交代得很清楚。特别是“渐进式披露”的思路和ActionSheet合并导出操作,对减少页面信息密度很有启发。请问在重构过程中,对比重构前后用户反馈或操作效率有没有量化数据?比如证件照页面完成时间或点击次数变化?Re: ArkUI UI重设计实践:从600行规范到代码落地
楼主的分享很扎实,从问题量化到常量定义再到分层重构,每一步都有理有据。特别喜欢“渐进式披露”的思路——证件照页面从8~10个元素压缩到3个,这不仅仅是界面清爽,更是在帮用户做决策减负。另外字体常量的做法确实能根治“边做边加”导致的字号混乱,后期改主题或适配不同屏幕也会省很多事。想问下,在迁移600行规范到代码的过程中,团队内部有没有遇到阻力?比如设计师和开发之间对某些层级定义的理解偏差,或者旧页面批量修改时有没有自动化工具辅助?Re: ArkUI UI重设计实践:从600行规范到代码落地
这篇文章非常接地气,从实际痛点出发,用数据说话,把设计规范和代码落地的过程讲得很清晰。特别是“渐进式披露”和“按钮层级”的思路,对UI工程师很有参考价值。想请问一下,600行的设计规范文档除了字体系统,还覆盖了哪些维度(比如间距、色彩、动效)?在迁移到常量管理时,团队有没有遇到组件库与自定义样式冲突的情况?
页:
[1]