鸿蒙NEXT场景化开发:制作复杂表单

98 阅读14分钟

表单是获取用户输入信息的关键界面,一个精心设计的表单,能够有效减少用户在填写过程中的困惑和错误,提升用户体验。本章将深入探讨如何利用Flex组件的灵活性和强大功能,来构建复杂且高效的表单用户界面。

5.1 实现弹性布局

Flex 组件以弹性方式布局子组件的容器组件,能够根据容器的大小和子元素的属性,灵活地调整子元素的排列方式和尺寸。Flex 组件常用于构建动态的用户界面,尤其是在需要根据屏幕尺寸或内容变化自动调整布局的场景中。

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

5.1.1 创建输入框子组件

创建一个子组件CustomTextInputView,用于渲染输入框组件,并通过text、title、placeholder、inputType 参数动态设置输入框的输入文本、标题、提示文本、输入类型,代码如下:

//第5章/Index.ets
@Component
struct CustomTextInputView {
  @Link text: string
  @Prop title: string
  @Prop placeholder: string
  @Prop inputType: InputType

  build() {
    Column({ space: 10 }) {
      Text(this.title)
        .fontWeight(FontWeight.Bold)
        .margin({ left: 5 })
      TextInput({ text: this.text, placeholder: this.placeholder })
        .type(this.inputType)
        .onChange((value: string) => {
          this.text = value
        })
    }
    .alignItems(HorizontalAlign.Start)
  }
}

CustomTextInputView子组件中,text 参数使用@Link装饰器进行装饰,用于实现双向数据绑定,确保输入框的值能够实时更新并同步到组件的内部状态中。title、placeholder和inputType使用@Prop装饰器进行装饰,使这些属性支持从父级视图传递参数值,从而可以根据不同的需求渲染出具有不同标题、占位符和输入类型的文本输入框。

在构建 UI 时,CustomTextInputView子组件组件通过Column布局组件将标题文本和输入框垂直排列,标题文本设置为加粗并添加了左侧边距,输入框则绑定了onChange事件来更新绑定的text值,并且整个布局设置为靠左对齐,以实现清晰且功能完善的表单输入单元。

5.1.2 实现姓名输入框

声明string类型的状态变量firstName、lastName,分别用于存储用户输入的名字和姓氏,代码如下:

@State firstName: string = ''
@State lastName: string = ''

在build方法中使用Flex组件作为页面容器,并使用CustomTextInputView子组件来构建姓名输入框,代码如下:

//第5章/Index.ets
import { LengthMetrics } from '@kit.ArkUI'

@Entry
@Component
struct Index {
  @State firstName: string = ''
  @State lastName: string = ''

  build() {
    Flex({
      direction: FlexDirection.Row,
      space: { main: LengthMetrics.px(40), cross: LengthMetrics.px(40) }
    }) {
      CustomTextInputView({
        text: this.firstName,
        title: 'First Name',
        placeholder: 'Please Input',
        inputType: InputType.Normal
      })
      CustomTextInputView({
        text: this.lastName,
        title: 'Last Name',
        placeholder: 'Please Input',
        inputType: InputType.Normal
      })
    }
  }
}

@Component
struct CustomTextInputView {...}

Flex组件中,设置direction属性为FlexDirection.Row,表示容器中的子组件按照水平方向排列,通过space属性设置主轴和交叉轴的间距为40像素,确保输入框之间有足够的间隔,避免内容过于拥挤。由于LengthMetrics 接口能力需要使用@kit.ArkUI 库,因此还需要在 Index.ets 中使用import导入@kit.ArkUI 库。

使用CustomTextInputView 子组件作为Flex组件的子组件,CustomTextInputView组件分别绑定了firstName和lastName,同时设置不同标题、占位符和输入类型等参数值,用于显示不同的 UI。

打开预览器,开发者可以预览姓名输入框的效果,如图 5-1 所示。

图 5-1 姓名输入框预览

5.1.3 实现联系方式输入框

