鸿蒙Next上获取输入框的内容,居然能卡我一整天

“按下按钮时获取输入框的内容”,这种HelloWorld级的需求,十年老工程师花了一天都没搞定,ArkUI这东西,真是给人用的?

前言

最近工作中需要开发一点ArkUI的代码,直接进入正题,给大家看看ArkUI实现这么简单的需求到底有多费劲。

插曲

在编写本文的过程中,意外发现我的 Pura70ProPlus 居然连不上电脑,换了三台电脑,换了N个USB口,换了N条数据线,最终发现反复连接/断开/连接/断开,网上一搜发现不止我一个这样。

此时,一位潜在的鸿蒙开发巨星陨落了

那我们用无线调试吧!

然后发现,hdc命令的help里,居然没有连接到无线调试的命令???

1
2
hdc --help 2>&1 | findstr tconn
# No output

此时,一位潜在的鸿蒙开发巨星又一次陨落了

最终在一些野生文档里终于找到了命令:hdc tconn 192.168.3.126:38633,成功开启鸿蒙之旅。

简单绘制一下界面,舒服了。

getText API?查无此人!

上述的绘制涉及两个组件,分别叫 TextInputButton,都是非常通俗易懂的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
build() {
Column() {
TextInput()
.fontSize(50)
.type(InputType.Number)
.fontWeight(FontWeight.Bold)
Button("button")
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
console.log("click")
})
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}

Button的点击事件可以轻松完成,虽然我还没有将 TextInput 对象绑定到变量上,但应该能获得到这个对象,我们翻一下 TextInput 的官方指南吧~

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-common-components-text-input-V5 , emmmmm,只提供了onChange和onSubmit的回调函数。

onChange?性能爆炸!

根据我长期的开发经验,onChange是只要有字符变化就触发一次回调,假设输入了"1234567890",会连续触发10次该函数,绝大部分情况下是不建议这么干的。

但毕竟是官方文档,万一是我想错了呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TextInput()
.onChange((value: string) => {
console.info("[onChange]", value);
})
// [onChange] 1
// [onChange] 12
// [onChange] 123
// [onChange] 1234
// [onChange] 12345
// [onChange] 123456
// [onChange] 1234567
// [onChange] 12345678
// [onChange] 123456789
// [onChange] 1234567890

emmm,我可以将 string 写入到全局变量里、在Button按下时读取全局变量,但肯定不该是这样的,这样频繁地读写 string 造成内存拷贝,是非常粗鲁的写法。

arkui.club 这个网站似乎是arkui爱好者维护的,看起来他们也是用 onChange 来完成这件事的,太粗鲁了。

onSubmit?官方示例误导人!

继续看官方的示例,onSubmit是输入完成时触发的,按照正常人的思路,应该是会返回一个 string 的,虽然将它存到全局变量里不是一个好习惯,但也不是不能接受,便有了以下demo。

1
2
3
4
5
// 官方demo
TextInput()
.onSubmit((EnterKeyType)=>{
console.info(EnterKeyType+'输入法回车键的类型值')
})

emmm,和我想象中的不一样,string呢???submit居然只返回一个“回车键的类型”,这回调有什么意义?除了按下屏幕上的“完成”按钮之外还有什么别的选择?难道是有用户接入了蓝牙键盘然后巧了个“Enter”键?

哇,这API设计得也太细了吧,用户是触摸屏幕还是蓝牙键盘都区分,属实是在边边角角做优化了。

你以为到这就放弃了?仔细看 onSubmit 的文档,发现官方的demo和官方的文档是对不上的,文档里说 onSubmit 接受两个参数 (enterKey: EnterKeyType, event: SubmitEvent) => void,由于 typescript 特有的松弛感,导致单个参数也是能运行的。实际上 event: SubmitEvent 里面就有 text 字段,表示输入的内容,因此官方demo应当调整为:

1
2
3
4
5
6
7
TextInput()
.onSubmit((EnterKeyType, Event)=>{
console.info(EnterKeyType+'输入法回车键的类型值')
console.info('Event.text=', Event.text)
})
// 6输入法回车键的类型值
// Event.text= 123456

