HarmonyOS实战《疆域纪行》第03篇|首页搭起来:Hero 大图、快捷入口和底部导航实战
这一篇进入 ArkUI 页面搭建。目标不是堆控件,而是把一个旅行 App 的首页做出“第一眼想继续看”的感觉:顶部大图负责氛围,快捷入口负责效率,横向卡片和底部导航负责主流程。
项目落点:
- 首页结构:
HomePage() - 大图头部:
Hero() - 快捷入口:
QuickEntrances() - 底部导航:
BottomNav() - 卡片组件:
SmallImageCard()、ListCard()、RouteCard()
读完这一篇,你可以复用一套内容型 App 首页结构:Hero 大图、分区标题、横向精选卡、纵向列表卡和固定底部导航。
首页的目标不是堆内容
旅行 App 的首页有一个特殊要求:它既要提供入口,又要制造“想出发”的感觉。
《疆域纪行》的首页没有做成工具列表,而是采用了旅行杂志式结构:
- 顶部大图 Hero
- 快捷入口
- 热门目的地横向卡片
- 精选路线列表
- 必吃美食列表
- 固定底部导航
这个结构兼顾了视觉和功能:用户第一眼看到的是目的地氛围,继续下滑才是具体内容。
首页 Builder 结构
首页入口在 Index.ets 的 HomePage():
@Builder
HomePage() {
Column({ space: 18 }) {
this.Hero()
this.QuickEntrances()
this.HomeSectionTitle('热门目的地', 'HOT DESTINATIONS', 'spot')
Scroll() {
Row({ space: 12 }) {
ForEach(this.spots, (item: AtlasItem) => {
this.SmallImageCard(item)
}, (item: AtlasItem) => item.id)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
this.HomeSectionTitle('精选路线', 'FEATURED ROUTES', 'route')
ForEach(this.routes, (item: RouteItem) => {
this.RouteCard(item)
}, (item: RouteItem) => item.id)
this.HomeSectionTitle('必吃美食', 'LOCAL FOODS', 'food')
ForEach(this.foods, (item: AtlasItem) => {
this.ListCard(item)
}, (item: AtlasItem) => item.id)
}
}
这段代码有几个值得注意的点。
第一,用 Column({ space: 18 }) 控制整体节奏,而不是每个模块手动写 margin。页面会更稳定。
第二,横向目的地使用 Scroll + Row + ForEach,适合数量不算太大的精选内容。
第三,不同内容类型使用不同卡片:
- 目的地:
SmallImageCard - 路线:
RouteCard - 美食:
ListCard
这让同一个首页有信息层级,而不是全页重复同一种卡片。
Hero:旅行 App 的第一张名片
顶部 Hero 使用 Stack 叠加图片、渐变和文字:
@Builder
Hero() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.img_sayram'))
.width('100%')
.height(250)
.objectFit(ImageFit.Cover)
Column()
.width('100%')
.height(250)
.linearGradient({
angle: 180,
colors: [['#001E36CC', 0.0], ['#001E3633', 0.48], ['#001E36DD', 1.0]]
})
Column({ space: 8 }) {
Text('疆域纪行')
.fontSize(21)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Text('XINJIANG ATLAS')
.fontSize(10)
.fontColor('#D7E8F4')
Text('DISCOVER')
.fontSize(28)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Text('XINJIANG')
.fontSize(34)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Text('发现新疆之美')
.fontSize(17)
.fontColor('#F7E7C2')
}
.padding(18)
}
.width('100%')
.height(250)
.borderRadius(18)
.clip(true)
}
核心思路是三层:
- 图片负责氛围。
- 渐变负责可读性。
- 文案负责定位和入口暗示。
很多新手会直接把白色文字放在图片上,结果一换图就看不清。加渐变不是装饰,而是保证信息在各种图片上的稳定性。
横向目的地卡片
SmallImageCard 是一个典型的小型图片卡:
@Builder
SmallImageCard(item: AtlasItem) {
Stack({ alignContent: Alignment.BottomStart }) {
Column()
.width(124)
.height(94)
.backgroundColor(item.color)
Image(item.image)
.width(124)
.height(94)
.objectFit(ImageFit.Cover)
.opacity(0.48)
Column({ space: 3 }) {
Text(item.title)
.fontSize(14)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.region)
.fontSize(10)
.fontColor('#F5E8D2')
.maxLines(1)
}
.padding(10)
}
.width(124)
.height(94)
.borderRadius(14)
.clip(true)
.onClick(() => {
this.openDetail(item.kind, item.id);
})
}
这里有一个小技巧:先放一层 backgroundColor(item.color),再放图片并设置透明度。这样图片即使色彩不统一,最终卡片也会有统一的视觉调性。
同时,卡片宽高固定。这对横向滚动列表很重要。如果卡片尺寸跟随文字变化,列表滑动时会显得不稳定。
列表卡片:信息密度更高
美食列表使用 ListCard,它比图片卡更偏信息展示:
@Builder
ListCard(item: AtlasItem) {
Row({ space: 12 }) {
Stack() {
Image(item.image)
.width(92)
.height(82)
.objectFit(ImageFit.Cover)
.opacity(0.48)
}
.width(92)
.height(82)
.borderRadius(14)
.clip(true)
Column({ space: 5 }) {
Row() {
Text(item.title)
.fontSize(17)
.fontColor('#1F2D36')
.fontWeight(FontWeight.Bold)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
Text(this.isFavorite(item.id) ? '♥' : '♡')
.fontSize(18)
.fontColor(this.isFavorite(item.id) ? '#B5792C' : '#8B9298')
}
Text(item.summary)
.fontSize(12)
.fontColor('#56636B')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
.constraintSize({ minWidth: 0 })
}
.padding(12)
.width('100%')
.borderRadius(18)
.backgroundColor(Color.White)
}
layoutWeight(1) 和 constraintSize({ minWidth: 0 }) 是这里的重点。它们能让右侧文本区域正确压缩,避免长标题把收藏按钮挤出屏幕。
旅行内容里中文地名和描述都可能很长,所有标题和摘要都应该配合:
maxLinestextOverflowlayoutWeightconstraintSize
这是 ArkUI 写列表卡片时必须养成的习惯。
底部导航:主流程稳定器
主页面的 Shell 是:
@Builder
MainShell() {
Column() {
if (this.activeTab === 'favorite') {
this.FavoriteStickyPage()
} else {
Scroll(this.mainScroller) {
Column({ space: 18 }) {
if (this.activeTab === 'home') {
this.HomePage();
} else if (this.activeTab === 'explore') {
this.ExplorePage();
} else if (this.activeTab === 'route') {
this.RoutePage();
} else {
this.ProfilePage();
}
}
.padding({ left: 16, right: 16, top: 18, bottom: 92 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
}
this.BottomNav()
}
.width('100%')
.height('100%')
}
底部导航不是简单地放几个按钮,它承担了两个作用:
- 保证主功能入口固定。
- 给内容区域预留底部空间,避免被遮挡。
所以内容区域 bottom padding 设置为 92,给底部导航留出空间。这种细节如果忽略,列表最后几项很容易被底部栏盖住。
Tab 切换时回到顶部
项目还定义了滚动控制:
private mainScroller: Scroller = new Scroller();
private scrollMainToTop(): void {
this.mainScroller.scrollTo({ xOffset: 0, yOffset: 0, animation: false });
}
private switchTab(key: string): void {
this.scrollMainToTop();
this.activeTab = key;
}
这个体验很细,但很实用。用户在首页滑到很下面,再切到探索页,如果探索页也停留在旧滚动位置,会让人困惑。Tab 切换时回到顶部,符合大多数内容 App 的预期。
首页的可复用组件清单
这一篇里涉及的组件都可以迁移到其他 HarmonyOS 项目:
Hero:图片头图 + 渐变 + 标题SmallImageCard:横向滚动目的地卡片ListCard:图文列表卡片RouteCard:路线信息卡片HomeSectionTitle:带英文副标题和跳转入口的分区标题BottomNav:固定底部导航
如果项目继续扩大,可以把这些 @Builder 抽成独立组件文件。但在 MVP 阶段,放在同一个页面里更利于快速迭代。
本篇小结
首页搭建的关键不是控件用得多,而是层级清楚:
- 用 Hero 建立产品气质。
- 用横向 Scroll 展示精选目的地。
- 用不同卡片区分不同内容密度。
- 用底部导航稳定主流程。
- 用滚动控制提升 Tab 切换体验。
下一篇我们看探索页:如何用搜索、类型切换、地区筛选、主题筛选和排序,做一个完全本地的内容检索体验。
更多推荐


所有评论(0)