联系方式栏目常常要求用户以区号+手机号格式进行填写,其中区号部分,在交互上会采用下拉框的方式让用户能够更便捷地选择正确的区号。在 ArkUI 中,Select 组件用于实现下拉选择功能,开发者可以使用 Select 组件实现区号选择功能。

使用 @State 装饰器声明一个SelectOption[]类型的状态变量作为对象用于存储Select 组件的选项数据,代码如下:

//第5章/Index.ets
@State selectOptions: SelectOption[] = [
  { value: '+86' }, 
  { value: '+81' }, 
  { value: '+44' }, 
  { value: '+91' }, 
  { value: '+61' }
]

声明string类型的参数countryText、number类型的参数countryCodeIndex,分别存储下拉选择菜单的选中选项的文字、选项索引,代码如下:

@State countryText: string = this.selectOptions[0].value.toString()
@State countryCodeIndex: number = 0

声明string类型的参数phoneNumber,用于存储手机号,代码如下:

@State phoneNumber: string = ''

在build方法中使用Flex组件作为页面容器,并使用Select组件来构建区号,并使用CustomTextInputView子组件来构建手机号输入框,代码如下:

//第5章/Index.ets
import { LengthMetrics } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  @State firstName: string = ''
  @State lastName: string = ''
  @State selectOptions: SelectOption[] = [...]
  @State countryText: string = this.selectOptions[0].value.toString()
  @State countryCodeIndex: number = 0
  @State phoneNumber: string = ''

  build() {
    Flex({
      direction: FlexDirection.Column,
      space: { main: LengthMetrics.px(60), cross: LengthMetrics.px(60) }
    }) {
      // 姓名输入框
      ...

      // 联系方式输入框
      Flex({
        direction: FlexDirection.Row,
        space: { main: LengthMetrics.px(40), cross: LengthMetrics.px(40) }
      }) {
        Column({ space: 10 }) {
          Text('Code')
            .fontWeight(FontWeight.Bold)
          Select(this.selectOptions)
            .selected(this.countryCodeIndex)
            .value(this.countryText)
            .onSelect((index: number, text: string) => {
              this.countryCodeIndex = index
              this.countryText = text
            })
            .width('100%')
        }.alignItems(HorizontalAlign.Start)
        .width('40%')

        CustomTextInputView({
          text: this.phoneNumber,
          title: 'Phone Number',
          placeholder: 'Please Input',
          inputType: InputType.PhoneNumber
        })
      }
    }
    .padding(10)
  }
}

@Component
struct CustomTextInputView {...}

在联系方式输入框的设计上,以 Flex 组件作为父级容器,设置Flex 组件的子组件的排列方式和间距。

在子组件的构建上,使用Column 作为下拉菜单的父级容器,并纵向排布Text 组件和Select组件,其中Select组件绑定下拉菜单的数据源selectOptions,通过selected 修饰器设置下拉选择框的当前选中项的索引,通过value 修饰器设置下拉选择框的当前显示值,而onSelect 修饰器则为下拉选择框绑定一个事件,当用户选中一个选项时,更新该选项的索引和文字。

在实现联系方式输入框后,使用Flex 组件作为姓名输入框和联系方式输入框的父级容器,并设置两个输入框按照垂直方向进行排布。

打开预览器,开发者可以预览联系方式输入框的效果,如图 5-2 所示。

图 5-2 联系方式输入框预览

5.1.4 实现邮件、密码输入框

声明string类型的参数email、password,分别用于存储邮件文字和密码,代码如下:

@State email: string = ''
@State password: string = ''

使用CustomTextInputView子组件来构建邮件输入框和密码输入框,代码如下:

//第5章/Index.ets
import { LengthMetrics } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  ...

  build() {
    Flex({
      direction: FlexDirection.Column,
      space: { main: LengthMetrics.px(60), cross: LengthMetrics.px(60) }
    }) {
      // 姓名输入框
      ...

      // 联系方式输入框
      ...

      // 邮件输入框
      CustomTextInputView({
        text: this.email,
        title: 'Email',
        placeholder: 'Please Input',
        inputType: InputType.Email
      })

      // 密码输入框
      CustomTextInputView({
        text: this.password,
        title: 'Password',
        placeholder: 'Please Input',
        inputType: InputType.Password
      })
    }
    .padding(10)
  }
}

