这一篇进入 ArkUI 页面搭建。目标不是堆控件,而是把一个旅行 App 的首页做出“第一眼想继续看”的感觉:顶部大图负责氛围,快捷入口负责效率,横向卡片和底部导航负责主流程。

项目落点:

  • 首页结构:HomePage()
  • 大图头部:Hero()
  • 快捷入口:QuickEntrances()
  • 底部导航:BottomNav()
  • 卡片组件:SmallImageCard()ListCard()RouteCard()

读完这一篇,你可以复用一套内容型 App 首页结构:Hero 大图、分区标题、横向精选卡、纵向列表卡和固定底部导航。

首页的目标不是堆内容

旅行 App 的首页有一个特殊要求:它既要提供入口,又要制造“想出发”的感觉。

《疆域纪行》的首页没有做成工具列表,而是采用了旅行杂志式结构:

  1. 顶部大图 Hero
  2. 快捷入口
  3. 热门目的地横向卡片
  4. 精选路线列表
  5. 必吃美食列表
  6. 固定底部导航

这个结构兼顾了视觉和功能:用户第一眼看到的是目的地氛围,继续下滑才是具体内容。

首页 Builder 结构

首页入口在 Index.etsHomePage()

@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 }) 是这里的重点。它们能让右侧文本区域正确压缩,避免长标题把收藏按钮挤出屏幕。

旅行内容里中文地名和描述都可能很长,所有标题和摘要都应该配合:

  • maxLines
  • textOverflow
  • layoutWeight
  • constraintSize

这是 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 切换体验。

下一篇我们看探索页:如何用搜索、类型切换、地区筛选、主题筛选和排序,做一个完全本地的内容检索体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