前言导读
最近能在一个鸿蒙的项目,里面有用到类似安卓和ios验证码输入框,所以就参考网上的一些案例重写了一个鸿蒙版本 那么废话不多说我们正式开始。
开发环境
鸿蒙 Next(基于API12) 实现一个验证码输入组件(VerifyCodeInput)
效果图


具体实现
为了解决输入或删除自动移格通过Text组件来显示验证码而非TextInput,键盘输入通过inputMethod.InputMethodController 自绘控件绑定输入法形式进行输入内容监听,最后通过组件本身 onVisibleAreaChange事件判断页面是隐藏(跳转下一页)或者显示(返回)动态控制键盘弹出和隐藏
基础变量声明
ini
verifyCodeInputCallback?:VerifyCodeInputCallback
@State codeText: string = '';
private inputController: inputMethod.InputMethodController = inputMethod.getController();
private arrArea: number[] = [0.0, 1, 0];
private verifyCodeLength: number = 6;
private codeIndexArray: Array<number> = Array.from([0, 1, 2, 3, 4, 5]);
private textConfig: inputMethod.TextConfig = {
inputAttribute: {
textInputType: inputMethod.TextInputType.NUMBER,
enterKeyType: inputMethod.EnterKeyType.GO
}
};
private windowClass: window.Window = AppStorage.get('windowClass') as window.Window;
private isFirstOpenPage: boolean = true;
private registerCount: number = 1;
2、自绘控件输入法事件回调设置和事件解绑
kotlin
async attach() {
if (this.isFirstOpenPage) {
await this.inputController.attach(true, this.textConfig);
this.listen();
return;
}
try {
this.windowClass.on('windowEvent', async (windowState) => {
if (windowState === window.WindowEventType.WINDOW_ACTIVE && this.registerCount === 1) {
this.registerCount++;
await this.inputController.attach(true, this.textConfig);
this.listen();
}
});
} catch (error) {
hilog.error(0x0000, TAG, `failed to getWindowState callback, error code : ${error.code}`);
}
}
listen() {
this.inputController.on('insertText', (text: string) => {
if (this.codeText.length >= this.verifyCodeLength || isNaN(Number(text)) || text === ' ') {
return;
}
this.codeText += text;
this.verifyCodeInputCallback?.getinputcode(this.codeText)
})
this.inputController.on('deleteLeft', () => {
this.codeText = this.codeText.substring(0, this.codeText.length - 1);
this.verifyCodeInputCallback?.getinputcode(this.codeText)
})
}
detach(): void {
this.isFirstOpenPage = false;
this.registerCount = 1;
this.windowClass.off('windowEvent');
this.inputController.off('insertText');
this.inputController.off('deleteLeft');
this.inputController.detach((_err: BusinessError) => {
});
}
我们在 aboutToDisappear 进来的时候要调用一下this.detach()默认清空我们的输入框
验证码输入框布局和输入绑定和解绑
typescript
@Builder
buildVerifyCodeComponent() {
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.codeIndexArray, (item: number) => {
Text(this.codeText[item])
.verifyCodeUnitStyle()
.backgroundColor(this.codeText[item] === undefined ? $r('app.color.index_background') :
$r('app.color.theme_color_m'))
}, (item: number) => JSON.stringify(item))
}
.onVisibleAreaChange(this.arrArea, async (isVisible: boolean, currentRatio: number) => {
if (isVisible && currentRatio >= 1.0) {
this.attach();
}
if (!isVisible && currentRatio <= 0.0) {
this.detach();
}
})
.backgroundColor(Color.Transparent)
.height($r('app.float.text_height'))
.margin({
left: $r('app.float.background_margin'),
right: $r('app.float.background_margin')
})
.defaultFocus(true)
.onClick(() => {
this.inputController.attach(true, this.textConfig);
})
}
定义接口回调输入拿到的结果
我们通过构造方法传入我们的interface
typescript
/***
*
* 输入框回调
*
*/
export interface VerifyCodeInputCallback{
getinputcode: (value?:string) => void
}
回调结果当然我们也可以使用@Link装饰器来实现
kotlin
listen() {
this.inputController.on('insertText', (text: string) => {
if (this.codeText.length >= this.verifyCodeLength || isNaN(Number(text)) || text === ' ') {
return;
}
this.codeText += text;
//回调我们的结果
this.verifyCodeInputCallback?.getinputcode(this.codeText)
})
this.inputController.on('deleteLeft', () => {
this.codeText = this.codeText.substring(0, this.codeText.length - 1);
// //回调我们的结果
this.verifyCodeInputCallback?.getinputcode(this.codeText)
})
}
完整代码
kotlin
import { inputMethod } from '@kit.IMEKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { VerifyCodeInputCallback } from './VerifyCodeInputCallback';
/*****
*
* 创建人:xuqing
* 创建时间:2025年4月21日10:51:13
* 类说明:自定义输入框格子
*
*/
@Extend(Text)
function verifyCodeUnitStyle() {
.fontSize($r('app.float.font_size'))
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.width($r('app.float.text_height'))
.aspectRatio(1)
.margin({
left: $r('app.float.text_margin'),
right: $r('app.float.text_margin')
})
.borderRadius($r('app.float.row_width'))
}
const TAG = 'BackgroundColorChange';
@Component
export default struct VerifyCodeInput {
verifyCodeInputCallback?:VerifyCodeInputCallback
@State codeText: string = '';
private inputController: inputMethod.InputMethodController = inputMethod.getController();
private arrArea: number[] = [0.0, 1, 0];
private verifyCodeLength: number = 6;
private codeIndexArray: Array<number> = Array.from([0, 1, 2, 3, 4, 5]);
private textConfig: inputMethod.TextConfig = {
inputAttribute: {
textInputType: inputMethod.TextInputType.NUMBER,
enterKeyType: inputMethod.EnterKeyType.GO
}
};
private windowClass: window.Window = AppStorage.get('windowClass') as window.Window;
private isFirstOpenPage: boolean = true;
private registerCount: number = 1;
aboutToDisappear(): void {
this.detach();
}
async attach() {
if (this.isFirstOpenPage) {
await this.inputController.attach(true, this.textConfig);
this.listen();
return;
}
try {
this.windowClass.on('windowEvent', async (windowState) => {
if (windowState === window.WindowEventType.WINDOW_ACTIVE && this.registerCount === 1) {
this.registerCount++;
await this.inputController.attach(true, this.textConfig);
this.listen();
}
});
} catch (error) {
hilog.error(0x0000, TAG, `failed to getWindowState callback, error code : ${error.code}`);
}
}
listen() {
this.inputController.on('insertText', (text: string) => {
if (this.codeText.length >= this.verifyCodeLength || isNaN(Number(text)) || text === ' ') {
return;
}
this.codeText += text;
this.verifyCodeInputCallback?.getinputcode(this.codeText)
})
this.inputController.on('deleteLeft', () => {
this.codeText = this.codeText.substring(0, this.codeText.length - 1);
this.verifyCodeInputCallback?.getinputcode(this.codeText)
})
}
detach(): void {
this.isFirstOpenPage = false;
this.registerCount = 1;
this.windowClass.off('windowEvent');
this.inputController.off('insertText');
this.inputController.off('deleteLeft');
this.inputController.detach((_err: BusinessError) => {
});
}
@Builder
buildVerifyCodeComponent() {
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.codeIndexArray, (item: number) => {
Text(this.codeText[item])
.verifyCodeUnitStyle()
.backgroundColor(this.codeText[item] === undefined ? $r('app.color.index_background') :
$r('app.color.theme_color_m'))
}, (item: number) => JSON.stringify(item))
}
.onVisibleAreaChange(this.arrArea, async (isVisible: boolean, currentRatio: number) => {
if (isVisible && currentRatio >= 1.0) {
this.attach();
}
if (!isVisible && currentRatio <= 0.0) {
this.detach();
}
})
.backgroundColor(Color.Transparent)
.height($r('app.float.text_height'))
.margin({
left: $r('app.float.background_margin'),
right: $r('app.float.background_margin')
})
.defaultFocus(true)
.onClick(() => {
this.inputController.attach(true, this.textConfig);
})
}
build() {
Column() {
this.buildVerifyCodeComponent();
}.width('100%')
}
}
在index 里面调用调试
scss
import Logger from './Logger';
import VerifyCodeInput from './VerifyCodeInput';
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Column() {
Row() {
Text($r('app.string.enter_the_verification_code'))
.fontSize($r('app.float.font_size_l'))
.fontWeight(FontWeight.Medium)
}.height($r('app.float.list_padding_top'))
Row() {
Text($r('app.string.Verification_code_sent_to'))
.fontSize($r('app.float.font_size_m'))
.fontColor($r('app.color.phone_color'))
.fontWeight(FontWeight.Regular)
Text($r('app.string.phone_number1'))
.fontSize($r('app.float.font_size_m'))
.fontColor($r('app.color.theme_color'))
.fontWeight(FontWeight.Regular)
}
.margin({
top: $r('app.float.row_width'),
bottom: $r('app.float.margin_bottom')
})
Row(){
VerifyCodeInput({verifyCodeInputCallback: {
getinputcode(data) {
Logger.error("数据框回调 data --- > " + data)
}
}
});
}.width("100%").height(50).margin({top:15})
Row() {
Text($r('app.string.reacquire'))
.fontSize($r('app.float.font_size'))
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.theme_color'))
}
.width('90%')
.margin({ top: $r('app.float.row_margin') })
.justifyContent(FlexAlign.End)
}
.padding({ top: 48})
.height('100%')
.width('100%')
}
}
查看日志打印
我们查看到日志无论是我们删除输入框的内容 还是增加输入框的内容控制台里面都有对应结果的输出

