围绕题目“HarmonyOS7 PickerSwitcherPanel 教程:一个页面切换三种选择器

前言

把不同选择器放进一个切换面板后,条件渲染、结果管理和模块分组就都出现了。 真正上手时,难点往往不在组件本身,而在你要不要把它写得像个真实页面。 我不会按属性表的顺序平铺,而是直接挑最影响使用体验的部分来讲。

这篇我会重点从 组件封装角度 这个角度往下讲。不是为了把每个属性都讲一遍,而是帮你建立一个更接近真实开发的阅读顺序:先看页面目标,再看状态流,最后再看样式怎么收口。

这个页面适合放进什么场景

这类示例最怕读成“API 列表”,所以我会直接从页面行为往回拆。

“选择器综合”这种能力,单独演示时很轻,落到项目里却往往要和表单、列表、卡片或者反馈区绑在一起,这也是这个案例值得细看的原因。

这一类写法常见在 筛选面板、报名表单、地址选择、分类联动 这些页面里。你只要把示例里的交互换成自己的业务文案,再把静态数据换成真实数据源,骨架基本就够用了。

示例读不明白时,最好的办法不是多看几遍属性表,而是先把页面里的角色关系搞明白。

根据文章“先看界面层次”这一段,画一张手绘框架图,主题是 HarmonyOS7 PickerSwit

如果你后面要把示例改成线上页面,别急着先美化,先确认 状态字段和展示结果有没有保持同一份数据来源,避免页面看起来“能动”但结果不可信。

先看界面层次

页面最外层通常先用 Column 把主轴定下来,这一步看起来普通,但它决定了后面所有内容是顺着读,还是碎着看。

如果代码里出现了 Scroll,那基本说明作者已经预判到内容会超过一屏。这种处理很实在,至少不会等页面做长了再返工。

这一页更多是纵向阅读逻辑,说明作者希望用户按顺序完成操作,而不是在多个区域来回跳。

第 36 篇案例在布局上最值得学的地方,是它没有为了展示组件能力去强行堆内容,而是尽量让每一块区域都有明确职责。这样的代码后面更好拆组件。

完整代码

下面这份代码保留了案例本身的实现思路,但把示例编号替换成了更自然的英文命名。你直接拿去做实验、拆段运行,阅读成本会低很多。

把文章中的交互逻辑画成一张手绘流程图,主题为 HarmonyOS7 PickerSwitcherPa

/**
 * PickerSwitcherPanel - 选择器综合
 */
import { PRESET_COLORS, generateListItems, PAGE_BG_COLOR, PANEL_BG_COLOR, ACCENT_COLOR, MUTED_TEXT_COLOR } from './types'

@Entry
@Component
struct PickerSwitcherPanel {
  @State isShow: boolean = true
  @State typeIndex: number = 0
  @State dateResult: string = ''
  @State timeResult: string = ''
  @State textResult: string = ''
  private pickerTypes: string[] = ['DatePicker 日期', 'TimePicker 时间', 'TextPicker 文本']

  build() {
    Column() {
      if (this.isShow) {
        Scroll() {
          Column({ space: 16 }) {
            /* 选择器类型切换 */
            Column() {
              Text('选择器类型').fontSize(14).fontColor(ACCENT_COLOR).margin({ bottom: 8 })
              TextPicker({ range: this.pickerTypes, selected: this.typeIndex })
                .onChange((value: string | string[], index: number | number[]) => {
                  this.typeIndex = index as number
                })
            }
            .backgroundColor(PANEL_BG_COLOR).padding(12).borderRadius(12)

            /* 根据类型显示对应选择器 */
            if (this.typeIndex === 0) {
              Column() {
                Text('DatePicker').fontSize(14).fontColor('#4D96FF').margin({ bottom: 8 })
                DatePicker({ start: new Date(2020, 0, 1), end: new Date(2030, 11, 31), selected: new Date() })
                  .onChange((value: DatePickerResult) => {
                    this.dateResult = (value.year ?? 0) + '/' + ((value.month ?? 0) + 1) + '/' + (value.day ?? 0)
                  })
              }.backgroundColor('#E8F4FD').padding(16).borderRadius(12)
            } else if (this.typeIndex === 1) {
              Column() {
                Text('TimePicker').fontSize(14).fontColor('#6BCB77').margin({ bottom: 8 })
                TimePicker({ selected: new Date() })
                  .onChange((value: TimePickerResult) => {
                    this.timeResult = (value.hour ?? 0) + ':' + (value.minute ?? 0)
                  })
              }.backgroundColor('#F0FAF1').padding(16).borderRadius(12)
            } else {
              Column() {
                Text('TextPicker').fontSize(14).fontColor('#9B59B6').margin({ bottom: 8 })
                TextPicker({ range: ['选项A', '选项B', '选项C', '选项D'], selected: 0 })
                  .onChange((value: string | string[], index: number | number[]) => {
                    this.textResult = value as string
                  })
              }.backgroundColor('#F5EEF8').padding(16).borderRadius(12)
            }

            /* 结果汇总 */
            Column({ space: 8 }) {
              Text('DatePicker结果: ' + this.dateResult).fontSize(13).fontColor('#4D96FF')
              Text('TimePicker结果: ' + this.timeResult).fontSize(13).fontColor('#6BCB77')
              Text('TextPicker结果: ' + this.textResult).fontSize(13).fontColor('#9B59B6')
            }
            .width('100%').backgroundColor(PANEL_BG_COLOR).padding(14).borderRadius(12)
          }.width('100%')
        }
      }
      Text('选择器综合 - TextPicker+DatePicker+TimePicker')
        .fontSize(12).fontColor('#999999').margin({ top: 12 })
    }
    .width('100%').height('100%').backgroundColor('#F5F6FA').padding(16)
  }
}

