前言
在往期的分享中,小编介绍了如何通过 flutter 自带的 EditableText 实现扫码枪数据源的获取。大致实现如下:
- 扫码枪本质上是一个外接的输入设备。
- 使用 Stack 结合自己的布局控件 childWidget 将 editableText 封装,控制隐藏。可通过监听 onSubmitted 获取扫码枪的输入内容。
痛点问题
回顾 往期分享 痛点问题 :
使用 EditableText 的过程中遇到了系统键盘弹出的问题。我们通过 Edit 的焦点来获取扫码枪的输入。但 EditableText 一旦获取了焦点,内部会调用原生层唤起键盘。
扫码枪触发焦点后,系统键盘自动弹起。这样的失败交互困扰了小编很久。
- 往期分享中的临时方案
之前的处理方式是通过定制化源码的方式,将指定版本内的
TextInput.show
手动注释掉。
PS:这是一个笨方法,只能解燃眉之急,输入框和文本,一直都是官方每个版本改动的重点。指定版本不是长久的方案。
如何在不改动源码的方式下,动态控制焦点是否触发键盘弹出?
1.系统键盘弹出的原因
实际上,系统键盘是否弹出,完全是因为 SystemChannels.textInput.invokeMethod<void>('TextInput.show')
的调用,但是我们不可能去每个调用该方法地方去做处理,那么这个方法执行后续,我们有办法拦截吗? 答案当然是有的。
2. 如何拦截 methodChannel
Flutter 的 Framework 层发送信息 TextInput.show 到 Flutter 引擎是通过 MethodChannel, 而我们可以通过重载 WidgetsFlutterBinding 的 createBinaryMessenger 方法来处理Flutter 的 Framework 层通过 MethodChannel 发送的信息。
具体代码如下:
使用 mixin 对 WidgetsFlutterBinding 进行方法重载
mixin TextInputBindingMixin on WidgetsFlutterBinding {
@override
BinaryMessenger createBinaryMessenger() {
return TextInputBinaryMessenger(super.createBinaryMessenger());
}
}
在 main 方法中初始化这个 binding
class TextInputBinding extends WidgetsFlutterBinding with TextInputBindingMixin {}
void main() {
TextInputBinding();
runApp(const MyApp());
}
自定义 TextInputBinaryMessager 对 methodChannel 进行自定义拦截操作
class TextInputBinaryMessenger extends BinaryMessenger {
TextInputBinaryMessenger(this.origin);
final BinaryMessenger origin;
// Flutter 的 Framework 层发送信息到 Flutter 引擎,会走这个方法
@override
Future<ByteData?>? send(
String channel,
ByteData? message,
) {
//TODO 拦截处理
}
// Flutter 引擎 发送信息到 Flutter 的 Framework 层的回调,无需处理
@override
void setMessageHandler(
String channel,
MessageHandler? handler,
) {
... 省略
}
//无需处理
@override
Future<void> handlePlatformMessage(
String channel,
ByteData? data,
PlatformMessageResponseCallback? callback,
) {
... 省略
}
}
send 方法:flutter 的 framework 层发送信息到 flutter 引擎,会走这个方法,这也是我们需要的处理的方法。
3. 拦截思路
可以根据我们的需求处理 send 方法了。当 channel
为 SystemChannels.textInput
的时候,根据方法名字来拦截 TextInput.show
。
再定义一个特别的 FocusNode,并且定义好一个属性用于判断(也有那种需要随时改变是否需要拦截信息的需求)。例如 TextInputFocusNode
:
import 'package:flutter/material.dart';
class TextInputFocusNode extends FocusNode {
bool ignoreSystemKeyboardShow = true;
}
根据思路,我们的拦截方法实现如下:
@override
Future<ByteData?>? send(
String channel,
ByteData? message,
) {
if (channel == SystemChannels.textInput.name) {
final methodCall = SystemChannels.textInput.codec.decodeMethodCall(
message,
);
switch (methodCall.method) {
case 'TextInput.show':
final FocusNode? focus = FocusManager.instance.primaryFocus;
if (focus != null &&
focus is TextInputFocusNode &&
focus.ignoreSystemKeyboardShow) {
return Future.value(
SystemChannels.textInput.codec.encodeSuccessEnvelope(null),
);
}
break;
default:
break;
}
}
return origin.send(channel, message);
}
扫码库更新
小编已将本次的方案调整重新发布上传,使用方式如下:
- 在pubspec.yaml文件中进行引用:
dependencies:
scan_gun: ^2.0.0
- 提供
ScanMonitorWidget
作为父节点,嵌套使用:
ScanMonitorWidget({
Key? key,
required ChildBuilder childBuilder,
TextInputFocusNode? scanNode,
FocusNode? textFiledNode,
required void Function(String) onSubmit,
})
- 在 main 方法中初始化 TextInputBinding
void main() {
TextInputBinding();
runApp(const MyApp());
}
参数说明:
-
childBuilder :
typedef ChildBuilder = Widget Function(BuildContext context)
,使用者自己UI作为子节点 -
scanNode:
非必传,如果传,可通过 scanNode
监听获取当前扫码可用状态,hasFocus
时为获取焦点
- GlobalKey scanKey:
非必传,如果传,可通过 'scanKey' 强制获取获取焦点,保证扫码可用,如下
scanKey.currentState?.requestKeyboard()
- textFiledNode:
提供外部存在输入框键盘输入与扫码输入同时存在的场景。内部做了焦点切换能力,保证输入框焦点取消后,能马上切换成扫码枪的焦点
扩展内容
1.有焦点,却获取不到扫码
小编在开发过程中遇到过一个现象。
明明 FocusNode 节点拥有焦点,扫码枪进行扫码操作后,输入框确拿不到内容。
在此记录一下问题所在,如下,以 EditableText 为例,我们看源码中提供一个方法 requestKeyboard
,用于唤起键盘接收键盘输入源
结论:对于输入框来说,获取到键盘输入源需要两个条件
- 拥有焦点
- 建立开启连接
2. 扫码枪触发联想输入(扫码无响应)
原因: 输入框输入法使用了中文输入法,扫码数据包含 英文 + 数字 时,会出发系统的联想输入,造成扫码无响应、数据错误等问题。
处理方式:
在 EditableText 中添加属性 keyboardType: TextInputType.visiblePassword
, 这样便不会触发联想输入。
现存问题:
- 某些输入法,例如讯飞输入法,扫码会丢失内容,原因是它在内容中会插入Enter,提前结束扫码
解决思路: keyboardType 设置为 TextInputType.multiline。不唯一依赖 onSubmit 标识扫码完成,因为扫码枪的输入速度是很快的。在每次 onChanged 使用 Timer 判断 200 毫秒 value 是否还有更新,如无更新,则认为扫码已完成,设置一个方法把扫码内容 callback 出来。
大致代码如下:
EditableText(
keyboardType: TextInputType.multiline,
onSubmitted: (result){
checkScanFinish();
},
onChanged: (value){
checkScanFinish();
},
)
Timer? checkScanFinishTimer;
// 扫码枪输入与手动输入的区别在于输入的速度
void checkScanFinish() {
checkScanFinishTimer?.cancel();
checkScanFinishTimer = Timer(
Duration(milliseconds: 200),
() {
// callback 出去
_onSubmit(controller.text);
},
);
}
- 还是无法避免个别输入法中文模式下,会触发关联联想,造成内容不正确
3. 关于扫码的知识点调研
-
支持 Android 热插拔USB扫描枪会在有EditText时,扫描枪扫描内容自动输入到编辑框了,但是有很多输入法兼容的问题,比如搜狗输入法识别到HID设备时会隐藏无法弹出,如果输入法切换成中文时会输入中文等等。 HID 模式,对非数字支持不佳,与输入法相关,在某些时候会触发英文联想-_-||,与虚拟键盘会发生冲突,连接扫码枪时需要切换键盘输入法输入。
-
通过串口的方式直接获取原始数据,不再跟输入法产生冲突,可惜设备是USB HID的,通过大量的尝试(包括USB虚拟串口)都不支持
-
扫码枪是基于键盘输入的,可以尝试从获取焦点的Activity中的 dispatchKeyEvent(KeyEvent event) 进行拦截,代码大致如下:
// 可惜只能解决掉中文的问题,事件还是先走到输入法才能回到Activity。
// 有遇到输入法冲突问题(讯飞输入法),现象是当页面带输入框且带焦点时,扫码内容中字母不触发 ACTION.DOWN 只触发 ACTION.UP
private val idBufferResultMap = mutableMapOf<Int, StringBuffer>()
... 省略
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val keyCode = event.keyCode
if ((event.device?.id ?: 0) > 0) {
if (event.action == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
//若为回车键,直接返回
if (idBufferResultMap[event.device.id] != null) {
val bf = idBufferResultMap.remove(event.device.id)
if (!TextUtils.isEmpty(bf?.toString())) {
sendEvent(bf.toString())
Log.d("rex ---> scan ---> ", bf.toString())
}
}
return super.dispatchKeyEvent(event)
}
val pressedKey = event.unicodeChar.toChar()
if (pressedKey.code != 0) {
if (idBufferResultMap[event.device.id] == null) {
idBufferResultMap[event.device.id] = StringBuffer()
}
idBufferResultMap[event.device.id]?.append(pressedKey)
}
return super.dispatchKeyEvent(event)
}
}
return super.dispatchKeyEvent(event)
}
- 相较在 Activity 中重写 dispatchKeyEvent,使用 AccessibilityService 重写 onKeyEvent 可以优先获取到键盘事件,但需要到系统设置->无障碍->服务 开启当前服务。
使用Android无障碍服务实现扫码数据获取可参考:
Github:github.com/liyufengrex…
Gitcode:gitcode.com/cashier/acc…
- 厂家扫码枪广播监听的方式
-
常见扫码枪广播:blog.51cto.com/lwc0329/491…
Example
GitHub: github.com/liyufengrex…
GitCode: gitcode.com/liyufengrex…