HarmonyOS一杯冰美式的时间 -- 验证码框

一、前言

像是短密码、验证码都有可能需要一个输入框,像是如下:

恰好在写HarmonyOS的时候也需要写一个验证码输入框,但是在实现的时候碰了几次灰,觉得有必要分享下,故有了此篇文章。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏 PS:二三为错误示例,如果你只想要代码,在四开始

二、ForEach + TextInput

一开始直接上手就是使用Android的老方案,使用多个EditText,只需要切换焦点即可。在HarmonyOS中对应的就是TextInput。因为需要数个相同的输入框,我们先写一个通用的输入框。

 @Component
 struct CodeInputView {
   build() {
     TextInput()
       .backgroundColor("#CCFFFFFF")
       .borderRadius(10)
       .maxLength(1)
       .type(InputType.Number)
       .align(Alignment.Center)
   }
 }

如果一个个去添加输入框,太麻烦了,如果有改动也很头大,所以我们可以塞到一个父布局中,使用ForEach来添加。因为这种情形的输入一般是横向的,使用Row是一个很好的主意,所以变成了"在Row中使用ForEach添加若干个TextInput",我们稍微修改下:

 @Preview
 @Component
 struct CodeInputView {
   // 创建一个包含5个空字符串的数组,用于存储输入的数字
   @State codeKids: Array<string> = new Array(5).fill('')
 ​
   // 构建界面
   build() {
     Row({ space: 10 }) {
       ForEach(this.codeKids, (item: string, index: number) => {
         TextInput(this.codeKids[index])
           .backgroundColor("#CCFFFFFF") // 设置文本输入框的背景颜色
           .borderRadius(10) // 设置文本输入框的圆角
           .maxLength(1) // 设置最大输入长度为1
           .layoutWeight(1) // 设置布局权重
           .fontSize(25) // 设置字体大小
           .height("100%") // 设置高度为100%
           .type(InputType.Number) // 设置输入类型为数字
           .align(Alignment.Center) // 设置文本居中对齐
       }, (item: string) => item)
     }.backgroundColor(Color.Black) // 设置整个行的背景颜色为黑色,方便preview
     .height(80) // 设置行的高度为80
   }
 }

如果我们逐个手动添加输入框,会显得非常繁琐,而且如果需要进行修改的话也会变得很复杂。

因此,我们可以将这些输入框放置在一个父布局中,然后使用 ForEach 函数来动态添加它们。由于这种情况下输入框通常是水平排列的,所以使用 Row 组件是一个明智的选择。因此,我们将代码改成了 '在 Row 中使用 ForEach 动态添加多个 TextInput' 的方式。

我们新增了一个名为 codeKids 的数组,并用空字符进行了填充,并使用 @State 注解来修饰它。在 RowForEach 中,我们直接使用 codeKids 作为数据源,这样输入框的数量会根据 codeKids 数组的长度而变化,而 codeKids 的大小就代表了验证码的长度。

layoutWeight(1){ space: 10 } 这两个组合参数,实现了等宽和等间距的效果。

通过@Preview,我们已经能看到效果了。

接下来我们需要它动起来,也就是"输入一个切换到下一个输入框,最后一个返回完整的验证码"。

这里显然需要我们使用onChange方法监听字符的输入。

分解一下

  1. 监听每个 TextInputonChange 事件,当用户输入字符后,将字符存入相应位置的 codeKids 数组,并移动焦点到下一个 TextInput
  2. 在最后一个输入框中,当用户输入字符后,将字符存入 codeKids 数组,并触发验证码完成的操作。

