鸿蒙开发第一篇:创建一个应用,使用Tabs和导航栏结构。

1,453 阅读10分钟

第一篇:创建一个应用,使用Tabs和导航栏结构。

背景:鸿蒙0基础探索。

愿景:本系列旨在用简单直接的方式从让鸿蒙app先从0-1。

理由:有产出更有继续下去的动力。

记录:每篇文章保留下探索的过程。

PS:  不教写代码,不封装,业务代码能力因人而异。(关键我不会啊😁)

第一篇:创建一个应用,使用Tabs和导航栏结构

第二篇:使用网格Grid与列表list

第三篇:接入第三方库,使用Axios网络请求,PullToRefresh上拉刷新下拉加载

第四篇:整理工程 - 模块化

第五篇:探索打包提审上架

探索过程:

因为我是iOS开发,新建应用后的第一件事,我就想知道 应用用常见的UITabbar,在鸿蒙中该用什么。 

立马我就问了AI:”在arkTS中,使用类似于iOS UITabbar的功能是什么组件”

回答:“在arkTS中,类似于iOS UITabbar的功能可以通过使用TabBar组件来实现。您可以在页面中添加TabBar组件,然后通过TabBar的items属性来设置TabBar的每一项。每一项可以是一个Button组件,您可以通过Button的text属性来设置TabBar的每一项的标题。”

对不对不知道,先试试他给的TabBar关键词,转头就去官方文档搜索关键词(真心没刨过文档,还是搜索适合我)

搜索

Pasted Graphic.png

很幸运第一个结果我就发现了一个宝藏资料(这篇文章真实用,手动点赞),:

典型布局场景: 里面有页签栏,导航栏,banner图,侧边栏,三分栏,网络等等非常常用的示例。

OS:如果按照正常把文档刷一遍再开始。我怕我还没刷到这篇藏在这么下方的链接,就已经从入门到放弃了😭。

页签栏: Tab组件 ,就是我想要的。

单/双栏: Navigation组件 类似于导航栏

虽然ai的回答没那么准确,提供了关键字,还是顺利帮我们完成了。撸起袖子开干

编译器下载, 创建应用,启动模拟器

鸿蒙编译器下载链接 Beta3测试版本,跑模拟器需要登录华为账号,申请模拟器只要填个名字就行,1小时内就审核过了。(之前想申请个模拟器还要考试来着)。 我的环境是:DevEco Studio NEXT Developer Beta3(5.0.3.600) 支持API12。

创建应用 Snipaste_2024-08-17_14-09-25.png

安装模拟器 Snipaste_2024-08-17_14-17-47.png 不得不夸一下这个模拟器,明显感觉很丝滑。 ps:图上忘记标了。第4步骤运行之前,要先启动模拟器。Device-Manager里 Action下面那个红色框框要点一下才启动模拟器。

使用Tabs组件(后面发现这更像是翻页的组件,和iOS的UITabbar还是不一样的),官方文档链接

很简单对不对,里面代码能看懂多少看多少,看不懂的部分没有关系。务必相信自己,随着开展业务代表,这些自然就会了

先附上个结果:

代码复制到工程的index.ets里,运行。- - 不出意外的话,意外果然出来了。

错误1: 找不到breakpointsystem文件

breakpointsystem是什么,我没有common文件夹啊,你这类哪里来的,这样给示例不是坑吗。

这时候有两种选择,一个是删除掉breakpointsystem的引用。

当我看见sm,md,lg这样的字符,感觉有点像small,medium,large的缩写,难道breakpointsystem是适配机型尺寸用的,那这好赖也不能删吧(总不能当玩具只写Hello Word吧),犹豫不决就去文档搜索碰碰运气。 搜素发现了这个响应式布局里面有breakpointsystem.ets文件

概念:当前系统提供了如下三种响应式布局能力断点, ps: 他这里的断点,和我以为的开发调试的打断点,不是一个概念。这里看起来是读取设备尺寸用的。

导入 - 皆大欢喜?- 看进度条发现事情并不简单 - 冒出后续2,3,4,5个问题。

问题2 Constructor of class 'BreakpointSystem' is private and only accessible within the class declaration.

private breakpointSystem: BreakpointSystem = BreakpointSystem.getInstance()

问题3 Property 'register', 'unregister' does not exist on type 'BreakpointSystem'.

分别替换register,unregister.
this.breakpointSystem.start()
this.breakpointSystem.stop()

问题4 '"../common/breakpointsystem"' has no exported member named 'BreakPointType'. Did you mean 'BreakpointType'?

最坑的来了。他里面还定义一个 BreakpointType,注意看大小写。 很明显,在响应式布局中复制进来的封装代码和典型布局场景中所引用的代码不是一个版本,所需要的入参,初始化啥是不要的。