@Component
struct CustomTextInputView {...}

自定义组件CustomTextInputView 中,当设置inputType 的值为InputType.Password 时,该输入框将支持密码输入框的明文和密文的显隐切换交互。

打开预览器,开发者可以预览邮件输入框和密码输入框的效果,如图 5-3 所示。

图 5-3 邮件输入框和密码输入框预览

5.2 实现禁用控制

在表单填写过程中,当用户按照填写要求完成表单填写后,应用会允许用户进行下一步的操作,但当表单填写异常时,开发者需要考虑如何提供清晰的错误提示和引导用户修正,同时确保表单验证逻辑的准确性。

在交互设计上,可以设计一个简单的逻辑,比如当某一个选项未完成填写时,禁用主要的操作按钮。

5.2.1 创建为空判断方法

创建一个为空判断方法checkFieldsFilled(),并使其根据逻辑判断返回一个boolean 类型的值,代码如下:

//第5章/Index.ets
import { LengthMetrics } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  ...

  build() {...}

  checkFieldsFilled(): boolean {
    if (
      this.firstName.trim() === '' ||
        this.lastName.trim() === '' ||
        this.phoneNumber.trim() === '' ||
        this.email.trim() === '' ||
        this.password.trim() === ''
    ) {
      return false
    }
    return true
  }
}

@Component
struct CustomTextInputView {...}

checkFieldsFilled()方法中,使用if 语句逐个检查每个字段是否为空字符串,使用 trim() 方法可以去除字符串两端的空格,避免因用户输入的多余空格导致误判为空字符串。

如果其中一个字段为空,则返回 false,如果所有字段都通过了检查(即都不为空),则返回 true。

5.2.2 实现非空判断逻辑

使用Button组件来显示一个按钮,为按钮添加enabled修饰器,调用checkFieldsFilled()方法判断按钮是否允许被点击,代码如下:

//第5章/Index.ets
import { LengthMetrics } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  ...

  build() {
    Flex({
      direction: FlexDirection.Column,
      space: { main: LengthMetrics.px(60), cross: LengthMetrics.px(60) }
    }) {
      // 姓名输入框
      ...

      // 联系方式输入框
      ...

      // 邮件输入框
      ...

      // 密码输入框
      ...

      // 注册按钮
      Button('Sign Up', { type: ButtonType.Capsule, stateEffect: true })
        .backgroundColor(Color.Blue)
        .width('100%')
        .height(40)
        .enabled(this.checkFieldsFilled())
    }
    .padding(10)
  }

  checkFieldsFilled(): boolean {...}
}

@Component
struct CustomTextInputView {...}

在预览器中,开发者可以观察到,当输入框任意一个选项为空时,按钮将处于禁用状态,并且通过弱化其填充色透明度来告知用户按钮不可点击。如图 5-4 所示。

图 5-4 禁用控制效果预览

5.3 实现输入导航

当表单需要用户填写多个信息时,优化输入流程对于提升用户体验至关重要。开发者可以通过合理设置输入框的焦点顺序和字段间的输入导航,让用户在完成一个信息的填写后能够快速定位到下一个输入框,从而实现流畅的输入体验。

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

5.3.1 实现登录页面

声明string类型的状态变量username、password,分别用于存储用户输入的账号和密码,并使用Flex 组件和TextInput、Button 组件构建一个简单的登录页面,代码如下:

//第5章/Index.ets
@Entry
@Component
struct Index {
  @State username: string = ''
  @State password: string = ''