需要注意的是,并不能使用focusable(true)来达到将焦点赋予给某个输入框的操作,移动焦点需要使用focusControl.requestFocus(),而requestFocus需要的参数是输入框的key,这里我们需要新增一个key:

 @Preview
 @Component
 struct CodeInputView {
   // 用于存储用户输入的字符的数组,初始值为5个空字符串
   @State codeKids: Array<string> = new Array(5).fill('')
   
   // 回调函数,用于传递输入结果给父组件
   inputResultCallback: (string) => void
 ​
   build() {
     // 创建一个横向排列的行,每个输入框之间有一定的间隔
     Row({ space: vp(10) }) {
       ForEach(this.codeKids, (item: string, index: number) => {
         TextInput()
           .backgroundColor("#CCFFFFFF") // 设置文本输入框的背景颜色
           .borderRadius(10) // 设置文本输入框的圆角
           .maxLength(1) // 设置最大输入长度为1
           .layoutWeight(1) // 设置布局权重
           .fontSize(25) // 设置字体大小
           .height("100%") // 设置高度为100%
           .type(InputType.Number) // 设置输入类型为数字
           .align(Alignment.Center) // 设置文本居中对齐
           .key(`code${index}`) // 为每个输入框设置唯一的键
           .onChange((value) => {
             if (value.length <= 1) {
               this.codeKids[index] = value // 存储用户输入的字符
             }
             if (index - 1 < this.codeKids.length) {
               let nextIndex = index + 1
               // 将焦点自动移动到下一个输入框
               focusControl.requestFocus(`code${nextIndex}`)
             } else {
               // 触发验证码完成回调函数
               this.inputResultCallback(this.codeKids.join(""))
             }
           })
       }, (item: string) => item)
     }
     .backgroundColor(Color.Black) // 设置整个行的背景颜色为黑色
     .height(80) // 设置行的高度为80
   }
 }

在新的代码中

  1. inputResultCallback属性 :新增了一个名为 inputResultCallback 的属性,用于在用户完成输入后将结果传递给父组件。

  2. TextInput的onChange事件 :在每个 TextInput 组件中添加了 onChange 事件处理程序。当用户输入内容时,这个事件处理程序会被触发。在事件处理程序内部,会进行以下操作:

    • 检查输入的值长度是否小于等于1,如果是则将该值存储在 codeKids 数组的相应位置上,以保证每个输入框只能输入一个字符。
    • 检查是否还有下一个输入框(index + 1 是否小于 codeKids 数组的长度)。如果有下一个输入框,将焦点自动移动到下一个输入框,以方便用户连续输入。
    • 如果没有下一个输入框,触发 inputResultCallback 回调函数,将输入的值传递给父组件或其他调用者。
  3. key属性 :为每个 TextInput 组件添加了 key 属性,以确保focusControl.requestFocus的正确触发,这里我们使用了 index 来生成唯一的键。

三、奇怪的问题

  1. 输入框没有焦点

    第一次初始化的时候并没有获取焦点,系统也不知道焦点给谁。

    我们只需要在TextInput中加入

    .defaultFocus(index == 0)

  2. 删除onChange方法并不会触发

    整个流程都已经完成了,包括删除验证码!

    if (value.length <= 1) {
    this.codeKids[index] = value
    }

    这段代码赋予了当被删除的时候,数组中的值也会正确的改变。但是!

    令人奇怪的是,在当前版本中当进行删除操作的时候,onChange方法并不会触发(平板、模拟器、手机均不会),所以我们需要另寻它法。

    监听onKeyEvent!

    .onKeyEvent((event)=>{
    if (event.keyCode == KeyCode.KEYCODE_DEL) {

    }
    

    })

事实上,想法是美好的,这个方法也不会触发(模拟器、平板不触发、手机触发异常)

  1. 软键盘显示异常

    focusControl.requestFocus(nextKeyStr)

    使用requestFocus的确可以将焦点切换到下一个输入框,但是软键盘确收起来了!

    在这里我试了很多种办法。都没法做到尽善尽美。

    多方查证,也觉得TextInput来做这个应该是不可行的,只能等官方下场修复。

    那怎么办呢?

四、反过来想 Text() + TextInput()

如果多个输入框有问题,那么我用一个输入框不就行了?于是我就想到了使用多个Text(),一个TextInput的方案。

多个Text()用于排列显示,TextInput用于处理输入