最后总结:
在鸿蒙next 里面确实还有很多功能效果没有现成的组件去实现,但是生态越来越强大,我相信在不久未来 鸿蒙系统的生态会发展得越来越好,有兴趣的同学可以拷贝代码自己去尝试 , 今天的文章就讲到这里有兴趣的 关注我B站教程 了解更多鸿蒙开发的知识 可以关注坚果派公众号 。 谢谢
课程地址
www.bilibili.com/cheese/play...
项目内容:
1 常用布局组件的学习
2 网络请求工具类封装
3 arkui 生命周期启动流程
4 日志工具类的封装
5 自定义组合组件的封装
6 路由导航跳转的使用
7 本地地数据的缓存 以及缓存工具类的封装
8 欢迎页面的实现
9 登录案例和自动登录效果实现
10 请求网络数据分页上拉加载 下拉刷新的实现
11 list数据懒加载实现
12 webview组件的使用
如果使用更多好用的鸿蒙next三方库
团队介绍
团队介绍:坚果派由坚果等人创建,团队由12位华为HDE以及若干热爱鸿蒙的开发者和其他领域的三十余位万粉博主运营。专注于分享 HarmonyOS/OpenHarmony,ArkUI-X,元服务,仓颉,团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,目前已开发鸿蒙 原生应用,三方库60+,欢迎进行课程,项目等合作。
友情链接
harmony-utils 一款功能丰富且极易上手的HarmonyOS工具库,借助众多实用工具类,致力于助力开发者迅速构建鸿蒙应用,能够满足各种不同的开发需求。
harmony-dialog 一款极为简单易用的零侵入弹窗,仅需一行代码即可轻松实现,无论在何处都能够轻松弹出。