在breakpointsystem.est中加入下面代码。 请务必多备几个AI工具, 因为下面的代码我是让fittencode生成的,不是一步到位,把每次编译器的报错喂给他,让他修改。ps:这泛型代码,真是在文档示例中不给一下,这不是为难我们探索者吗,这点真要批评一下

interface BreakpointConfig<T> {
  sm: T;
  md: T;
  lg: T;
}

export class BreakPointType<T> {
  private breakpoints: BreakpointConfig<T>;

  constructor(breakpoints: BreakpointConfig<T>) {
    this.breakpoints = breakpoints;
  }

  getValue(currentBreakpoint: string): T | undefined {
    if (currentBreakpoint === 'sm') {
      return this.breakpoints.sm;
    } else if (currentBreakpoint === 'md') {
      return this.breakpoints.md;
    } else if (currentBreakpoint === 'lg') {
      return this.breakpoints.lg;
    } else {
      return undefined;
    }
  }
}

打个比方,我是这样问出来的。

“这是ArkTs的代码,请你推演出BreakPointType的定义,看起来似乎还是泛型参数,他该怎么定义。”
“Indexed signatures are not supported (arkts-no-indexed-signatures) 这是什么意思”
“Object literals cannot be used as type declarations (arkts-no-obj-literals-as-types) 这又是什么意思”
“getValu方法 Indexed access is not supported for fields (arkts-no-props-by-index)”

使用Navigation组件,作为导航栏

本来以为很简单,在这里遇到了一头包。

遇到问题的同时也让我自然而然了解到了很多概念点。最终选择了 探索四 的方式。

  • 布局分析工具 强烈建议初学者一上来就使用,无论是自己看结构,还是看别人的demo,都帮助极大

  • Navigation:一般作为Page页面的根容器。 什么意思呢:你要一个自己的做主的导航栏,那就使用Navigation包裹你的容器

// 导航栈,用来保存记录页面的容器。
private navPathStack: NavPathStack = new NavPathStack()

build() {
   Navigation(this.navPathStack) {
       Column() {
       }
   }
   // 导航栏的模式,单页面 / 分栏. 适配设备用的。
   .mode(NavigationMode.Auto)
   // 隐藏了标题栏。
   // .hideTitleBar(true)
}
  • NavDestination: 作为NavRouter组件的子组件,用于显示导航内容区。 什么意思呢。你如果是被push出来的二级页面,就用NavDestination包裹你的容器。
build() {
    NavDestination() {
      Column() {
      }
    }
    .title("我是HomeDetail的NavDestination")
}

路由:可以看到Router的页面跳转是忽略导航的。 0000000000011111111.20240821120900.46397073328188244299670815009616.png

导航: 标题栏 + 菜单栏 + 内容 + 工具栏 0000000000011111111.20240821120858.13513785761683984419355894467766.png

探索一:Tabs根容器 + 每个容器内单独的Navigation (以iOS的开发经验先入为主了)
  • Tabs根容器 + 每个容器内单独的Navigation。(以iOS的开发经验先入为主了) 遇到的问题: tabs的标签,我在页面push跳转的时候不知道怎么隐藏,望大佬指导下。比如在iOS有hidesBottomBarWhenPushed来隐藏。于是乎我放弃了这种方案。
探索二:Tabs根容器 + 使用Router进行页面跳转。
  • 这个方案是可行,有点像iOS里present一个全屏的控制器,那就得自定义导航栏样式。
  • 然后官方文档还挂个[不推荐], 再加上希望使用导航栏的能力,选择继续尝试其他方案。 Router的文档链接
探索三: 直接使用Navigation + toolbarConfiguration的配置。
  • 在探索过程中,发现Navigation可以设置"工具栏",还能有点击事件,那直接用它是不是就可以达成目的了。 遇到的问题: 那这样我在点击按钮的时候需要切换布局,这样好像我的应用只有一个主界面,点击其他item的时候,会销毁掉旧的,创建新的。我又又又放弃了
探索四: 使用Navigation作为根容器,Tabs放到根容器下面。
  • 做一个全局的导航栏。每个需要跳转的页面去获取导航栈。
  • 获取导航栏提供了四种方式,我用了用二三,挺好用。文档链接
    1. 方式一:通过@Provide和@Consume传递给子页面(有耦合,不推荐)
    2. 方式二:子页面通过OnReady回调获取。
    3. 方式三:通过全局的AppStorage接口设置获取。
    4. 方式四:通过自定义组件查询接口获取。

代码示例:根容器的Navigation隐藏掉.hideTitleBar(true)。 然后比如MinePage页面需要使用导航栏功能的话,在MinePage中用NavDestination() {} 去包装。