代码里最该先看的部分

第一处关键代码

@State isShow: boolean = true

这一行先别急着略过,它通常就是页面状态的起点。后面很多显示内容,都会跟着它一起变化。 顺着它往下看,你通常能更快找到页面真正的主逻辑。

第二处关键代码

@State typeIndex: number = 0

看起来只是一个字段定义,但页面后面能不能“动起来”,很多时候就靠它。 顺着它往下看,你通常能更快找到页面真正的主逻辑。

第三处关键代码

@State dateResult: string = ''

这种状态声明的价值,不在写法本身,而在于它决定了谁来保存结果、谁来触发刷新。 顺着它往下看,你通常能更快找到页面真正的主逻辑。

从案例走到业务页面

很多人第一次读示例会从头到尾扫一遍,但更高效的做法,其实是先看下面这些触发点:

  • .onChange((value: string | string[], index: number | number[]) => {
  • .onChange((value: DatePickerResult) => {
  • .onChange((value: TimePickerResult) => {
  • .onChange((value: string | string[], index: number | number[]) => {

对大多数表单和选择类页面来说,交互的质量基本取决于回调有没有把结果及时、清楚地反馈出来。

读到交互段时,不妨多问一句:这个结果有没有唯一数据源?很多页面后面难维护,就是从这里埋下的。

一个很实用的改法,是尽早把状态分层:谁负责输入,谁负责展示,谁负责兜底。只要这一步做清楚,后面页面再长,build() 也不至于越来越乱。

再往前走一步

真要把这个案例拿去改业务页面,我会按下面这个顺序动手:

  • 把演示用的静态数据替换成真实数据源,别等接口接进来以后再改页面结构。
  • 把重复出现的卡片、标题栏、结果区提成小组件,后面加状态会轻松很多。
  • 提前补上异常态,比如空值、失败态、禁用态、超范围输入,否则示例一进业务就会露怯。
  • 如果是综合选择器,我会优先拆模块,把不同选择区的状态边界切干净,别让一个页面状态互相串。

这里最容易被忽略的一点,是 状态字段和展示结果有没有保持同一份数据来源,避免页面看起来“能动”但结果不可信。 这个问题。很多示例在静态数据下看不出毛病,一接真实状态就开始暴露问题,所以这一步最好趁早做。

容易被忽略的小地方

有些细节在第一眼看代码时很容易被跳过去,但它们往往决定了页面是不是耐改。

  • @State 不是装饰器样板,它决定了哪些数据变化后会重新驱动画面。
  • 私有数组或字段一般在承担“页面配置”的角色,比如选项范围、颜色映射、联动源数据。
  • 对于 Picker 类页面,我会特别留意文案反馈是不是跟着用户动作一起变化,因为这直接影响页面有没有“会说话”的感觉。

这些东西单看都不复杂,组合在一起,才是一个案例真正的完成度。

写在最后

这类页面往往不是功能难,而是容易写得太像示例。只要你把状态、反馈和视觉层次这三件事捏紧,页面质感就会立刻上来。

先把数据从哪来、结果往哪去想清楚,再去调样式,返工会少很多。

Logo

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

更多推荐