鸿蒙NEXT场景化开发:设计页面导航

172 阅读7分钟

在第 4 章中,我们了解了基于 Navigation 组件的使用技巧,涵盖了页面标题的设置、子页面的实现,以及页面之间的导航跳转交互。当应用中包含多个页面,且这些页面之间存在数据关联时,页面导航无疑是一种高效的解决方案。

在本章中,我们将进一步拓展页面导航的应用场景,深入探讨更多相关的开发技巧,包含页面导航的 UI 设计以及数据驱动的页面导航策略,确保页面之间的数据传递更加高效、准确。

7.1 实现多页面导航

创建一个名为MySimpleNavigation的新的 HarmonyOS 项目,并打开工程开发面板。

7.1.1 创建多个子页面

使用@Component 装饰器创建两个自定义组件,分别命名为ChildAView、ChildBView,代码如下:

//第7章/Index.ets
@Component
struct ChildAView {
  @Consume('NavPathStack') pageStack: NavPathStack

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.Center }) {
        Column({ space: 68 }) {
          Column({ space: 20 }) {
            SymbolGlyph($r('sys.symbol.label'))
              .fontSize(48)
              .fontColor([Color.Blue])
            Text('This is ChildView')
              .fontSize(17)
              .fontColor('#999999')
          }
          Button('Back to IndexView', { type: ButtonType.Capsule, stateEffect: true })
            .onClick(()=>{
              this.pageStack.pop()
            })
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      .height('100%')
    }
    .title('ChildAView')
  }
}

@Component
struct ChildBView {
  @Consume('NavPathStack') pageStack: NavPathStack

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.Center }) {
        Column({ space: 68 }) {
          Column({ space: 20 }) {
            SymbolGlyph($r('sys.symbol.location_north_up_right_circle_fill'))
              .fontSize(48)
              .fontColor([Color.Green])
            Text('This is ChildView')
              .fontSize(17)
              .fontColor('#999999')
          }
          Button('Back to IndexView', { type: ButtonType.Capsule, stateEffect: true })
            .onClick(()=>{
              this.pageStack.pop()
            })
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      .height('100%')
    }
    .title('ChildBView')
  }
}

ChildAView、ChildBView组件分别实现了两个功能模块的界面和返回跳转逻辑。两个页面都使用@Consume 装饰器将pageStack注入导航路径栈对象,并使用NavDestination 构建子组件的 UI。

UI部分,ChildAView、ChildBView 组件都使用垂直布局,由SymbolGlyph、Text、Button 组件组成页面内容,其中,在Button 组件的点击事件中,组件都调用pageStack 的pop()方法来实现页面的返回操作。

7.1.2 实现多页面跳转

回到Index 组件,使用Navigation 组件,并实现页面跳转逻辑,代码如下:

//第7章/Index.ets
@Entry
@Component
struct Index {
  @Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.pageStack) {
      Stack({ alignContent: Alignment.Center }) {
        Column({ space: 68 }) {
          Column({ space: 20 }) {
            SymbolGlyph($r('sys.symbol.house'))
              .fontSize(48)
              .fontColor([Color.Red])
            Text('This is IndexView')
              .fontSize(17)
              .fontColor('#999999')
          }

          Column({ space: 20 }) {
            Button('Show ChildAView', { type: ButtonType.Capsule, stateEffect: true })
              .onClick(() => {
                this.pageStack.pushPath({ name: 'ChildAView' })
              })
            Button('Show ChildBView', { type: ButtonType.Capsule, stateEffect: true })
              .onClick(() => {
                this.pageStack.pushPath({ name: 'ChildBView' })
              })
          }
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      .height('100%')
    }
    .title('IndexView')
    .navDestination(this.pagesMap)
  }

  @Builder pagesMap(name: string) {
    if (name == 'ChildAView') {
      ChildAView()
    } else if (name == 'ChildBView') {
      ChildBView()
    }
  }
}

@Component
struct ChildAView{...}

@Component
struct ChildBView{...}

