动图魔方技术拆解 16:HarmonyOS ArkUI contentWidth 与 BottomNav 移动端自适应布局

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 验收口径。

一、真实工程问题背景

"动图魔方"不是展示型页面,而是一个高频切换的本地创作工作台:

  1. 首页要承接功能入口、英雄卡片和最近作品。
  2. 编辑页要同时容纳预览区、操作卡片、导出设置和底部工作台。
  3. 作品页、发现页、"我的"页还要复用同一套卡片语言。

这类产品最容易出现的不是"某个按钮颜色不对",而是页面容器节奏不一致

  1. 首页卡片边距是 20vp,编辑页工具区又是另一套 16vp,用户切页时会直觉感到页面忽然变窄。
  2. 预览区如果直接全宽贴边,下面卡片再做大圆角白底,就会像两套布局临时拼接。
  3. 底部导航如果宽度和上层内容毫无关系,截图时会显得"漂"在页面最下方。
  4. 同样都叫卡片,首页功能卡、作品卡、发现页模板卡和我的页信息卡如果没有共同的圆角、边框和横向收口,视觉上就会越来越散。

这类问题很难靠"最后 UI 调一调"补救,因为它本质上不是某个控件样式问题,而是布局规则没有先被定义出来;一旦页面数量增加,临时调出来的边距会互相覆盖,后续每次截图验收都要重新返工。

如果这个问题不提前收口,后面的功能迭代会越来越难控制。比如第 17 篇准备继续拆分享链路时,分享按钮、导出结果、作品卡片和底部导航都会同时出现在一个截图里;如果第 16 篇没有先把移动端横向节奏固定下来,后面每加一个入口都要重新判断"这个卡片到底应该跟谁对齐"。所以这篇文章不是做视觉润色,而是在给后续 GIF 导出、分享、作品管理和主题切换建立共同的页面边界。

二、目标与边界:先定义移动端布局验收口径

这次的目标不是重做视觉风格,而是把已有页面的布局规则收口成可复核的工程边界:

  1. 目标一:统一页面主宽度 。手机端保持 100%,宽屏时集中收口,避免多设备截图表现失控。
  2. 目标二:统一页面级留白。首页、作品页、发现页、我的页都围绕 20vp 主节奏组织内容。
  3. 目标三:保留编辑页的功能差异。预览舞台允许更沉浸,操作卡片仍然保持可读和可触控。
  4. 目标四:让底部工作台独立成层。它可以不像正文卡片一样宽,但必须服从整页视觉秩序。

对应的边界也要说清楚:

  1. 本文不重讲第 15 篇的深浅色 token,只复用它已经沉淀下来的颜色函数。
  2. 本文不讨论 GIF 编码、抽帧、导出性能,只处理页面结构层的问题。
  3. 本文不追求所有控件同宽,而是按"页面、舞台、卡片、底部工作台"四类对象分别定义宽度语义。

基于这个边界,我没有先去微调某一张卡片,而是先把"截图看起来一致"拆成四条可以复核的工程规则:

验收对象 规则 为什么这么定
页面主容器 手机端走 100%,宽屏收口到 720vp 避免平板上内容无限拉伸,也避免手机端出现人为窄栏
普通内容区 统一使用 20vp 左右边距 首页、作品页、发现页、我的页切换时横向节奏一致
编辑操作区 预览舞台可满宽,下面工具卡片退回 16vp 内边距 预览需要沉浸感,操作区需要可读性和触控密度
底部工作台 独立 90% 宽度并居中悬浮 底部导航是操作台,不应该和正文卡片抢同一个宽度模型

这里有一个很容易被忽略的判断:移动端一致性不是所有东西都同宽,而是每一类对象都有稳定的宽度语义。预览舞台、功能卡片、底部导航、页面标题区本来就承担不同任务,强行把它们都套同一个宽度,反而会让页面显得僵硬。

我把这套口径和 ArkUI 的布局约束对应成下面几条实现原则:

  1. 宽度判断只放在顶层入口,不在每个页面里重复猜屏宽。
  2. 页面级留白由 Header()、页面主区和列表容器共同遵守。
  3. 编辑页允许预览舞台"破格",但操作卡片必须回到可读宽度。
  4. 底部导航只跟随 contentWidth() 的上限,不跟随某一个页面的局部卡片。
  5. 每个验收项都必须能用源码定位、真实截图和发布前检查互相印证。

三、源码对象、验证环境与当前证据面

这篇文章对应的真实源码对象和证据文件如下:

  1. entry/src/main/ets/products/main/Index.ets
  2. entry/src/main/ets/entryability/EntryAbility.ets
  3. entry/src/main/resources/base/profile/main_pages.json
  4. doc/screenshots_current/gifrubik_real_home.jpeg
  5. doc/screenshots_current/gifrubik_editor_uniform_cards_final.jpeg
  6. doc/screenshots_current/gifrubik_real_works.jpeg
  7. doc/screenshots_current/gifrubik_profile_expanded_light.jpeg
  8. doc/csdn-series/publish-record.json