如果真是这么简单,我也不会写文章来怒斥 ArkUI 了,请接着往下看,还有高手!

仔细观察数字键盘,右上角有一个“收起键盘按钮”,右下角有一个“完成按钮”,用户使用哪个结束输入完全取决于用户的习惯,onSubmit逆天的地方在于,只有点击 “完成按钮” 的时候才会触发,点击“收起键盘按钮”时不触发。

emmm,也算合理,毕竟 onSubmit 字面意思上是用户彻底完成输入才算,写到一半收起键盘不算,勉强蒙混过关吧。

经过仔细研究,TextInput的65个API里,主要包括这几类:设置属性(字体、颜色),回调函数(onChange、onSubmit)、TextInputOptions(placeHolder、text、controller),我倒要看看这么简单的需求该怎么实现。

官方针对 TextInput 给了无数的示例,但偏偏没有读取文本内容的示例,也是逆天。

  • 示例1(设置与获取光标位置)
  • 示例2(设置下划线)
  • 示例3(设置自定义键盘)
  • 示例4(设置右侧清除按钮样式)
  • 示例5(设置计数器)
  • 示例6(电话号码格式化)
  • 示例7(设置文本断行规则)
  • 示例8(设置文本样式)
  • 示例9(设置文字特性效果)
  • 示例10(自定义键盘避让)
  • 示例11(设置文本自适应)
  • 示例12(设置折行规则)
  • 示例13(实现了插入和删除的效果)
  • 示例14(文本扩展自定义菜单)

@State 绑定属性?单向同步!

在疯狂的搜索后,发现 Text 有一些读写的Demo,例如按下按钮后刷新文本内容,可读可写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Index {
@State message: string = 'Hello World';
build() {
Column() {
Text(this.message)
Button("button")
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.message += "1"
})
}
}
}

@State message 绑定到 TextInput 上,神奇的事情就此发生。用户的键盘输入对 @State message没有任何影响,但程序的按钮读写会对 UI 产生影响,比如这个鬼畜的 GIF。

1
TextInput({ text: this.message })

大致原理就是:无论用户怎么编辑,都不影响内存中 message 的值。

napi追一遍?底层有API,但不对外注册,你气不气!

到这个时候,我是真没办法了,我倒要看看这个输入框到底在哪里存放了数据,直接开始翻 arkui 的代码,https://gitee.com/openharmony/arkui_ace_engine

1
2
3
4
5
6
7
8
9
// frameworks/bridge/declarative_frontend/engine/jsi/nativeModule/arkts_native_api_impl_bridge.cpp
textInput->Set(vm, panda::StringRef::NewFromUtf8(vm, "resetText"),
panda::FunctionRef::New(const_cast<panda::EcmaVM*>(vm), TextInputBridge::ResetText));
textInput->Set(vm, panda::StringRef::NewFromUtf8(vm, "setText"),
panda::FunctionRef::New(const_cast<panda::EcmaVM*>(vm), TextInputBridge::SetText));
textInput->Set(vm, panda::StringRef::NewFromUtf8(vm, "resetController"),
panda::FunctionRef::New(const_cast<panda::EcmaVM*>(vm), TextInputBridge::ResetController));
textInput->Set(vm, panda::StringRef::NewFromUtf8(vm, "setController"),
panda::FunctionRef::New(const_cast<panda::EcmaVM*>(vm), TextInputBridge::SetController));

从注册的API看,确实没有提供 "getText" 系列的API。

再追一下 onChangeonSubmit 的实现,看看数据在哪里,最终在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// frameworks/core/components/text_field/text_field_component.cpp
void TextFieldComponent::SetOnTextChange(const EventMarker& onTextChange)
{
declaration_->SetOnTextChange(onTextChange);
}

void TextFieldComponent::SetOnTextChangeFunction(std::function<void(const std::string&)>&& onTextChangeCallback)
{
declaration_->SetOnTextChangeFunction(std::move(onTextChangeCallback));
}