Index 组件作为应用的主页面组件,通过@Provide 装饰器声明 pageStack 导航堆栈,用于后续页面之间的导航路径管理。在 UI 交互上,定义两个按钮并绑定了点击事件,分别将 ChildAView与ChildBView路径压入导航栈中,来实现跳转逻辑。

通过@Builder 装饰器定义pagesMap 方法,用于实现页面的动态导航目标构建,其核心逻辑是根据按钮被点击时传入的页面名称返回对应的视图组件实例。

在预览器中,开发者可以预览IndexView页面的呈现样式,如图 7-1 所示。

图 7-1 IndexView 页面预览

当开发者点击Show ChildBView 按钮时,系统将会跳转到ChildBView 页面,如图 7-2 所示。

图 7-2 ChildBView 页面预览

7.2 数据驱动导航

从API Version 10开始,鸿蒙官方不再推荐使用Router模块实现页面路由,转而推荐Navigation组件实现页面间以及组件内部的页面跳转。如果开发者之前使用的是Router模块,那么现在使用Navigation和NavDestination将会更加简单。Navigation不仅能够实现页面之间的简单跳转,还可以由数据驱动来实现定向子视图的导航跳转。

创建一个名为MyModernNavigation的新的 HarmonyOS 项目,并打开工程开发面板。

7.2.1 导入素材准备

将下载好的图片素材拖放media文件夹中,路径为entry/src/main/resources/base/media。如图 7-3 所示。

图 7-3 图片素材预览

7.2.2 实现FlowerModel接口

在 ets 目录下新建一个名为 model 的目录,并新建一个名为FlowerModel的ArkTS文件,在该文件下定义一个名为 FlowerModel的接口,用于描述花朵对象的结构和参数,每个花朵对象都有一个唯一的标识符、花朵名称、图片路径 3 个参数,代码如下:

//第7章/FlowerModel.ets
export interface FlowerModel {
  id: number
  name: string
  image: Resource
}

通过const声明一个FlowerModel 类型的数组 flowerData用于存储初始花朵内容,并创建 4 个静态数据,代码如下:

//第7章/FlowerModel.ets
export const flowerData: FlowerModel[] = [
  {id:1,name:'Tulip',image:$r('app.media.Tulip')},
  {id:2,name:'Sunflower',image:$r('app.media.Sunflower')},
  {id:3,name:'Rose',image:$r('app.media.Rose')},
  {id:4,name:'Lotus',image:$r('app.media.Lotus')}
]

7.2.3 实现FlowerViewModel类

在 ets 目录下新建一个名为 viewmodel 的目录,并新建一个名为FlowerViewModel的ArkTS文件,在该文件下定义一个名为 FlowerViewModel的类,用于管理UI状态和交互逻辑,代码如下:

//第7章/FlowerViewModel.ets
import { flowerData, FlowerModel } from "../model/FlowerModel";

@Observed
export class FlowerViewModel {
  flowers: Array<FlowerModel> = []

  getData() {
    this.flowers = flowerData
  }
}

由于flowerData 和FlowerModel归属于model目录,因此在使用时需要使用import 关键字在 FlowerViewModel.ets 文件中引入相关接口文件。

@Observed 装饰器用于将 FlowerViewModel 类标记为可观察对象,使得类中的属性变化可以被框架自动检测并触发视图更新。

声明FlowerModel 类型的数组flowers,用于存储花朵的数据,初始值为空数组。声明 getData()方法,用于将 flowerData 中的数据赋值给 flowers 数组,从而实现数据的初始加载。

7.2.4 创建FlowerRowView子组件

创建一个子组件FlowerRowView,用于渲染单个花朵列表项视图,并通过FlowerModel类型的 flower参数动态设置花朵的名称和图片来源,代码如下:

//第7章/Index.ets
import { FlowerModel } from '../model/FlowerModel'

@Component
struct FlowerRowView {
  @Prop flower: FlowerModel

