鸿蒙纪·梦始卷#04 | 猜数字 -需求与静态界面

622 阅读8分钟

《鸿蒙纪元》张风捷特烈 计划打造的一套 HarmonyOS 开发系列教程合集。致力于创作优质的鸿蒙原生学习资源,帮助开发者进入纯血鸿蒙的开发之中。本系列的所有代码将开源在 HarmonyUnit 项目中:

github: github.com/toly1994328…
gitee: gitee.com/toly1994328…


本文是《鸿蒙纪·梦始卷》 的第四章,上一篇我们基于计数器的小案例,继续优化界面表现。了解资源文件的使用、学会沉浸状态导航栏,实现全屏布局的方案。


接下来,我们将通过几个有趣的小案例,沿着我的免费小册 《Flutter 入门教程》 的脚步,踏上初入鸿蒙应用开发的取经之路。第一个小功能是 猜数字,本文将会学到:

  • [1]. 如何将代码拆分成多个文件维护。
  • [2]. 了解猜数字的交互功能,与分析需求。
  • [3]. 构建猜数字的静态界面。

一、多文件拆分

随着应用中功能的不断增加,如何组织代码结构是一个非常重要的课题。把所有代码都塞入一个文件中是不可取的。我们应该根据 功能需求 和 逻辑复用 情况,来综合考虑代码的组织。首先我们应该学会,一个项目中,多个文件之间该如何关联起来,共同服务于项目。


1. 拆分计数器代码

之前计数器的所有代码都放在 Index.ets 中,会让入口代码显得冗长。根据功能需求,计数器是一个独立的功能;另外其中的 AppBar 组件其他界面可以复用。所以可以进行如下调节:

  • 创建 components 文件夹,盛放打算复用的封装组件,将 AppBar 组件单独分文件维护;
  • 在 pages 文件夹下,创建 CounterPage 维护计数器的功能需求。


2. 文件的导入与导出

此时 Index 组件中,就可以引入 CounterPage 实现构建逻辑。另外底部导航的高度避让,属于全应用的事,可以在 Index 中处理。也减轻了 CounterPage 的负担,这就是 明确功能需求的职责 ,谁该做什么事,就能够更专注地维护。这些在代码编写前就应该梳理清楚。
导入其他文件中的组件,可以使用 import 关键字,被导入的组件需要通过 export 导出:

import { CounterPage } from "./CounterPage";

@Entry
@Component
struct Index {
  
  @StorageProp('bottomRectHeight')
  bottomRectHeight: number = 0;

  build() {
    Column() {
      CounterPage()
    }.padding({ bottom: px2vp(this.bottomRectHeight) })

  }
}

文件拆分完后,这里提交一个小里程碑:计数器-v6-拆分文件维护。如果 CounterPage 功能需求界面比较复杂,也可以对 CounterPage 的代码进一步拆分。比如创建一个 counter 文件夹,然后界面中划分组件块交给每个文件维护。但拆分虽好,可不要贪杯哦 ~


二、猜数字界面交互与界面准备

猜数字是鸿蒙纪元的一个案例,功能比较简单,非常适合新手朋友入门学习。该需求中包含的知识点包括:

  • 随机数的生成
  • 输入框的使用
  • 界面构建的练习
  • 逻辑控制的练习
  • 动画的简单使用

1. 界面交互介绍

下面是两个最基础的交互:

  • 点击按钮生成 0~99 的随机数,并将随机数密文隐藏。
  • 头部的输入框,点击时弹出软键盘,可输入猜测的数字。
点击生成随机数可输入文字

如下所示,点击右上角的运行按钮,可以比较输入猜测值和生成值的大小,并在界面上通过两个色块进行提示。每次比较时,提示面板中的文字会有动画的变化,给出交互示意。

比较结果:小了比较结果:大了

这三个交互就是本案例的所有功能需求。你可以找几个朋友一起玩这个猜数字的小游戏,比如随机生成一个数后,每人输入一个数,最后猜中的人获取胜利。其中控制猜测的范围,使其更利于自己猜出结果,也是一点斗智斗勇。


2. 增加猜数字界面

现在可以增加一个 GuessingPage.ets 的代码负责维护猜数字的功能:

---->[GuessingPage.ets]----
@Component
export struct GuessingPage {
    build() {
       //TODO 构建界面
    }
}

目前暂时还没有接触导航,可以在入口中将 CounterPage 注释一下,展示 GuessingPage

---->[Index.ets]----
import { GuessingPage } from './GuessingPage';

@Entry
@Component
struct Index {
  @StorageProp('bottomRectHeight')
  bottomRectHeight: number = 0;

  build() {
    Column() {
      // CounterPage()
      GuessingPage()
    }.padding({ bottom: px2vp(this.bottomRectHeight) })

  }
}

3. 静态界面布局

在布局上,猜数字在结构上和计数器非常类似,都是 上中下 结果,如下所示:

  • 顶部栏的标题需要缓成搜索框,右侧的按钮用于确认提交;
  • 中间区域展示信息,当生成随机数后,需要展示密文;
  • 底部的按钮在开始时可以点击生成随机数,猜数字过程中需要被禁用;
开始时生成随机数字