只要显示正常,感知正常,那就没人知道怎么输入进去的~

 @Preview
 @Component
 struct CodeInputView {
   // 用于存储用户输入的字符的数组,初始值为5个空字符串
   @State codeKids: Array<string> = new Array(5).fill('')
 ​
   // 回调函数,用于传递输入结果给父组件
   inputResultCallback: (string) => void
 ​
   build() {
     // 使用 Stack 布局组织界面元素
     Stack() {
       if (this.codeKids != null) {
         // 创建一个横向排列的行,每个字符之间有一定的间隔
         Row({ space: vp(10) }) {
           // 使用 ForEach 循环遍历 codeKids 数组
           ForEach(this.codeKids, (item: string, index: number) => {
             // 显示用户输入的字符
             Text(item)
               .backgroundColor($r('app.color.white_80')) // 设置背景颜色
               .height(match()) // 设置高度匹配内容
               .layoutWeight(1) // 设置布局权重
               .fontSize(fp(25)) // 设置字体大小
               .textAlign(TextAlign.Center) // 设置文本水平居中对齐
               .align(Alignment.Center) // 设置垂直居中对齐
               .borderRadius(vp(15)) // 设置圆角
               .focusable(false) // 不可获得焦点
               .defaultFocus(false) // 默认不获得焦点
               .focusOnTouch(false) // 不在触摸时获得焦点
           }, (item: string) => item)
         }
         .height(match()) // 设置行的高度匹配内容
         .width(match()) // 设置行的宽度匹配内容
 ​
         // 创建一个输入框用于用户输入
         TextInput()
           .maxLength(this.viewSize) // 设置最大输入长度
           .fontSize(fp(25)) // 设置字体大小
           .borderRadius(vp(15)) // 设置圆角
           .type(InputType.Number) // 设置输入类型为数字
           .key(this.inputKey) // 设置唯一的键
           .onChange((value) => {
             // 将输入的字符拆分并分别显示在 Text 组件中
             let a = value.split('')
             this.codeKids.forEach((value, index) => {
               this.codeKids[index] = a[index] || ''
             })
             if (a.length >= this.viewSize) {
               // 当达到验证码长度时,触发回调函数传递输入结果
               this.inputResultCallback(value)
             }
             // 控制光标显示/隐藏
             this.showCaret = (a.length == 0)
           })
           .copyOption(CopyOptions.None) // 禁用复制操作
           .caretColor(this.showCaret ? Color.Black : Color.Transparent) // 设置光标颜色
           .fontColor(Color.Transparent) // 设置文本颜色为透明
           .backgroundColor(Color.Transparent) // 设置背景颜色为透明
           .height(match()) // 设置高度匹配内容
           .width(match()) // 设置宽度匹配内容
       }
     }
     .height(vp(80)) // 设置整个 Stack 的高度
   }
 }
  1. TextInput填充布局,置于顶层。文字和背景设置为透明,隐藏光标

    .copyOption(CopyOptions.None) // 禁用复制操作
    .caretColor(Color.Transparent) // 设置光标为透明
    .fontColor(Color.Transparent) // 设置文本颜色为透明
    .backgroundColor(Color.Transparent) // 设置背景颜色为透明

  2. 添加对应数量的Text,用作显示验证码。这一步其实就是将之前的ForEach中添加的TextInput换为Text即可

  3. 在onChange中分隔字符串,并存入对应下标的数组中

    // 将输入的字符拆分并分别显示在 Text 组件中
    let a = value.split('')
    this.codeKids.forEach((value, index) => {
    this.codeKids[index] = a[index] || ''
    })
    if (a.length >= this.viewSize) {
    // 当达到验证码长度时,触发回调函数传递输入结果
    this.inputResultCallback(value)
    }