  build() {
    Row({space:10}){
      Text(this.flower.name)
        .fontSize(17)
      Blank()
      Image(this.flower.image)
        .objectFit(ImageFit.Cover)
        .width(60)
        .height(60)
      SymbolGlyph($r('sys.symbol.chevron_right'))
        .fontSize(17)
        .fontColor(['#999999'])
    }
    .width('100%')
    .padding(10)
    .backgroundColor('#F5F5F5')
    .borderRadius(16)
  }
}

FlowerRowView 组件使用 @Prop 装饰器定义了 flower 参数,使其可以从父组件接收 FlowerModel 类型的数据,并动态渲染花朵信息。

Text 组件用于显示花朵的名称,Image 组件用于显示花朵的图片,SymbolGlyph 组件用于显示一个向右的箭头符号。子组件使用Row 组件进行布局,并设置了一个浅灰色的背景颜色和 16 度的圆角,使组件整体风格简洁美观。

7.2.5 实现FlowerDetailView子页面

使用@Component 装饰器创建一个自定义子页面FlowerDetailView,并实现FlowerDetailView页面的UI,代码如下:

//第7章/Index.ets
@Component
struct FlowerDetailView {
  @Prop flower: FlowerModel
  @Consume('NavPathStack') pageStack: NavPathStack

  build() {
    NavDestination() {
      Stack() {
        Image(this.flower.image)
          .objectFit(ImageFit.Cover)
          .width('80%')
          .borderRadius(16)
      }
      .width('100%')
      .height('100%')
    }
    .title(this.flower.name)
  }
}

FlowerDetailView 子页面使用@Prop 装饰器定义了 flower 参数,用于从父组件接收 FlowerModel 类型的数据,从而动态展示花朵的详细信息。同时通过 @Consume('NavPathStack') 装饰器创建了NavPathStack 实例,以便能够操作导航路径堆栈,实现页面导航功能。

在 build()中,使用 NavDestination 定义了一个导航目标页面,用于页面跳转后的内容展示。Image 组件用于显示花朵的图片,title 修饰器用于设置页面标题为花朵名称。

7.2.6 实现Flower页面

回到 Index视图,引入FlowerViewModel.ets 文件到 Index 文件中,并实例化FlowerViewModel类,用于管理花朵数据,代码如下:

//第7章/Index.ets
import { FlowerModel } from '../model/FlowerModel'
import { FlowerViewModel } from '../viewmodel/FlowerViewModel'

@Entry
@Component
struct Index {
  @State viewmodel: FlowerViewModel = new FlowerViewModel()

  build() {...}
}

@Component
struct FlowerRowView {...}

@Component
struct FlowerDetailView {...}

将 build()函数中的主体内容替换为 Navigation组件和 List 组件,通过ForEach()函数从viewmodel的flowers数组中获取数据,并将获取的数据传给FlowerRowView组件,从而渲染一个花朵列表视图,代码如下:

//第7章/Index.ets
import { FlowerModel } from '../model/FlowerModel'
import { FlowerViewModel } from '../viewmodel/FlowerViewModel'

@Entry
@Component
struct Index {
  @Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack()
  @State viewmodel: FlowerViewModel = new FlowerViewModel()

  aboutToAppear() {
    this.viewmodel.getData()
  }

  build() {
    Navigation(this.pageStack) {
      List({ space: 10 }) {
        ForEach(this.viewmodel.flowers, (item: FlowerModel) => {
          ListItem() {
            FlowerRowView({flower:item})
              .margin({left:10,right:10})
              .onClick(() => {
                this.pageStack.pushPath({ name: 'FlowerDetailView',param:item })
              })
          }
        })
      }
      .height('100%')
      .width('100%')
    }
    .title('Flower')
    .navDestination(this.pagesMap)
  }

  @Builder pagesMap(name: string,param: FlowerModel) {
    if (name == 'FlowerDetailView') {
      FlowerDetailView({flower:param})
    }
  }
}

@Component
struct FlowerRowView {...}

@Component
struct FlowerDetailView {...}