// frameworks/bridge/declarative_frontend/jsview/js_textfield.cpp
JSRef<JSVal> JSTextField::CreateJsOnChangeObj(const PreviewText& previewText)
{
JSRef<JSObject> previewTextObj = JSRef<JSObject>::New();
previewTextObj->SetProperty<int32_t>("offset", previewText.offset);
previewTextObj->SetProperty<std::u16string>("value", previewText.value);
return JSRef<JSVal>::Cast(previewTextObj);
}

// frameworks/core/components_ng/pattern/text_field/text_field_model_ng.cpp
void TextFieldModelNG::SetOnChange(FrameNode* frameNode, std::function<void(const std::u16string&,
PreviewText&)>&& func)
{
CHECK_NULL_VOID(frameNode);
auto eventHub = frameNode->GetEventHub<TextFieldEventHub>();
CHECK_NULL_VOID(eventHub);
eventHub->SetOnChange(std::move(func));
}


// frameworks/core/components_ng/pattern/text_field/text_field_event_hub.h
void SetOnChange(std::function<void(const std::u16string&, PreviewText&)>&& func)
{
onChange_ = std::move(func);
}

// frameworks/core/common/ime/text_edit_controller.h
void SetText(const std::string& newText, bool needFireChangeEvent = true);
void SetHint(const std::string& hint);
void SetSelection(const TextSelection& selection);
void Clear();
const std::string& GetText() const;
const TextSelection& GetSelection() const;

大概就是:TextEditController里存放的,使用 GetText 可以拿到。

是不是和ArkUI的设置text、设置hint、设置选中对上了?诶,底层有 GetText,但就是不给你用,你气不气?你气不气?你气不气?

官方APP[短信]会怎么做?居然是onChange?

到这时候,其实已经很明白了,TextInput 狗都不用,我已经无法想象各大主流APP在开发鸿蒙5.0时开发者到底有多崩溃了,由于大厂APP肯定不开源,我去研究一下“短信APP”是怎么实现文本编辑的。

不看不知道,一看吓一跳,鸿蒙的官方APP用的也是 onChange,好家伙,我直呼好家伙。

只能说,我开发水平不足以理解这种高深的写法。

在神仙 4qwerty7 的反复尝试后,翻到了塞到某个犄角旮旯里的文档

难道,就此放弃了吗?

每当我遇到奇奇怪怪的无法解决的问题,都会向 4老师 寻求意见,一边怒斥 ArkUI 的变量绑定做得烂,一边搞了好几个小时没搞出来,本来以为 4老师 也要经历滑铁卢了,突然晚饭时候和我说有方案了。

我也不知道他是怎么找到的,$$语法:内置组件双向同步 ,反正就,用了这个语法就会自动刷新变量中的值了,甚至demo还真是 TextInput 做的。

1
2
3
@State message: string = '';
TextInput({ text: this.message }) // 单向同步,写入message时,从内存同步到UI
TextInput({ text: $$this.message }) // 双向同步,读写message均是最新的值

这个 $$ 是个什么东西我也不太懂,自定义的刁钻语法糖,只能说,4老师牛逼……

技术总结

如果要在UI和内存中双向同步一个数据,需要使用 $$ 来修饰 State变量(不代表以后能用,懂的都懂,不能明说),demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entry
@Component
struct Index {
@State message: string = '';

build() {
Column() {
TextInput({ text: $$this.message })
.fontSize(50)
.type(InputType.Number)
.fontWeight(FontWeight.Bold)
Button("点我追加一")
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
console.log("current value", this.message)
this.message += "1"
})
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}

吐槽总结

本文就是用于输出情绪的,从一个简单的功能来有理有据地批评 ArkUI 到底是怎样的一坨,毫不夸张地讲,两位顶尖的工程师都得花这么久的时间才能研究明白,其他菜鸟工程师解决起来只会更费劲。

更进一步,以 ArkUI 这个样子,显然是鸿蒙生态的路上一道阻碍,我也不是第一次用了,私下里用一次骂一次、用一次骂一次,本以为是我太菜了,经过这次事件我深刻认识到,ArkUI就是一坨,不接受反驳。