这几个对象的职责并不相同:

  1. Index.ets 是主实现,里面同时包含屏宽测量、页面切换、卡片 Builder、编辑页工具卡片和底部导航。
  2. EntryAbility.ets 是应用级入口,负责窗口和颜色模式的初始状态,决定页面是否能稳定进入同一套视觉环境。
  3. main_pages.json 是路由入口证明,说明当前验收围绕同一个主页面展开,不是多个页面临时拼接。
  4. doc/screenshots_current/*.jpeg 是视觉验收证据,用来对齐源码规则和真实移动端截图。

我对照的官方能力文档主要有两类:

  1. HarmonyOS ArkUI 多设备自适应布局最佳实践,核心是不要把页面写死成单一尺寸,而是让布局根据窗口和设备形态调整。
  2. 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())
}

这一步解决的不是"某张卡片多宽",而是整个工作台在不同屏宽下按什么节奏生长

  1. 手机竖屏默认走全宽容器,页面内部只需要关心自己的左右留白。
  2. 宽屏场景先收口到 720vp,不会让单页工作台在平板上无限摊开。
  3. 首页、作品页、发现页和"我的"页共用同一套外层宽度判断,截图验收时更容易保持系列感。

也就是说,内容宽度是页面级规则,卡片宽度只是它下面的局部规则。如果把顺序反过来,最后一定会变成每个页面都在局部猜尺寸。

五、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 的意义在于:

  1. 标题、英雄卡、功能网格、作品列表和个人页信息卡都能对齐到同一条竖线。
  2. 页面留白足够承托 18-24vp 圆角的大卡片,不会一上来就贴边。
  3. 手机截图时能清楚看出"内容区域"和"系统边界"的关系。

但编辑页没有照抄 20vp,而是把工具区缩到了 16vp:

复制代码
}.width('100%').padding({ left: 16, right: 16 }).margin({ top: 14, bottom: 160 })

这里用 16vp 不是随手拍脑袋,而是因为编辑页的操作密度更高:

  1. 同一屏里要同时放预览、操作按钮、导出设置和草稿入口。
  2. 操作卡片内部还会再有一层 padding(16),外层继续给 20vp 很容易挤压按钮区。
  3. 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 })

这个切法很关键,因为它把编辑页明确拆成了两个视觉层次:

  1. 上半段偏"舞台",允许预览区更开。
  2. 下半段偏"控制台",必须回到统一卡片宽度和按钮节奏。

如果两者都按一种宽度处理,页面要么显得太散,要么显得太挤。当前截图里已经能直接看到这个差异:

从这张图可以直接验收出三件事:

  1. 预览区虽然更开,但下面的基础输出参数卡和导出设置卡已经回到同一宽度。
  2. 卡片左边界、右边界、标题起始线和底部导航视觉中心保持一致。
  3. 页面不会出现"上半屏一套宽度、下半屏又一套宽度"的断裂。

七、底部工作台不能和内容同宽,但必须服从整页节奏

底部导航不是普通卡片,所以不应该硬性要求和上面所有卡片等宽。当前项目给它的是一条独立但受控的规则:

复制代码
@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%"这个数字本身,而是它和整页节奏的关系:

  1. width('90%') 让底部工作台比内容区略窄,看起来像悬浮控制台,而不是整屏底栏。
  2. margin({ bottom: 14 }) 给系统手势区留出了呼吸空间。
  3. 导航始终居中,不会和上面的卡片左右对不齐而互相打架。

首页截图里,这个关系会更直观:

对工具类 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 })

这样看截图时就不会只停在"好不好看",而是能继续追问三件事:

  1. 这张图能不能反推到具体源码。
  2. 这张图能不能覆盖一种真实用户路径。
  3. 这张图能不能发现下一轮回归风险。

下面三张页面截图分别补足作品页、我的页和多页统一性的证据:

九、复现与回归检查:把视觉判断变成命令和清单

如果只靠肉眼看截图,下一次改动还是容易把宽度节奏带偏。我的处理方式是把复现过程拆成三步。

第一步,先用 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)

我还会把下面几类改动列为回归高风险:

  1. 在单个卡片里新增固定宽度,例如 width(320)width(360)
  2. 给某个页面单独写一套 margin({ left: xx, right: xx }),但没有解释它和 20vp 主节奏的关系。
  3. 改底部导航宽度,却没有同时检查首页、编辑页和作品页截图。
  4. 把预览舞台和工具卡片放进同一个宽度规则里,导致沉浸预览和操作密度互相牵制。
  5. 只在浅色主题截图里验收,忘了第 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 工具》,重点会落在:

  1. 为什么工具类 App 后期必须移除演示假数据,转向真实图片、GIF 和视频样例。
  2. TestAssetService 如何承担测试素材导入、真实流程验证和截图证据生成。
  3. 如何把"素材导入 -> 预览 -> 导出 -> 作品页回看"串成真正可复验的验收闭环。