问题描述
TextField限制长度时, 当你的输入字符长度已经到了最大值-1时,使用第三方手写输入法或者ios原生拼音输入法输入liang(什么拼音都行,这里只是举例),输到i那么li都会消失。
原因分析
这是因为第三方手写输入法或者ios原生拼音输入法,虽然还没选中哪个汉子,但是输入的拼音字母已经显示在输入框了,那么这个字符串就会算作已经输入了,再计算TextField字符串长度的时候会加上未选中的拼音,跟最大长度作对比,字符数比最大值还要大的时候就会因为TextField的判定机制使未选中的字符消失。
解决方法
TextField除了使用maxLength设置最大长度外,还可以使用inputFormatters限制:
TextField(
controller: _controller,
inputFormatters: [
LengthLimitingTextInputFormatter(maxLength),
],
......
源码分析
LengthLimitingTextInputFormatter是flutter原生的类,代码如下:
class LengthLimitingTextInputFormatter extends TextInputFormatter {
LengthLimitingTextInputFormatter(
this.maxLength, {
this.maxLengthEnforcement,
}) : assert(maxLength == null || maxLength == -1 || maxLength > 0);
final int? maxLength; //传入的最大长度
//确定应如何强制执行 maxLength 限制,默认为 MaxLengthEnforcement.enforced
final MaxLengthEnforcement? maxLengthEnforcement;
//获取默认的maxLengthEnforcement,总共有三种,
//有兴趣可以看文章最后或者看源码注释研究一下,这个问题的解决方法不需要这个方法
static MaxLengthEnforcement getDefaultMaxLengthEnforcement([
TargetPlatform? platform,
]) {
if (kIsWeb) {
return MaxLengthEnforcement.truncateAfterCompositionEnds;
} else {
switch (platform ?? defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.windows:
return MaxLengthEnforcement.enforced;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.fuchsia:
return MaxLengthEnforcement.truncateAfterCompositionEnds;
}
}
}
//截断字符串
@visibleForTesting
static TextEditingValue truncate(TextEditingValue value, int maxLength) {
final CharacterRange iterator = CharacterRange(value.text);
if (value.text.characters.length > maxLength) {
iterator.expandNext(maxLength);
}
final String truncated = iterator.current;
return TextEditingValue(
text: truncated,
selection: value.selection.copyWith(
baseOffset: math.min(value.selection.start, truncated.length),
extentOffset: math.min(value.selection.end, truncated.length),
),
composing: !value.composing.isCollapsed && truncated.length > value.composing.start
? TextRange(
start: value.composing.start,
end: math.min(value.composing.end, truncated.length),
)
: TextRange.empty,
);
}
//顾名思义,更新字符串,每次输入的值发生改变时都会调用。
//根据字符串最大值以及maxLengthEnforcement判断返回的是什么。
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final int? maxLength = this.maxLength;
if (maxLength == null ||
maxLength == -1 ||
newValue.text.characters.length <= maxLength) {
return newValue;
}
assert(maxLength > 0);
switch (maxLengthEnforcement ?? getDefaultMaxLengthEnforcement()) {
case MaxLengthEnforcement.none:
return newValue;
case MaxLengthEnforcement.enforced:
if (oldValue.text.characters.length == maxLength && oldValue.selection.isCollapsed) {
return oldValue;
}
return truncate(newValue, maxLength);
case MaxLengthEnforcement.truncateAfterCompositionEnds:
if (oldValue.text.characters.length == maxLength &&
!oldValue.composing.isValid) {
return oldValue;
}
if (newValue.composing.isValid) {
return newValue;
}
return truncate(newValue, maxLength);
}
}
}
输入框显示的是什么是formatEditUpdate可以决定的,使用这次的更改主要是formatEditUpdate。
解决代码
参考LengthLimitingTextInputFormatter写一个新的TextInputFormatter。
这里只改了formatEditUpdate,以及不需要判定maxLengthEnforcement,所以把相关代码也删了。
class MyLengthLimitingTextInputFormatter extends TextInputFormatter{
final int? maxLength;
MyLengthLimitingTextInputFormatter(this.maxLength) : assert(maxLength == null || maxLength == -1 || maxLength > 0);
@visibleForTesting
static TextEditingValue truncate(TextEditingValue value, int maxLength) {
final CharacterRange iterator = CharacterRange(value.text);
if (StringUtil.getTextLength(value.text) > maxLength) {
iterator.expandNext(maxLength);
}
final String truncated = iterator.current;
return TextEditingValue(
text: truncated,
selection: value.selection.copyWith(
baseOffset: math.min(value.selection.start, truncated.length),
extentOffset: math.min(value.selection.end, truncated.length),
),
composing: !value.composing.isCollapsed && truncated.length > value.composing.start
? TextRange(
start: value.composing.start,
end: math.min(value.composing.end, truncated.length),
)
: TextRange.empty,
);
}
//oldValue就是显示在输入框上的旧值,newValue就是显示在输入框上的旧值加你新输入的字符串
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final int? maxLength = this.maxLength;
//加上你新输入的字符串都没大于限制的最大值就直接在输入框显示新字符串
if (maxLength == null ||
maxLength == -1 ||
newValue.text.characters.length <= maxLength) {
return newValue;
}
assert(maxLength > 0);
//已经达到最大值且新输入的字符串不是正在输入的状态
//(真正输入存活状态:拼音拼出的字还没选中(下面会有下划线)或者手写的字还没在输入法选中(下面会有下划线);
//存活状态就是反过来,选中了或者输入英文的时候就是打出的每个字母都直接到输入框,下面也不会有下划线) ,直接返回旧的值
if (oldValue.text.characters.length == maxLength && !newValue.composing.isValid) {
return oldValue;
}
//已经达到最大值且新输入的字符串是正在输入的状态,显示旧值+你新输入的值=newValue,
//就是这句解决了拼音会被吞的问题
if (oldValue.text.characters.length == maxLength && newValue.composing.isValid){
return newValue;
}
// Enforced to return a truncated value.
return truncate(newValue, maxLength);
}
}
使用
将LengthLimitingTextInputFormatter换成MyLengthLimitingTextInputFormatter就行。
TextField(
controller: _controller,
inputFormatters: [
MyLengthLimitingTextInputFormatter(maxLength),
],
......
缺陷
但是这样的解决方法还有一个问题:只要你英文打的够快,然后快速选择还在输入法上没消失的待选择的字母,它就能吞了前面的字。
附上这个问题没用到的:maxLengthEnforcement的区别
源码:
enum MaxLengthEnforcement {
/// No enforcement applied to the editing value. It's possible to exceed the
/// max length.
none,
/// Keep the length of the text input from exceeding the max length even when
/// the text has an unfinished composing region.
enforced,
/// Users can still input text if the current value is composing even after
/// reaching the max length limit. After composing ends, the value will be
/// truncated.
truncateAfterCompositionEnds,
}
none:限制长度后还是可以随便输入 ,设了等于没设;
enforced:当字符长度达到限制长度时,正在输入的字符停止输入后会强制消失,例如输入英文字符停手后就会从输入法消失
truncateAfterCompositionEnds:当字符长度达到限制长度时,但正在输入的字符强制不会消失,也就是输入法里面预选的字不会消失
另一个方法
网上找到的另一个我还没验证的方法:
https://juejin.cn/post/6844904096407765005