在学习Flutter的过程中经常会遇到一些小坑,由于目前Flutter还不够完善,有些问题还是要我们自己想办法解决的,下面就分享一个关于TextField的问题,如有错误,请指出。
首先先上个被软键盘遮蔽的问题
TextField弹出系统软键盘时,如果被遮蔽是不会自动上移的。这对于开发惯了原生的同学来说简直是不能原谅的,网上有一些很复杂的自定义插件方案可以完美解决,其实我们偷懒的话只需要在最外层嵌套一个ScrollView组件里就可以了,无论是ListView还是SingleChildScrollView,这样基本解决了被遮蔽的问题,如果还需要控制滑动多少则需要对ScrollView的controller进行设置了。
开胃菜吃完了,现在进入正餐
在一些业务场景里我们有时候是不希望弹出系统的软键盘的,比如输入密码、搜索等情况,但是Flutter并没有给我们提供这样的属性,那我们要如何做到呢?
首先,我们先观察一下TextField是怎么弹出键盘的。从最直观的手机界面看,当TextField获得焦点时弹出了,失去焦点时消失了,而且弹出的键盘是系统原生的键盘。现在我们猜测一下,他是通过焦点监听来决定系统键盘状态的,带着这2个猜想我们看看TextField的代码是怎么写的。
由于源码比较多,我就直接上图了
随便定义一个TextField组件点进去,来到text_field.dart文件中,构造函数中一大串设置属性,不急,先看下这个类有什么继承,实现什么的
哦,很好,很简单,那么我们可以往下找他的_state类了
继续点进_TextFieldState类中,一路翻下来,看到了很多私有方法,还有生命周期的重写,我们先不关心里面的逻辑,先找到最重要的build方法
嗯,很长,在return里找到最里层的child
点过去
这里他建了一个EditableText类,有点熟悉的名字了,点过去,来到editable_text.dart文件中
focusNode通常是flutter里处理焦点的,是不是和我们观察界面时总结的焦点监听有点联系呢?带着疑问,我们追踪一下这个属性,最后我们在_state类里找到了
PS:这里插队一条小知识
在追踪的过程里发现了这个,回过去看了一下
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText> implements TextInputClient, TextSelectionDelegate {}
这是mixin了AutomaticKeepAliveClientMixin需要重写的方法,这样用来保证输入框在有焦点时状态不会被UI刷新时重置,是不是联想到了TabBarView在切换时又被重置的问题了~方法已经教你了,剩下的你懂的
继续看焦点的问题,从widget类中拿到了focusNode对象,进行了焦点监听,通过_handleFocusChanged方法实现
这么接地气的方法名,大概猜出来了吧,继续点...
当获得焦点的时候open当没焦点时close,我们是想阻止他在获得焦点时不弹键盘,所以我们只需要看open这个方法
上面一堆我们也不知道啥意思,我们也看不懂,但是他最后都会调用show方法,是不是show键盘呢??我们过去看看
/// An interface for interacting with a text input control.
///
/// See also:
///
/// * [TextInput.attach]
class TextInputConnection {
TextInputConnection._(this._client)
: assert(_client != null),
_id = _nextId++;
static int _nextId = 1;
final int _id;
final TextInputClient _client;
/// Whether this connection is currently interacting with the text input control.
bool get attached => _clientHandler._currentConnection == this;
/// Requests that the text input control become visible.
void show() {
assert(attached);
SystemChannels.textInput.invokeMethod<void>('TextInput.show');
}
/// Requests that the text input control change its internal state to match the given state.
void setEditingState(TextEditingValue value) {
assert(attached);
SystemChannels.textInput.invokeMethod<void>(
'TextInput.setEditingState',
value.toJSON(),
);
}
/// Stop interacting with the text input control.
///
/// After calling this method, the text input control might disappear if no
/// other client attaches to it within this animation frame.
void close() {
if (attached) {
SystemChannels.textInput.invokeMethod<void>('TextInput.clearClient');
_clientHandler
.._currentConnection = null
.._scheduleHide();
}
assert(!attached);
}
}
这是text_input.dart文件里的TextInputConnection类,一共就这么多内容,从我们点过来的show方法可以看出他调用了SystemChannels.textInput里的方法,看到System是不是菊花一紧,这是要和系统打交道了吗??点过去看看吧
这个方法在platform_channel.dart文件下,看过原生混合开发的同学大概都猜出了,这是在和原生系统进行交互了,到这一步我们观察UI表现时的2个猜想都被证实了。
看到这里已经有同学知道怎么做了,要禁止弹出键盘只需要把show方法里的调用给禁掉就好啦
和java不同的是,flutter的源码是可以直接修改的,你在源码文件中随便敲个字就会弹出
选择一个你想要的方式就可以直接修改源码了,我们现在选择第一个,再把show方法里的实现屏蔽掉来验证下是不是真的可以不弹出键盘
/// Requests that the text input control become visible.
void show() {
assert(attached);
// SystemChannels.textInput.invokeMethod<void>('TextInput.show');
}
测试发现真的不弹了!但是我不是想让所有输入框都不弹键盘啊,我的登陆框怎么办??难道我要把这么多文件复制出来自定义一个输入框专门用来不弹??当然这是可以的,但是这一点也不优雅!
通过上面这么多源码的分析,我们大概已经知道了flutter是怎么控制的了,那么我们可以自己定义一个属性给TextField,按着上面的流程一步一步传递过去,用来控制最终的show方法,我们想弹就弹,不想弹就禁止,一劳永逸。
最后的调用代码:
void show({bool needshow = true}) {
assert(attached);
if(needshow){
SystemChannels.textInput.invokeMethod<void>('TextInput.show');}else {close();}
}
注意需要在else的逻辑里调用原本类中的close()方法,这样就算一个要弹键盘,一个不要弹键盘的输入框在一个界面也不会出问题啦