aboutToAppear()方法实现在Index 页面显示前调用viewmodel 的getData()方法来加载花朵数据,当数据完成后,使用 ForEach 循环渲染 viewmodel.flowers 中的每一项,并传入当前项的 flower 数据渲染FlowerRowView组件,从而实现花朵列表的渲染。

在点击列表项的事件中,调用pushPath()方法导航到花朵详情页的同时,将当前项的 flower 数据作为参数传给FlowerDetailView子页面,来实现数据驱动导航跳转。

在预览器中,开发者可以预览Flower页面的呈现样式,如图 7-4 所示。

图 7-4 Flower页面预览

当开发者点击花朵列表项时,系统将会跳转到FlowerDetailView页面,并将当前项的参数传给FlowerDetailView 页面,如图 7-5 所示。

图 7-5 FlowerDetailView页面预览

7.3 设置菜单项和工具栏

菜单项是在页面右上角显示图标按钮,便于开发者对当前页面设置定义操作,在移动端设计中,竖屏最多支持显示3个图标,横屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。

工具栏是在页面底部显示工具栏内容,常用用于放置页面的操作选项。在移动端设计中,竖屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。

由于菜单项和工具栏都需要使用到symbolIcon 接口,因此需要在页面中导入@kit.ArkUI 模块中的SymbolGlyphModifier工具接口,该接口用于实现调用ArkUI 的symbolIcon组件的能力,实现对 symbol 调用。代码如下:

import { SymbolGlyphModifier } from '@kit.ArkUI'

使用 @State 装饰器声明一个Array类型的状态变量作为对象用于存储菜单项的选项数据,代码如下:

@State menuItems: Array<NavigationMenuItem> = [
  {
    value: '',
    symbolIcon: new SymbolGlyphModifier($r('sys.symbol.plus')),
    action: () => {}
  },
  {
    value: '',
    symbolIcon: new SymbolGlyphModifier($r('sys.symbol.star')),
    action: () => {}
  },
  {
    value: '',
    symbolIcon: new SymbolGlyphModifier($r('sys.symbol.share')),
    action: () => {}
  }
]

每个菜单项都包含value、symbolIcon、action3 个参数,其中value 表示菜单项的文本,当前设置为空。symbolIcon 用于显示菜单项的 icon,可以创建 SymbolGlyphModifier 实例调用 Symbol 的资源来图标。action用于设置当前选项被选中的事件回调。

使用 @State 装饰器声明一个Array 类型的状态变量作为对象用于存储工具栏的选项数据,代码如下:

@State toolItems: Array<ToolbarItem> = [
  {
    value: 'home',
    symbolIcon: new SymbolGlyphModifier($r('sys.symbol.house')),
    action: () => {}
  },
  {
    value: 'mine',
    symbolIcon: new SymbolGlyphModifier($r('sys.symbol.more')),
    action: () => {}
  }
]

每个工具栏都包含value、symbolIcon、action 3个参数,与菜单项规则一致,3 个参数分别用于设置工具栏的文字、icon 和事件回调。

下一步,为Navigation 组件添加menus、toolbarConfiguration 修饰器,用于显示菜单项和工具栏,代码如下:

Navigation(this.pageInfos) {...}
.title('Flower')
.navDestination(this.pagesMap)
.menus(this.menuItems)
.toolbarConfiguration(this.toolItems)

在预览器中,开发者可以预览Flower页面菜单项和工具栏的呈现样式,如图 7-6 所示。

图 7-6 菜单项和工具栏效果预览

7.4 使用Tabs实现视图切换

Tabs 是一种通过页签来切换内容视图的容器组件,当用户点击标签页的按钮时,该按钮关联的页签内容也会随之切换。

创建一个名为MyTabs的新的 HarmonyOS 项目,并打开工程开发面板。

7.4.1 创建tabItem子组件

使用 private 关键字声明一个TabsController类型的状态变量作为控制器来管理 Tabs组件,该控制器用于与页签组件进行绑定,代码如下:

private controller: TabsController = new TabsController()