interface TabBar  {
  name: string
  icon: Resource
  selectIcon: Resource
}

@Entry
@Component
struct Index {
  @State currentIndex: number = 0
  @State tabs: Array<TabBar> = [{
    name: '首页',
    icon: $r('app.media.startIcon'),
    selectIcon: $r('app.media.startIcon')
  }, {
    name: '我的',
    icon: $r('app.media.startIcon'),
    selectIcon: $r('app.media.startIcon')
  }]

  // 导航栈,用来保存记录页面的容器。
  private navPathStack: NavPathStack = new NavPathStack()

  aboutToAppear() {
    // 全局设置一个NavPathStack
    AppStorage.setOrCreate("yxb_navPathStack", this.navPathStack)
  }

  build() {
    Navigation(this.navPathStack) {
      // 底部:BarPosition.End
      Tabs( {barPosition: BarPosition.End} ) {
        ForEach(this.tabs, (item: TabBar, index) => {
          TabContent() {
            // 内容换成自己的组件。
            if (index == 0) {
              HomePage().width('100%').height('100%')
            } else {
              MinePage().width('100%').height('100%')
            }
          }
          .tabBar(this.tabBarBuilder(index!, item)) 
        })
      }
      // 禁止了tabs的滑动,只能点选。
      // .scrollable(false)
      .vertical(false)
      .barWidth('100%')
      .barHeight('72vp')
      .animationDuration(0)
      .onChange((index: number) => {
        this.currentIndex = index
      })
    }
    // 导航栏的模式,单页面 / 分栏. 适配设备用的。
    .mode(NavigationMode.Auto)
    // 隐藏了标题栏。
    .hideTitleBar(true)
  }
  
  @Builder tabBarBuilder(index: number, tabBar: TabBar) {
    Flex({direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}) {
      Image(this.currentIndex === index ? tabBar.selectIcon : tabBar.icon)
        .size({ width: 36, height: 36 })
      Text(tabBar.name)
        .fontColor(this.currentIndex === index ? '#FF1948' : '#999')
        .fontSize(16)
        .margin({top: 4})
    }
    .width('100%')
    .height('100%')
  }
}

使用push跳转页面 + 正向参数传递 + 返回参数传递

我看的文档应该是提供了系统路由表,自定义路由表两种。

自定义路由表

文档链接文档写的有点复杂,高级用法我还看不懂,这个示例是我理解的

// 导入自己定义的页面
import { HomePage } from './HomePage'

@Entry
@Component
struct Index {
  // 导航栈,用来保存记录页面的容器。
  private navPathStack: NavPathStack = new NavPathStack()
  build() {
    Navigation(this.navPathStack) {
      Column() {
        Button("自定义路由跳转")
          .onClick(() => {
            this.navPathStack.pushPath({name: "HomePage"})
          })
      }
    }
    .title("我是导航栏")
    .navDestination(this.homePageBuilder)
  }

  @Builder
  homePageBuilder() {
    HomePage()
  }
}

// 这相当于二级页面 注意在HomePage页面中,相当于子页面组件,**需要用NavDestination() {}包裹容器**
@Component
export struct HomeDetail {
  build() {
    NavDestination() {
      Column() {
        Text("随你吧。")
      }
    }
  }
}
系统路由表(一定得掌握,不然有点demo都看不懂人家是咋跳的)
// 简略版示意。
jumpMineDetailAction(): void {
  // 跳转到详情, 并且传了一个键值对的参数过去。
  // 把这些参数和类型都单独写出来,是为了好理解:入参需要什么。
  let param: string[] = ["喷火龙", "杰尼龟", "妙蛙种子"]
  // onPop是表示在MineDetail被pop返回的时候,会被触发的。相当于反向传参的。
  let detailPage: NavPathInfo = {name: "MineDetail", param: param, onPop: (popInfo: PopInfo) => {
    this.receiveOnPopAction(popInfo)
  }}
  this.navPathStack.pushPath(detailPage)
}

以this.navPathStack.pushPath(detailPage)为例,让我们看看他是通过字符串怎么跳转到目标控制器的。- 文档:系统路由表,希望你先看看这个文档

步骤, 图文结合看:

    • 通过push时,标明的name: "MineDetail" ->
    • module.json5里找routerMap对应的文件 ->
    • route_map.json找name为MineDetail的配置 ->
    • 通过配置里pageSourceFile找到文件地址 ->
    • 到对应文件执行buildFunction里所标的方法 ->
    • 要以@Builder修饰,里面对组件进行了初始化。

Snipaste_2024-08-21_16-38-53.png

看看效果

效果.gif

最后附上demo地址,把用得上几个文件拷进自己新建的工程就行。别忘了module.json5里加

"routerMap": "$profile:route_map",。 还有route_map.json文件别忘了。