前言

这个系列我们会从零开始,基于 HarmonyOS 6 开发一个宠物商店应用。

整个系列会以"功能模块"为主线逐步推进。作为第一篇文章,我们不急着写业务逻辑,先把项目框架搭起来——包括:

  1. 项目结构怎么组织。
  2. 深色模式怎么适配(颜色不要写死)。
  3. 底部悬浮导航怎么做。

一、项目结构

先看一眼我们现在的目录结构:

frontend/
  entry/
    src/main/ets/
      entryability/
        EntryAbility.ets        // Ability 生命周期入口
      components/
        PagePlaceholder.ets     // 公共占位组件
      pages/
        Index.ets               // 主入口,HdsTabs 悬浮页签容器
        HomePage.ets            // 首页
        ShopPage.ets            // 商店页
        CartPage.ets            // 购物车页
        ProfilePage.ets         // 个人中心页
    src/main/resources/
      base/
        element/
          color.json            // 浅色模式颜色资源
          float.json            // 字号资源
          string.json           // 字符串资源
        media/
      dark/
        element/
          color.json            // 深色模式颜色资源

目前我们只搭了框架,5 个底部页签页面都只是占位,后续会逐个填充业务功能。

几个设计上的选择:

  • 每个底部页签一个独立文件HomePage.etsShopPage.ets 等,不把所有内容塞在 Index.ets 里。
  • 公共组件放在 components/:例如 PagePlaceholder,多个页面可以复用。
  • Ability 代码单独放 entryability/:生命周期代码不跟 UI 混在一起。

这些在前期养成习惯,后面功能多了就不会乱。


二、深色模式适配

深色模式适配,核心思路就是颜色不写死

浅色模式
深色模式

2.1 颜色资源文件

我们在 base/element/color.json 中定义浅色模式的颜色,在 dark/element/color.json 中定义对应的深色模式颜色。系统会根据当前主题自动切换。

这是浅色模式的 base/element/color.json

{
  "color": [
    { "name": "start_window_background", "value": "#FFFFFF" },
    { "name": "page_background", "value": "#F1F3F5" },
    { "name": "card_background", "value": "#FFFFFF" },
    { "name": "text_primary", "value": "#182431" },
    { "name": "text_secondary", "value": "#666666" },
    { "name": "text_tertiary", "value": "#999999" },
    { "name": "accent", "value": "#007DFF" },
    { "name": "divider", "value": "#E5E5E5" },
    { "name": "tab_bar_background", "value": "#CCFFFFFF" }
  ]
}

这是深色模式的 dark/element/color.json

{
  "color": [
    { "name": "start_window_background", "value": "#000000" },
    { "name": "page_background", "value": "#121212" },
    { "name": "card_background", "value": "#1E1E1E" },
    { "name": "text_primary", "value": "#E6E6E6" },
    { "name": "text_secondary", "value": "#AAAAAA" },
    { "name": "text_tertiary", "value": "#777777" },
    { "name": "accent", "value": "#4EA1FF" },
    { "name": "divider", "value": "#2A2A2A" },
    { "name": "tab_bar_background", "value": "#CC1E1E1E" }
  ]
}

两个文件的 name 必须保持一致,系统才会在深色模式时自动切换到 dark/ 下的值。

2.2 如何使用

在代码中通过 $r('app.color.xxx') 引用,例如:

.backgroundColor($r('app.color.page_background'))
.fontColor($r('app.color.text_primary'))

不管当前是什么主题,系统都会自动取对应的颜色值。后续所有新页面的颜色,一律走资源文件,不要直接写十六进制色值。

2.3 Ability 中的配置

EntryAbility.ets 中,我们通过 setColorMode 设置为跟随系统:

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  try {
    this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
  } catch (err) {
    hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
  }
}

COLOR_MODE_NOT_SET 表示跟随系统设置,用户切换深色/浅色时,应用会自动响应。


三、底部悬浮导航

导航是应用的骨架。我们选用 HdsTabs 组件来做底部悬浮页签。

悬浮导航

3.1 为什么用 HdsTabs

从 API 23开始,官方提供了 HdsTabs 组件,它天然支持:

  • 底部悬浮样式(barOverlap)。
  • 沉浸光感材质(systemMaterialEffect)。
  • 迷你栏(miniBar)。

不需要自己写复杂的 Stack 叠加布局,一行配置就能开启。

3.2 导入模块

Index.ets 顶部导入:

import { HdsTabs, HdsTabsController, hdsMaterial } from '@kit.UIDesignKit';

三个东西各有用处:

导入 作用
HdsTabs 页签容器组件
HdsTabsController 控制器,用来编程式切换页签
hdsMaterial 沉浸光感材质的枚举参数

3.3 初始化控制器

@Entry
@Component
struct Index {
  private controller: HdsTabsController = new HdsTabsController();
  // ...
}

HdsTabsController 在后面如果需要编程式跳转页签时会用到。

3.4 页签内容组件

我们有 4 个底部页签,每个页面对应一个独立组件文件:

  • HomePage — 首页

  • ShopPage — 商店

  • CartPage — 购物车

  • ProfilePage — 我的

