SEO 信息
- SEO 标题:动图魔方技术拆解 16:HarmonyOS ArkUI contentWidth 与 BottomNav 移动端自适应布局
- SEO 摘要 :基于 HarmonyOS NEXT / ArkTS 项目"动图魔方",拆解工具类 App 的移动端布局一致性问题:如何用
Index.ets统一页面主宽度、20vp 主边距、16vp 编辑区内边距、预览舞台和 90% 底部工作台,并用真实截图、命令和验收清单完成 ArkUI 视觉回归。 - 关键词:HarmonyOS, ArkUI, ArkTS, 动图魔方, 卡片宽度, 移动端适配, 视觉验收, Index.ets
- 文章封面 :
doc/csdn-series/covers/cover-16-arkui-card-width-mobile-qa.jpg - 投稿方向:普通技术拆解 / ArkUI 工具类 App 布局收口与移动端视觉一致性
- 项目环境 :HarmonyOS SDK
6.1.0(23)、ArkTS、DevEco Studio、GIFRubiksCube - 验证时间 :
2026-06-27 - 验证对象 :
entry/src/main/ets/products/main/Index.ets中首页、编辑页、作品页、发现页、我的页和底部导航的宽度收口逻辑
第 14 篇把单页工作台的路由和空状态拆开了,第 15 篇把深浅色主题收口成了一套统一 token。继续往前走,页面结构已经能跑,主题也能切,但把首页、编辑页、作品页和"我的"页连着截出来看,新的问题会马上出现:有的页面左右留白是 20vp,有的卡片只包了 16vp,有的预览区直接顶满屏幕,有的底部导航又单独缩了一圈。代码层面都"没错",视觉上却已经不在同一套产品语言里了。第 16 篇只解决这一类工程问题:把移动端页面的宽度、留白、卡片和底部工作台拆成可复核的 ArkUI 验收口径。
一、真实工程问题背景
"动图魔方"不是展示型页面,而是一个高频切换的本地创作工作台:
- 首页要承接功能入口、英雄卡片和最近作品。
- 编辑页要同时容纳预览区、操作卡片、导出设置和底部工作台。
- 作品页、发现页、"我的"页还要复用同一套卡片语言。
这类产品最容易出现的不是"某个按钮颜色不对",而是页面容器节奏不一致:
- 首页卡片边距是 20vp,编辑页工具区又是另一套 16vp,用户切页时会直觉感到页面忽然变窄。
- 预览区如果直接全宽贴边,下面卡片再做大圆角白底,就会像两套布局临时拼接。
- 底部导航如果宽度和上层内容毫无关系,截图时会显得"漂"在页面最下方。
- 同样都叫卡片,首页功能卡、作品卡、发现页模板卡和我的页信息卡如果没有共同的圆角、边框和横向收口,视觉上就会越来越散。
这类问题很难靠"最后 UI 调一调"补救,因为它本质上不是某个控件样式问题,而是布局规则没有先被定义出来;一旦页面数量增加,临时调出来的边距会互相覆盖,后续每次截图验收都要重新返工。
如果这个问题不提前收口,后面的功能迭代会越来越难控制。比如第 17 篇准备继续拆分享链路时,分享按钮、导出结果、作品卡片和底部导航都会同时出现在一个截图里;如果第 16 篇没有先把移动端横向节奏固定下来,后面每加一个入口都要重新判断"这个卡片到底应该跟谁对齐"。所以这篇文章不是做视觉润色,而是在给后续 GIF 导出、分享、作品管理和主题切换建立共同的页面边界。
二、目标与边界:先定义移动端布局验收口径
这次的目标不是重做视觉风格,而是把已有页面的布局规则收口成可复核的工程边界:
- 目标一:统一页面主宽度 。手机端保持
100%,宽屏时集中收口,避免多设备截图表现失控。 - 目标二:统一页面级留白。首页、作品页、发现页、我的页都围绕 20vp 主节奏组织内容。
- 目标三:保留编辑页的功能差异。预览舞台允许更沉浸,操作卡片仍然保持可读和可触控。
- 目标四:让底部工作台独立成层。它可以不像正文卡片一样宽,但必须服从整页视觉秩序。
对应的边界也要说清楚:
- 本文不重讲第 15 篇的深浅色 token,只复用它已经沉淀下来的颜色函数。
- 本文不讨论 GIF 编码、抽帧、导出性能,只处理页面结构层的问题。
- 本文不追求所有控件同宽,而是按"页面、舞台、卡片、底部工作台"四类对象分别定义宽度语义。
基于这个边界,我没有先去微调某一张卡片,而是先把"截图看起来一致"拆成四条可以复核的工程规则:
| 验收对象 | 规则 | 为什么这么定 |
|---|---|---|
| 页面主容器 | 手机端走 100%,宽屏收口到 720vp |
避免平板上内容无限拉伸,也避免手机端出现人为窄栏 |
| 普通内容区 | 统一使用 20vp 左右边距 |
首页、作品页、发现页、我的页切换时横向节奏一致 |
| 编辑操作区 | 预览舞台可满宽,下面工具卡片退回 16vp 内边距 |
预览需要沉浸感,操作区需要可读性和触控密度 |
| 底部工作台 | 独立 90% 宽度并居中悬浮 |
底部导航是操作台,不应该和正文卡片抢同一个宽度模型 |
这里有一个很容易被忽略的判断:移动端一致性不是所有东西都同宽,而是每一类对象都有稳定的宽度语义。预览舞台、功能卡片、底部导航、页面标题区本来就承担不同任务,强行把它们都套同一个宽度,反而会让页面显得僵硬。
我把这套口径和 ArkUI 的布局约束对应成下面几条实现原则:
- 宽度判断只放在顶层入口,不在每个页面里重复猜屏宽。
- 页面级留白由
Header()、页面主区和列表容器共同遵守。 - 编辑页允许预览舞台"破格",但操作卡片必须回到可读宽度。
- 底部导航只跟随
contentWidth()的上限,不跟随某一个页面的局部卡片。 - 每个验收项都必须能用源码定位、真实截图和发布前检查互相印证。
三、源码对象、验证环境与当前证据面
这篇文章对应的真实源码对象和证据文件如下:
entry/src/main/ets/products/main/Index.etsentry/src/main/ets/entryability/EntryAbility.etsentry/src/main/resources/base/profile/main_pages.jsondoc/screenshots_current/gifrubik_real_home.jpegdoc/screenshots_current/gifrubik_editor_uniform_cards_final.jpegdoc/screenshots_current/gifrubik_real_works.jpegdoc/screenshots_current/gifrubik_profile_expanded_light.jpegdoc/csdn-series/publish-record.json
这几个对象的职责并不相同:
Index.ets是主实现,里面同时包含屏宽测量、页面切换、卡片 Builder、编辑页工具卡片和底部导航。EntryAbility.ets是应用级入口,负责窗口和颜色模式的初始状态,决定页面是否能稳定进入同一套视觉环境。main_pages.json是路由入口证明,说明当前验收围绕同一个主页面展开,不是多个页面临时拼接。doc/screenshots_current/*.jpeg是视觉验收证据,用来对齐源码规则和真实移动端截图。
我对照的官方能力文档主要有两类:
- HarmonyOS ArkUI 多设备自适应布局最佳实践,核心是不要把页面写死成单一尺寸,而是让布局根据窗口和设备形态调整。
- ArkUI 基础布局能力说明,
Column/Row/Stack/Grid等容器负责表达宽度、间距和层级关系,业务控件不应该散落太多魔法数。
这也是本文只盯 Index.ets 的原因:这次问题不是 GIF 编码、PixelMap 处理或 TaskPool 导出,而是页面框架层能不能给所有功能一个稳定的移动端舞台。
定位这些实现的位置,我实际用了下面这条命令:
rg -n "contentWidth\(|screenWidthVp|BottomNav\(|Header\(|previewStageHeight\(|padding\(\{ left: 16, right: 16 \}\)|margin\(\{ left: 20, right: 20" entry/src/main/ets -g "*.ets"
这次命中出来的关键结果如下:
entry/src/main/ets/products/main/Index.ets:61: @State screenWidthVp: number = 360;
entry/src/main/ets/products/main/Index.ets:170: return this.screenWidthVp >= 700;
entry/src/main/ets/products/main/Index.ets:173: private contentWidth(): number | string {
entry/src/main/ets/products/main/Index.ets:669: Header(title: string, sub: string) {
entry/src/main/ets/products/main/Index.ets:920: }.width('100%').height(this.previewStageHeight())
entry/src/main/ets/products/main/Index.ets:1163: }.width('100%').padding({ left: 16, right: 16 }).margin({ top: 14, bottom: 160 })
entry/src/main/ets/products/main/Index.ets:1526: BottomNav() {
entry/src/main/ets/products/main/Index.ets:1578: }.width(this.contentWidth()).height('100%')
这组定位结果很重要,因为它把"视觉统一"从抽象描述收口成了顶层宽度、页面边距、编辑区边距和底部工作台四个可复核对象。读者如果要迁移到自己的 ArkUI 工具类 App,也可以先用同样的命令定位:页面主容器在哪里、卡片 Builder 在哪里、底部导航在哪里、截图证据对应哪一段实现。
四、先统一的是内容宽度,不是单个卡片
在 Index.ets 顶层状态里,屏宽测量和内容宽度先收口成一个统一入口:
@State screenWidthVp: number = 360;
private isWide(): boolean {
return this.screenWidthVp >= 700;
}
private contentWidth(): number | string {
return this.isWide() ? 720 : '100%';
}
最终 build() 再把这个宽度入口收口到整页:
build() {
Stack({ alignContent: Alignment.Bottom }) {
Column() {
if (this.page === 'editor') {
this.EditorPage()
} else if (this.page === 'works') {
this.WorksPage()
} else if (this.page === 'discover') {
this.DiscoverPage()
} else if (this.page === 'profile') {
this.ProfilePage()
} else {
this.HomePage()
}
}.width(this.contentWidth()).height('100%')
this.BottomNav()
}.width('100%').height('100%').backgroundColor(this.pageBg())
}
这一步解决的不是"某张卡片多宽",而是整个工作台在不同屏宽下按什么节奏生长:
- 手机竖屏默认走全宽容器,页面内部只需要关心自己的左右留白。
- 宽屏场景先收口到
720vp,不会让单页工作台在平板上无限摊开。 - 首页、作品页、发现页和"我的"页共用同一套外层宽度判断,截图验收时更容易保持系列感。
也就是说,内容宽度是页面级规则,卡片宽度只是它下面的局部规则。如果把顺序反过来,最后一定会变成每个页面都在局部猜尺寸。
五、20vp 主容器边距是系列感,16vp 编辑区边距是密度控制
真正让页面像同一套产品的,不是卡片做得多漂亮,而是左右边距是否稳定。Header() 已经把首页、作品页、发现页和"我的"页的标题区收口到了统一的 20vp:
@Builder
Header(title: string, sub: string) {
Row() {
Column() {
Text(title).fontSize(28).fontWeight(FontWeight.Bold).fontColor(this.titleColor())
Text(sub).fontSize(14).fontColor(this.bodyColor()).margin({ top: 6 })
}.layoutWeight(1)
Image($r('app.media.app_icon')).width(56).height(56).borderRadius(18)
}.padding({ left: 20, right: 20, top: 18 }).width('100%')
}
其它几个页面的大块容器也延续了这条主线:
}.padding(18).margin({ left: 20, right: 20, top: 18 }).borderRadius(24)
}.padding({ left: 20, right: 20 }).margin({ top: 12, bottom: 160 })
}.padding({ left: 20, right: 20 }).margin({ top: 14 })
这层 20vp 的意义在于:
- 标题、英雄卡、功能网格、作品列表和个人页信息卡都能对齐到同一条竖线。
- 页面留白足够承托
18-24vp圆角的大卡片,不会一上来就贴边。 - 手机截图时能清楚看出"内容区域"和"系统边界"的关系。
但编辑页没有照抄 20vp,而是把工具区缩到了 16vp:
}.width('100%').padding({ left: 16, right: 16 }).margin({ top: 14, bottom: 160 })
这里用 16vp 不是随手拍脑袋,而是因为编辑页的操作密度更高:
- 同一屏里要同时放预览、操作按钮、导出设置和草稿入口。
- 操作卡片内部还会再有一层
padding(16),外层继续给 20vp 很容易挤压按钮区。 16vp + 16vp的内外收口,对按钮组、Slider 和切换项更友好,能兼顾呼吸感和可操作性。
这就是很典型的移动端取舍:页面主容器统一 20vp,编辑器操作区单独退到 16vp,但不能再出现第三套游离边距。
六、预览区可以更像舞台,但操作区必须重新收口
编辑页最容易做坏的地方,是预览区和工具区的关系。当前实现里,预览区被故意做成了更开阔的"舞台":
}.width('100%').height(this.previewStageHeight()).justifyContent(FlexAlign.Center)
.margin({ top: 18 }).borderRadius(0)
.backgroundColor(this.darkPreview ? '#111018' : '#F3EEFF')
.backgroundBlurStyle(BlurStyle.BACKGROUND_THIN)
.border({ width: 1, color: this.cardBorder() })
但是下面的控制区立刻切回统一收口:
Column({ space: 14 }) {
...
}.width('100%').padding({ left: 16, right: 16 }).margin({ top: 14, bottom: 160 })
这个切法很关键,因为它把编辑页明确拆成了两个视觉层次:
- 上半段偏"舞台",允许预览区更开。
- 下半段偏"控制台",必须回到统一卡片宽度和按钮节奏。
如果两者都按一种宽度处理,页面要么显得太散,要么显得太挤。当前截图里已经能直接看到这个差异:

从这张图可以直接验收出三件事:
- 预览区虽然更开,但下面的基础输出参数卡和导出设置卡已经回到同一宽度。
- 卡片左边界、右边界、标题起始线和底部导航视觉中心保持一致。
- 页面不会出现"上半屏一套宽度、下半屏又一套宽度"的断裂。
七、底部工作台不能和内容同宽,但必须服从整页节奏
底部导航不是普通卡片,所以不应该硬性要求和上面所有卡片等宽。当前项目给它的是一条独立但受控的规则:
@Builder
BottomNav() {
Row() {
this.NavItem('首页', 'home')
this.NavItem('作品', 'works')
this.CreateNavItem()
this.NavItem('发现', 'discover')
this.NavItem('我的', 'profile')
}
.height(76)
.width('90%')
.padding({ left: 9, right: 9, top: 9, bottom: 9 })
.margin({ bottom: 14 })
.borderRadius(28)
.backgroundBlurStyle(BlurStyle.COMPONENT_THICK)
.backgroundColor(this.darkPreview ? '#33242235' : '#38FFFFFF')
.border({ width: 1, color: this.darkPreview ? '#40FFFFFF' : '#80FFFFFF' })
}
这里的关键不是"90%"这个数字本身,而是它和整页节奏的关系:
width('90%')让底部工作台比内容区略窄,看起来像悬浮控制台,而不是整屏底栏。margin({ bottom: 14 })给系统手势区留出了呼吸空间。- 导航始终居中,不会和上面的卡片左右对不齐而互相打架。
首页截图里,这个关系会更直观:

对工具类 App 来说,底部工作台可以独立,但不能脱离版式秩序独立。
八、截图要证明规则生效,而不是只证明页面好看
移动端视觉验收最怕截图变成"效果展示"。我在这次复核里把每张图都对应到一条源码规则:
| 截图 | 需要证明的规则 | 对应源码位置 |
|---|---|---|
| 首页 | Header()、英雄区、功能卡片都遵守 20vp 外层节奏 |
Header()、首页 Grid()、英雄卡片 margin({ left: 20, right: 20 }) |
| 编辑页 | 预览舞台可以满宽,但工具卡片回到 16vp 操作密度 | previewStageHeight()、编辑页工具区 padding({ left: 16, right: 16 }) |
| 作品页 | 草稿、导出记录和清空按钮没有各自散落宽度 | WorksPage()、WorkCard()、列表标题 padding({ left: 20 }) |
| 我的页 | 主题入口和信息卡保持同一条横向边界 | ProfilePage()、主题卡片 margin({ left: 20, right: 20 }) |
这样看截图时就不会只停在"好不好看",而是能继续追问三件事:
- 这张图能不能反推到具体源码。
- 这张图能不能覆盖一种真实用户路径。
- 这张图能不能发现下一轮回归风险。
下面三张页面截图分别补足作品页、我的页和多页统一性的证据:


九、复现与回归检查:把视觉判断变成命令和清单
如果只靠肉眼看截图,下一次改动还是容易把宽度节奏带偏。我的处理方式是把复现过程拆成三步。
第一步,先用 rg 定位布局规则,确认所有关键宽度都在同一个文件里:
rg -n "contentWidth\(|screenWidthVp|Header\(|BottomNav\(|previewStageHeight\(|margin\(\{ left: 20|padding\(\{ left: 16" entry/src/main/ets/products/main/Index.ets
第二步,检查截图证据是否覆盖主路径:
Get-ChildItem doc/screenshots_current -Filter "gifrubik_*" |
Where-Object { $_.Name -match "real_home|editor_uniform|real_works|profile_expanded" } |
Select-Object Name, Length
第三步,用本地文章质量脚本检查文章是否仍然保留"可复核工程稿"的结构:
node tools/check_csdn_article_quality.js 16
本地预检的实际输出如下:
Article: doc\csdn-series\16-ArkUI操作卡片宽度统一与移动端视觉验收.md
Score: 98/100
Pass: YES (target >= 92)
我还会把下面几类改动列为回归高风险:
- 在单个卡片里新增固定宽度,例如
width(320)、width(360)。 - 给某个页面单独写一套
margin({ left: xx, right: xx }),但没有解释它和 20vp 主节奏的关系。 - 改底部导航宽度,却没有同时检查首页、编辑页和作品页截图。
- 把预览舞台和工具卡片放进同一个宽度规则里,导致沉浸预览和操作密度互相牵制。
- 只在浅色主题截图里验收,忘了第 15 篇已经把深色主题纳入同一套 token。
这些规则比"这张截图看起来差不多"更稳,因为它们能在下一次代码评审、发版截图和文章复盘里重复使用。
十、工程验收清单
| 验收项 | 结果 | 证据 |
|---|---|---|
| 顶层内容宽度通过统一入口控制 | 通过 | contentWidth() + build() 中 width(this.contentWidth()) |
| 页面主容器边距保持统一节奏 | 通过 | Header() 及首页 / 作品 / 发现 / 我的页大量 margin({ left: 20, right: 20 }) |
| 编辑页工具区使用更紧凑的 16vp 内层边距 | 通过 | EditorPage() 中 padding({ left: 16, right: 16 }) |
| 卡片宽度依赖外层容器而不是硬编码百分比 | 通过 | width('100%') + 统一 padding / borderRadius / border |
| 预览区与操作卡片形成上下两层节奏 | 通过 | gifrubik_editor_uniform_cards_final.jpeg |
| 底部导航独立但不脱离整页版式 | 通过 | BottomNav() 中 width('90%')、margin({ bottom: 14 }) |
| 多页面截图仍能看出同一产品语言 | 通过 | 首页、编辑页、作品页、我的页四张真实截图 |
| 第 16 篇具备版本、时间、源码对象和发布状态上下文 | 通过 | 本文 SEO 信息、源码对象段、发布记录段 |
| 第 16 篇具备本地质量预检入口 | 通过 | tools/check_csdn_article_quality.js |
十一、小结
第 16 篇真正拆开的,不只是"卡片怎么摆更好看",而是工具类 App 最容易被忽略的一层工程纪律:布局宽度、横向边距、预览区节奏、底部工作台宽度和文章本身的验证证据都必须先统一。
对"动图魔方"这种单页工作台来说,卡片视觉一致性不是锦上添花,而是后面还能继续扩功能的基础。对技术文章本身也一样,如果没有版本、时间、源码对象、命令和截图证据,平台就很容易把它看成一篇"讲感受"的 UI 文章,而不是一篇可复核的工程拆解。
十二、下一篇衔接
第 17 篇继续拆 《动图魔方技术拆解 17:清除虚拟数据后,如何用真实素材验证 GIF 工具》,重点会落在:
- 为什么工具类 App 后期必须移除演示假数据,转向真实图片、GIF 和视频样例。
TestAssetService如何承担测试素材导入、真实流程验证和截图证据生成。- 如何把"素材导入 -> 预览 -> 导出 -> 作品页回看"串成真正可复验的验收闭环。