使用也很简单

 CodeInputView({inputResultCallback: (code) => {
   //做点什么
 })

最终效果如下

五、最后

只需要稍微的封装下,将输入框的宽度、高度、圆角、颜色、输入类型、数量等包裹在一个对象中,使用@State修饰,并一一对应应用,即可将这个组件做成一个很标准的任意发挥的输入框啦。

唯一的遗憾是,目前没法去除TextInput点击的样式,除非你是纯色(纯色变化看不出来...)

以下就是该例子代码啦:

 @Preview
 @Component
 export struct CodeInputView {
   @State viewSize: number = 4
   inputResultCallback: (string) => void
   @Link codeKids: Array<string>
   @State showCaret: boolean = true
   private inputKey = "code_input"
 ​
   aboutToAppear() {
     if (this.codeKids == null) {
       this.codeKids = new Array(this.viewSize).fill('');
     }
   }
 ​
   build() {
     Stack() {
       if (this.codeKids != null) {
         Row({ space: vp(10) }) {
           ForEach(this.codeKids, (item: string, index: number) => {
             Text(item)
               .backgroundColor($r('app.color.white_80'))
               .height(match())
               .layoutWeight(1)
               .fontSize(fp(25))
               .textAlign(TextAlign.Center)
               .align(Alignment.Center)
               .borderRadius(vp(15))
               .focusable(false)
               .defaultFocus(false)
               .focusOnTouch(false)
               .onClick(() => {
                 focusControl.requestFocus(this.inputKey)
               })
           }, (item: string) => item)
         }
         .height(match())
         .width(match())
 ​
         TextInput()
           .maxLength(this.viewSize)
           .fontSize(fp(25))
           .borderRadius(vp(15))
           .type(InputType.Number)
           .key(this.inputKey)
           .onChange((value) => {
             let a = value.split('')
             this.codeKids.forEach((value, index) => {
               this.codeKids[index] = a[index] || ''
             })
             if (a.length >= this.viewSize) {
               this.inputResultCallback(value)
             }
             this.showCaret = (a.length == 0)
           })
           .copyOption(CopyOptions.None)
           .caretColor(this.showCaret ? Color.Black : Color.Transparent)
           .fontColor(Color.Transparent)
           .backgroundColor(Color.Transparent)
             //TODO 系统问题,如果背景色是透明的也没用,非透明可以
             // .stateStyles({ pressed: {.backgroundColor("跟背景一样的颜色(纯透明会黑色闪一下)")}})
           .height(match())
           .width(match())
       }
     }
     .height(vp(80))
   }
 }

六、总结

这个需求大概就告一段了,如果各位有什么想加的功能啥的,可以在评论区告知哦。

总之,HarmonyOS ArkUI的文档还是太少了,很多API都需要摸索,很多写法、操作都不习惯。以及很多坑!,Android的思维不适用在HarmonyOS。

为了能让大家更好的学习鸿蒙 (OpenHarmony) 开发技术,这边特意整理了《鸿蒙 (OpenHarmony)开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙 (OpenHarmony)开发学习手册》

入门必看:https://qr21.cn/FV7h05

  1. 应用开发导读(ArkTS)
  2. ......

HarmonyOS 概念:https://qr21.cn/FV7h05

  1. 系统定义
  2. 技术架构
  3. 技术特性
  4. 系统安全

如何快速入门?:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. 构建第一个JS应用
  4. ......

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ......

基于ArkTS 开发:https://qr21.cn/FV7h05

1.Ability开发

2.UI开发

3.公共事件与通知

4.窗口管理

5.媒体

6.安全

7.网络与链接

8.电话服务

9.数据管理

10.后台任务(Background Task)管理

11.设备管理

12.设备使用信息统计

13.DFX

14.国际化开发

15.折叠屏系列

16.......

相关推荐
艾格北峰2 小时前
STM32 物联网智能家居 (六) OLED显示设备
arm开发·stm32·单片机·嵌入式硬件·物联网·智能家居
雪兽软件2 小时前
物联网简介集合
物联网
Huang兄3 小时前
鸿蒙-状态管理V1
华为·harmonyos
别说我什么都不会5 小时前
鸿蒙轻内核M核源码分析系列十一 (2)信号量Semaphore
操作系统·harmonyos
塞尔维亚大汉5 小时前
OpenHarmony(鸿蒙南向)——平台驱动开发【GPIO】
harmonyos·领域驱动设计
敢嗣先锋9 小时前
鸿蒙5.0实战案例:基于ArkUI启动冷启动过程最大连续丢帧数问题分析思路&案例
性能优化·移动开发·多线程·harmonyos·arkui·鸿蒙开发
HarmonyOS_SDK9 小时前
智能网络感知,打造极致流畅的鸿蒙原生版中国移动云盘图文体验
harmonyos
楼台的春风9 小时前
PWM(脉宽调制)技术详解:从基础到应用实践示例
c语言·stm32·单片机·嵌入式硬件·mcu·物联网·嵌入式
SuperHeroWu79 小时前
【HarmonyOS Next】鸿蒙应用进程和线程详解
华为·线程·进程·harmonyos·鸿蒙