当点击运行时,会检测当前输入和目标值的大小关系。并以红色和蓝色的区域提示用户。其中红蓝各占高度的一半;文字展示在色块中间。在布局上,红蓝色块可以叠放在底层,根据猜测的状态值决定展示效果:

小了大了

三、静态界面构建

功能和布局分析完了,接下来就进行实际的代码编写吧。本文只会完成基本的静态界面布局,具体的交互逻辑和界面状态的变化,将在下一篇中介绍。


1. 头部栏 AppBar 优化

在当前需求中,头部栏的标题是自定义的组件。而我们之前封装的 AppBar 标题只能是文字。其实代码并不需要在一开始就尽善尽美,随着功能需求的增加,可以逐步迭代封装的组件,使之更具有通用价值。现在可以将 AppBar 中间内容的也通过插槽的方式,交由外界提供:

如下所示,增加 titleSlot 构造器,其中 titleBuilder 可以指定默认的展示组件;这样也不会影响之前的 title 属性:

@Component
export struct AppBar {
  /// 略同...

  @Builder
  titleBuilder() {
    Text(this.title)
      .fontSize($r('app.float.app_bar_title_size')).fontWeight(FontWeight.Bold)
      .fontColor($r('sys.color.white'))
  }

  @BuilderParam titleSlot: () => void = this.titleBuilder;

此时在 AppBar 中的 titleSlot 参数中传入 titleInput 构造器,使用 TextInput 展示输入框。layoutWeight 方法可以设置占位比重,让输入框的宽度延展剩余区域:

@Builder
titleInput() {
  TextInput({ placeholder: '输入 0~99 数字' })
    .layoutWeight(1)
    .margin({ left: 8, right: 8 })
    .backgroundColor('#F3F6F9')
}

2. 状态量与按钮、文字构建

按钮的状态想要在猜测状态数变为不可点击。所以需要一个状态量来记录输入,如下的 guessing 布尔值,在构建过程中,通过 Button#enabled 设置按钮是否可用;使用 guessing 状态设置按钮的背景色:

按钮可用按钮不可用
@State guessing: boolean = true;

@Builder
button() {
  Button({ type: ButtonType.Circle, stateEffect: true }) {
    SymbolGlyph($r('sys.symbol.scope'))
      .fontSize(24)
      .fontColor([Color.White])
      .fontWeight(FontWeight.Bold)
  }
  .width(56)
  .height(56)
  .margin({ right: 20, bottom: 16 })
  .backgroundColor(this.guessing ? '#9e9e9c' : $r('app.color.theme_color'))
  .enabled(!this.guessing)
  .onClick(() => this.gen())
}

同理,中间区域的内容也可以通过 guessing 状态来控制构建的逻辑。在构建中可以通过 if(flag) A else B 来根据 flag 决定展示 A 视图还是 B 视图;这里提取 buildCounterDisplay 方法来构建中间区域内容;如果构建逻辑比较复杂,也可以拆分成组件单独维护。

开始生成随机数后
@Builder
buildCounterDisplay(){
  if(!this.guessing)
  Column() {
    Text('点击生成随机数')
    Text('0').fontSize(46).fontColor('#727272')
  }.width('100%').height('100%')
  .justifyContent(FlexAlign.Center)
  else
  Column() {
    Text('开始输入猜数字吧~')
    Text('**').fontSize(46).fontColor('#727272')
  }.width('100%').height('100%')
  .justifyContent(FlexAlign.Center)
}

3. 结果展示区域构建

仔细观察需求的界面,大了、小了的布局特性是一致的。只不过颜色和文字不同,我们可以拆分出一个 ResultDisplay 组件来展示它。而不是类似的代码在 build 中复制粘贴两遍。结果展示所依赖的数据,这里提炼出 CheckResult 枚举,更便于使用:

@Component
struct ResultDisplay {
  private result: CheckResult = CheckResult.none;

  build() {
    Column() {
      Text(resultLabel(this.result))
        .fontSize(24)
        .fontColor(Color.White)
        .fontWeight(FontWeight.Bold)
    }
    .layoutWeight(1)
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor(resultColor(this.result))
  }
}

CheckResult 表示比较的结果,一开始为 none ,比较后会有大了、小了、相等三个结果。另外,通过了 resultLabelresultColor 提供枚举对应的文字和颜色:

enum CheckResult {
  none,
  bigger,
  smaller,
  equal,
}

function resultLabel(result: CheckResult): string | Resource {
  switch (result) {
    case CheckResult.bigger:
      return '大了';
    case CheckResult.smaller:
      return '小了';
  }
  return '';
}

function resultColor(result: CheckResult): ResourceColor {
  switch (result) {
    case CheckResult.bigger:
      return '#ff5454';
    case CheckResult.smaller:
      return '#448afc';
  }
  return '';
}

最后,将两个 ResultDisplay 叠放在主界面之下,即可实现期望的静态布局效果:

Stack() {
  Column() {
    ResultDisplay({ result: CheckResult.bigger })
    ResultDisplay({ result: CheckResult.none })
  }
  this.buildCounterDisplay()
  this.button()
}.layoutWeight(1)

这里提交一个小里程碑:计数器-v7-猜数字-静态界面


尾声

到这里,我们就完成了猜数字的静态界面布局。下一篇,将继续完善猜数字功能,实现交互与静态状态的变化。我们下次再见~

更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。