  build() {
    Flex({
      direction: FlexDirection.Column,
      justifyContent: FlexAlign.Center,
      alignItems: ItemAlign.Center
    }) {
      TextInput({text: this.username, placeholder: 'Username' })
        .type(InputType.Normal)
        .height(40)
        .margin({ bottom: 20 })
        .onChange((value: string) => {this.username = value})

      TextInput({text: this.password, placeholder: 'Password' })
        .type(InputType.Password)
        .height(40)
        .margin({ bottom: 20 })
        .onChange((value: string) => {this.password = value})

      Button('Sign In').width('50%').height(40)
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

Flex 组件中,direction用于设置子组件的排列方式,justifyContent 用于设置子组件在主轴方向上的对齐方式,比如当前排列方式为垂直排列,则主轴方向,即垂直方向上可设置居中子组件对齐。alignItems 用于设置子组件在交叉轴方向上的对齐方式,当前主轴方向为垂直方向,则交叉轴方向为水平方向。

5.3.2 设置默认聚焦

为账号输入框添加defaultFocus修饰器,并设置接收的值为true,代码如下:

TextInput({text: this.username, placeholder: 'Username' })
  // 隐藏部分代码
  .defaultFocus(true)

defaultFocus修饰器用于设置输入框的默认聚焦规则,当设置为true时,则在页面载入时系统会自动为输入框获得焦点,并打开系统软键盘,如图 5-4 所示。

图 5-4 默认聚焦效果预览

5.3.3 设置指定聚焦

当用户完成账号输入框的输入,此时用户常规操作为点击系统软键盘的“完成”按钮,此时开发者可以重新设置输入框的焦点指向,让指定的输入框获取焦点,实现输入框焦点的定向导航。

为密码输入框添加id 修饰器,并设置id 修饰器传入的内容为Password。为账号输入框添加onSubmit事件修饰器,并通过逻辑判断来执行焦点切换,代码如下:

//第5章/Index.ets
@Entry
@Component
struct Index {
  @State username: string = ''
  @State password: string = ''

  build() {
    Flex({...}) {
      TextInput({text: this.username, placeholder: 'Username' })
        // 隐藏部分代码
        ...
        .defaultFocus(true)
        .onSubmit((EnterKeyType) => {
          if (EnterKeyType === 6) {
            focusControl.requestFocus('Password')
          }
        })

      TextInput({text: this.password, placeholder: 'Password' })
         // 隐藏部分代码
        ...
        .id('Password')

      Button('Sign In').width('50%').height(40)
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

在用户名输入框的onSubmit 事件中,检查EnterKeyType是否为6(6 是 EnterKeyType 枚举中对应“完成”按钮的值),当满足条件下,调用focusControl.requestFocus() 方法,将焦点切换到id 为Password 的密码输入框。

在模拟器中开发者可以观察到,页面加载时,用户名输入框会自动获得焦点并弹出软键盘。在用户名输入框中输入内容后,按下“完成”按钮,焦点会切换到密码输入框,且软键盘保持弹出状态。如图 5-5 所示。

图 5-5 指定聚焦效果预览

5.4 集成搜索与清除按钮

为了满足用户对便捷性的需求,ArkUI在表单设计中引入了多种优化方案。比如在输入框中集成“清除”按钮,方便用户快速清空错误输入并重新输入,以及在搜索框中集成了“搜索”按钮,在提供键盘搜索键的同时,也提供了搜索图标引导用户启动搜索。

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

使用Flex、Search、TextInput组件实现一个简单的页面,其中Search 组件使用searchButton修饰器构建一个搜索按钮,以及使用cancelButton修饰器构建一个清除按钮。TextInput组件也使用cancelButton修饰器构建一个清除按钮,代码如下:

//第5章/Index.ets
@Entry
@Component
struct Index {
  @State searchText: string = ''
  @State inputText: string = ''

  build() {
    Flex({
      direction: FlexDirection.Column,
      justifyContent: FlexAlign.Start,
      alignItems: ItemAlign.Center
    }) {
      Search({ value: this.searchText, placeholder: '搜索' })
        .searchButton('搜索')
        .cancelButton({
          style: CancelButtonStyle.CONSTANT,
          icon: {
            size: 17,
            color: Color.Gray
          }
        })
        .margin({bottom:20})

      TextInput({ text: this.inputText, placeholder: '请输入你的问题' })
        .cancelButton({
          style: CancelButtonStyle.CONSTANT,
          icon: {
            size: 17,
            color: Color.Gray
          }
        })
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

searchButton修饰器专用于Search组件,集成onSubmit和onClick 事件功能来显示“搜索”按钮并实现搜索操作,而cancelButton修饰器则适用于Search和TextInput组件,通过onClick回调为输入框快速添加“清除”按钮,并同时实现点击删除内容的功能。

在预览器中,开发者可以体验搜索和清除功能的交互反馈,如图 5-6 所示。

图 5-6 搜索和清除按钮

5.5 设置自定义菜单

自定义菜单允许开发者在系统长按菜单中,除了默认的“剪切”、“复制”、“粘贴”和“全选”等操作外,还可以自定义构建功能菜单,并将自定义菜单与系统操作菜单集成,从而为用户提供更加丰富的功能服务。

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

自定义菜单的开发需要开发提前定义菜单扩展项的文本内容和回调的功能,代码如下:

//第5章/Index.ets
private customMenuItems: TextMenuItem[] = [
  {
    content: '搜索复制的文字',
    id: TextMenuItemId.of('search')
  },
  {
    content: '自动填充',
    id: TextMenuItemId.of('fill')
  }
]

private onMenuItemClick(menuItem: TextMenuItem): boolean {
  if (menuItem.id.equals(TextMenuItemId.of('search'))) {
    console.log('搜索复制的文字')
    return true
  }
  if (menuItem.id.equals(TextMenuItemId.of('fill'))) {
    console.log('自动填充')
    return true
  }
  return false
}

@State editMenuOptions: EditMenuOptions = {
  onCreateMenu: (menuItems: TextMenuItem[]) => this.customMenuItems.concat(menuItems),
  onMenuItemClick: this.onMenuItemClick
}

customMenuItems数组中定义了两个自定义菜单项,每个菜单项包含content和id属性。其中content属性用于显示菜单项的文本内容,id属性用于唯一标识菜单项。

onMenuItemClick()方法的作用是处理菜单项的点击事件,当用户点击某个菜单项时,通过menuItem.id.equals方法对点击的菜单项的id与预定义的TextMenuItemId 进行匹配判断,确定执行点击菜单项后的操作。

editMenuOptions状态变量中定义了onCreateMenu和onMenuItemClick两个属性,onCreateMenu属性接收默认的菜单项数组menuItems,并将customMenuItems数组与默认菜单项数组合并后返回,从而将自定义菜单项添加到默认菜单项中。onMenuItemClick属性绑定了onMenuItemClick方法,用于处理菜单项的点击事件。

下一步,使用Flex 组件和Search 组件构建主体页面,并为Search 组件添加editMenuOptions 修饰器,用于构建自定义菜单扩展项,代码如下:

//第5章/Index.ets
@Entry
@Component
struct Index {
  private customMenuItems: TextMenuItem[] = [...]
  private onMenuItemClick(menuItem: TextMenuItem): boolean {...}
  @State editMenuOptions: EditMenuOptions = {...}

  @State searchText: string = ''

  build() {
    Flex({
      direction: FlexDirection.Column,
      justifyContent: FlexAlign.Start,
      alignItems: ItemAlign.Center
    }) {
      Search({ value: this.searchText, placeholder: '搜索' })
        .searchButton('搜索')
        .editMenuOptions(this.editMenuOptions)
    }
    .padding({left:10,right:10})
  }
}

在预览器中,开发者可以在搜索框中点击鼠标左键并按住,来体验自定义菜单的交互效果,如图 5-7 所示。

图 5-7 自定义菜单预览

5.6 本章小结

本章通过弹性布局组件实现了多种输入框的设计,包括姓名、联系方式、邮件和密码输入框,并通过禁用控制和非空判断逻辑确保了表单的健壮性和数据准确性。此外,通过输入导航的优化和搜索与清除按钮的集成、自定义菜单的设置,进一步提升了表单的便捷性和用户体验,为表单功能的扩展提供了更多可能性,增强了界面的灵活性和可扩展性。

通过本章的学习,希望读者在提升开发效率的同时,也能在未来的项目中轻松运用表单设计的相关能力,设计出更加优秀的应用。