每个页面当前都是占位状态,统一使用 PagePlaceholder 组件展示标题和提示文案。

来看一下 PagePlaceholder 的实现:

@Component
export struct PagePlaceholder {
  @Prop pageTitle: ResourceStr = '';
  @Prop tipText: ResourceStr = $r('app.string.placeholder_tip');

  build() {
    Column() {
      Text(this.pageTitle)
        .fontSize($r('app.float.title_font_size'))
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .margin({ top: 64, bottom: 16 })

      Text(this.tipText)
        .fontSize($r('app.float.placeholder_font_size'))
        .fontColor($r('app.color.text_secondary'))
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.page_background'))
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
  }
}

几个细节:

  • @Prop pageTitle: ResourceStr:类型用 ResourceStr 而不是 string,因为 $r('app.string.xxx') 返回的是 Resource 类型,不能直接赋给 stringResourceStr = string | Resource,两者都能接收。
  • 所有颜色和字号都走资源文件$r('app.color.xxx')$r('app.float.xxx'),深色模式自动适配。
  • @Prop 的默认值tipText 给了默认值 $r('app.string.placeholder_tip'),调用方可以不传。

5 个页面组件则各自引用 PagePlaceholder,例如 HomePage

import { PagePlaceholder } from '../components/PagePlaceholder';

@Component
export struct HomePage {
  build() {
    Column() {
      PagePlaceholder({ pageTitle: $r('app.string.page_home_title') })
    }
    .width('100%')
    .height('100%')
  }
}

其他 4 个页面结构完全相同,只是传入的 pageTitle 不同。

3.5 自定义页签栏

底部页签栏需要展示文字,我们通过 @Builder 自定义:

@Builder
tabBuilder(text: ResourceStr, index: number) {
  Column() {
    Text(text)
      .fontSize($r('app.float.tab_font_size'))
      .fontColor($r('app.color.text_primary'))
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .onClick(() => {
    this.controller.changeIndex(index);
  })
}

这里没有用系统图标,我们先只展示文字。后续可以替换为图标。

3.6 组装 HdsTabs

这是 Index.ets 的核心构建方法:

build() {
  Column() {
    HdsTabs({ controller: this.controller }) {
      TabContent() {
        HomePage()
      }
      .tabBar(this.tabBuilder($r('app.string.tab_home'), 0))

      TabContent() {
        ShopPage()
      }
      .tabBar(this.tabBuilder($r('app.string.tab_shop'), 1))

      TabContent() {
        CartPage()
      }
      .tabBar(this.tabBuilder($r('app.string.tab_cart'), 2))

      TabContent() {
        ProfilePage()
      }
      .tabBar(this.tabBuilder($r('app.string.tab_profile'), 3))
    }
    .barOverlap(true)
    .barPosition(BarPosition.End)
    .vertical(false)
    .barFloatingStyle({
      barWidth: { smallWidth: 320, mediumWidth: 360, largeWidth: 420 },
      barBottomMargin: 28,
      systemMaterialEffect: {
        materialType: hdsMaterial.MaterialType.IMMERSIVE,
        materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
      }
    })
  }
  .width('100%')
  .height('100%')
  .backgroundColor($r('app.color.page_background'))
}

3.7 关键属性说明

布局相关:

.barOverlap(true)            // 页签栏悬浮于内容之上
.barPosition(BarPosition.End) // 页签栏放在底部
.vertical(false)              // 横向页签

这三个是悬浮底部页签的基础配置,缺一不可。

悬浮样式 barFloatingStyle

.barFloatingStyle({
  barWidth: { smallWidth: 320, mediumWidth: 360, largeWidth: 420 },
  barBottomMargin: 28,
  systemMaterialEffect: {
    materialType: hdsMaterial.MaterialType.IMMERSIVE,
    materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
  }
})

各参数含义:

参数 作用
barWidth 页签栏分档宽度,适配不同屏幕尺寸
barBottomMargin 页签栏距离容器底部的间距
systemMaterialEffect 沉浸光感材质效果

沉浸光感 systemMaterialEffect

systemMaterialEffect: {
  materialType: hdsMaterial.MaterialType.IMMERSIVE,
  materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
}

IMMERSIVE 是沉浸式材质,ADAPTIVE 让材质等级由系统自适应。如果设备不支持沉浸式,可以降级为 SMOOTH,不过这里我们先用 ADAPTIVE 交给系统处理。


四、最终效果

到这步为止,应用已经有了:

  1. 完整的项目结构:页面组件分离、公共组件抽离、资源文件分深浅色两套。
  2. 深色模式适配:所有颜色通过 $r('app.color.xxx') 引用,跟随系统自动切换。
  3. 底部悬浮导航:4 个页签,悬浮在内容之上,带有沉浸光感材质效果。

目前 4 个页面的内容都是占位状态。


以上就是第一篇文章的全部内容。代码全部来自我们的宠物商店项目,后续每实现一个功能模块,都会对应一篇教程文章,从零讲清楚实现思路和关键细节。

Logo

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

更多推荐