通过@State 装饰器声明两个number 类型的参数来表示页面当前索引和选中索引,代码如下:

@State currentIndex: number = 0
@State selectedIndex: number = 0

使用@Builder 装饰器创建一个自定义组件tabItem,用于实现单个页签的按钮样式,其中页签按钮由 icon 和文字组成,并将 icon 和文字的内容、颜色通过selectedIndex 参数进行判断,便于实现点击选中的切换效果,代码如下:

//第7章/Index.ets
@Builder tabItem(index: number, name: string,icon:string) {
  Column({space:5}) {
    SymbolGlyph($r(`sys.symbol.${this.selectedIndex === index ? `${icon}_fill` : icon}`))
      .fontSize(17)
      .fontColor([this.selectedIndex === index ? Color.Blue  : Color.Gray])
    Text(name)
      .fontSize(14)
      .fontColor(this.selectedIndex === index ? Color.Blue  : Color.Gray)
  }.width('100%')
}

7.4.2 实现底部标签栏

在 build()函数中,使用Tabs 组件和TabContent 子组件来实现底部选项卡效果,代码如下:

//第7章/Index.ets
@Entry
@Component
struct Index {
  private controller: TabsController = new TabsController()
  @State currentIndex: number = 0
  @State selectedIndex: number = 0

  @Builder tabItem(index: number, name: string,icon:string) {...}

  build() {
    Column(){
      Tabs({
        barPosition: BarPosition.End,
        index: this.currentIndex,
        controller: this.controller
      }) {
        TabContent() {
          Text('首页')
        }
        .tabBar(this.tabItem(0,'首页','house'))

        TabContent() {
          Text('发现')
        }
        .tabBar(this.tabItem(1,'发现','rectangle_on_rectangle'))

        TabContent() {
          Text('收藏')
        }
        .tabBar(this.tabItem(2,'收藏','heart'))

        TabContent() {
          Text('我的')
        }
        .tabBar(this.tabItem(3,'我的','worldclock'))
      }
      .scrollable(false)
      .onChange((index: number) => {
        this.currentIndex = index
        this.selectedIndex = index
      })
    }
    .height('100%')
    .width('100%')
  }
}

Tabs 组件的参数中,barPosition 参数用于设置标签栏的位置,设置其参数值为BarPosition.End,即将标签栏置于底部。index 和controller 分别用于绑定当前选中的标签页索引和控制器。

每个子页面内容通过 TabContent 子组件构建,tabBar修饰器用于配置当前标签对应的标题和图标。图标样式由自定义的 tabItem() 构建器函数生成,支持图标与文字组合,并根据选中状态自动切换图标颜色及样式(如 house 与 house_fill 的切换)。

Tabs 组件还支持 scrollable(false) 修饰器设置关闭或开启标签栏的横向滚动能力。onChange() 用于在用户切换标签时同步更新绑定状态变量,从而实现 UI 与数据状态的联动更新。

在预览器中,开发者可以预览标签栏的切换效果,如图 7-7 所示。

图 7-7 标签栏效果预览

7.5 本章小结

本章围绕多页面导航与视图切换展开,系统介绍了如何在 ArkUI 中构建具有良好结构和交互体验的多页面应用。首先实现了多个子页面的创建与跳转逻辑,明确了页面之间的导航关系。随后通过引入 ViewModel 和数据接口,将数据驱动与页面展示相结合,提升了页面的动态响应能力和可维护性。

在交互层面,通过自定义菜单项和工具栏,可以为页面添加了基础的操作功能。特别是在底部导航栏的实现部分,借助 Tabs 组件和自定义 tabItem 子组件,可以实现图标与标题样式的动态切换。

通过本章的学习,开发者可以掌握 ArkUI 多页面开发的核心能力,包括页面结构设计、数据与视图的解耦、以及导航组件的实际应用。这些能力将为开发复杂场景下的多页面应用提供坚实的基础,帮助开发者在项目实践中更高效地完成页面组织与交互设计。