问题
问题描述:当Flutter的输入框中支持上了表情符号(emoji),无论你用maxLength还是inputFormatters属性,都会出现长度超过你给定的值或表达式,而且光标还会在达到最后字符的时候往前移动一个字符。以下都是围绕当输入框有表情符号开展的。
问题原因:原本我是以为光标的rect数据,所以在text_painter.dart类看了Rect _getRectFromDownstream(int offset, Rect caretPrototype)方法,用来计算光标的矩阵。(文字属性是downStream的方法走这个方法,有对应的一个是upstream的方法的)然后在追查到最后的editable.dart的_paintCaretPaint方法绘制里,发现出错在rect矩阵数据里,绘制没有任何问题。然后回到计算光标矩阵的地方,在观察到往前移动的光标异常问题是因为offset引起的。
根本原因:无论是maxLength还是inputFormatters最后都是以inputFormatters作为EditableText的构造函数,在EditableText没有处理好限字符的问题。这里主要就是看LengthLimitingTextInputFormatter类了,因为EditableText都是用各种inputFormatters处理文案的问题。
![](https://img.haomeiwen.com/i14517298/ba063d7146dc7eec.png)
字符比限制的数字多一个原因:由于没有处理好最大字符数的问题,当flutter/engine的原生输入框数据返回时候就是用户真实输入的数据,所以到了EditableText更新text也是不做长度判断,所以才会导致比限制的数字多一个的问题。
光标前移原因:这里是因为flutter层在计算最大字符数时候,触发刷新,native又返回一次输入框的数据,这里可以在text.input.dart类中_handleTextInputInvocation(MethodCall methodCall)中返回数据解析生成的TextEditingValue中光标位置就是用户输入的maxLength。
如何解决
通过上面的问题大概知道就是当文字中混有表情符号(emoji)时候,flutter计算text时候就会出现漏洞。这里先补给下最重要的知识点,全面认识TextEditingValue类。
TextEditingValue
TextEditingValue类是所有输入框封装文案与光标的基类。TextEditingValue里面的属性分别都有自己用处。
text:输入框要展示的文案。
作用:展示文案的内容。
selection:TextSelection类(affinity:文字的TextAffinity属性;baseOffset:字符开始的偏移量(有光标情况:光标的起始位置);extentOffset:字符插入的位置(有光标情况:光标的结束位置)
作用:用户选择插入字符的位置。
composing:TextRange类(int start,int end)。start是文本编辑的起始位置,end是文本编辑的结束位置。
作用:用来判断该文本是否还在编辑状态,如果还在编辑状态,native层在_handleTextInputInvocation返回时候,将不带上TextRange的start和end标识的字符串。(这个属性专门用来处理iOS自带的输入框问题,会在章节《遇到的问题》中描述,坑爹)
解决办法
-
从源码中能看到EditableText每次接收到新的字符串内容,都会在_formatAndSetValue方法调用onChanged(String text)方法。其中可以看到这里inputFormatters没有处理好带表情的TextEditingValue,所以我们要做的就是在onChange方法中处理好TextEditingValue。
_formatAndSetValue方法
- 方法一(能达到超长的回调,也是比较挫的方法)
- 最好自己封装一个XXXTextField类,统一处理这类问题。或者自己写一个inputFormatters,把官网的弥补下。现在我先介绍我自己比较挫的解决办法先。
在封装的类中增加两个属性
//最大长度
final int selfMaxLength;
//达到最大长度后的回调
final Function maxLengthCallBack
class _XXXTextFieldState extends State<XXXTextField> {
……
//是否已经触发过一次超长
bool hasAlreadyMaxLength = false;
//第一次触发超长的文案
String lastMaxContent = "";
@override
void initState() {
if (widget.controller.text.length == widget.selfMaxLength) {
//UI带进来的文案超长,记录
hasAlreadyMaxLength = true;
lastMaxContent = widget.controller.text;
} else {
//UI带进来的文案没有超长
hasAlreadyMaxLength = false;
lastMaxContent = "";
}
}
//maxLength or inputFormatters正常设置不影响
@override
Widget build(BuildContext context) {
return Container(
child: TextField(
key: widget.key,
……
onChanged: (String value) {
setState(() {});
if (widget.onChanged != null) {
widget.onChanged(value);
}
_actionMaxLengthState(value);
},
……
)
)
}
/***真正处理逻辑***/
//第一次文案超长情况下,把光标定位到最后一个符号后面,裁切native返回的文案
void _resetSelection(String newText) {
var sRunes = newText.runes;
String result;
for (int i = 0; i < sRunes.length; i++) {
if (String.fromCharCodes(sRunes, 0, sRunes.length - i).length <= widget.selfMaxLength) {
result = String.fromCharCodes(sRunes, 0, sRunes.length - i);
if (result.runes.last == 105) {
//如果删除后剩下的还有一个空格,继续删除
result = String.fromCharCodes(result.runes, 0, result.runes.length - 1);
}
break;
}
}
TextSelection temp = widget.controller.value.selection.copyWith(
baseOffset: result.length,
extentOffset: result.length,
);
TextRange fixRange = widget.controller.value.composing;
if (widget.controller.value.composing.end > result.length) {
fixRange = TextRange(start: fixRange.start, end: result.length);
}
widget.controller.value = TextEditingValue(text: result, selection: temp, composing: fixRange);
lastMaxContent = result;
}
//当用户在超长情况下,继续怎样输入,显示第一次超长的文案,把光标定位到最后个字符位置。
void _initOldDataSelection(String newText) {
TextSelection actualSelection = widget.controller.value.selection;
actualSelection = widget.controller.value.selection.copyWith(
baseOffset: lastMaxContent.length,
extentOffset: lastMaxContent.length,
);
//ios:当TextRange不为-1时候,下次update会把start和end直接的变量值全部丢弃。当你确定内容不变时候,请把他们变成-1
TextRange fixRange = TextRange(start: -1, end: -1);
widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: fixRange);
}
void _actionMaxLengthState(String newText) {
if (newText.length >= widget.selfMaxLength) {
if (widget.maxLengthCallBack != null) widget.maxLengthCallBack();
if (hasAlreadyMaxLength) {
_initOldDataSelection(newText);
} else {
hasAlreadyMaxLength = true;
_resetSelection(newText);
}
} else {
//对controller的text设置,一定要是对value改变,要不然直接设置text,selection就是为-1默认值。
widget.controller.value =
TextEditingValue(text: newText, selection: widget.controller.value.selection, composing: widget.controller.value.composing);
//如果用户删除文案,达不到超长效果,就清除超长数据
hasAlreadyMaxLength = false;
lastMaxContent = "";
}
}
}
-
调用时候就用封装的XXXTextField类,然后添加selfMaxLength最大长度和maxLengthCallBack最大长度返回参数(callBack自己看业务吧)。
调用示例
- 方法二:
自定义LengthLimitingTextInputFormatter,道理和上面挫的方法差不多,这里就不一一解析了。如果用这个也要获取超长回调,直接设置个callBack即可,直接看代码
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:flutter/services.dart';
class MTLengthLimitingTextInputFormatter extends TextInputFormatter {
MTLengthLimitingTextInputFormatter(this.maxLength) : assert(maxLength == null || maxLength == -1 || maxLength > 0);
final int maxLength;
bool hasAlreadyMaxLength = false;
///超过文本长度回调
final Function maxLengthCallBack;
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, // unused.
TextEditingValue newValue,
) {
hasAlreadyMaxLength = oldValue.text.length >= maxLength;
if (!hasAlreadyMaxLength && maxLength != null && maxLength > 0 && newValue.text.length >= maxLength) {
///第一次超长
if(maxLengthCallBack != null) maxLengthCallBack();
return _resetSelection(newValue);
} else if (hasAlreadyMaxLength && maxLength != null && maxLength > 0 && newValue.text.length >= maxLength) {
///第二次往后超长
if(maxLengthCallBack != null) maxLengthCallBack();
return _initOldDataSelection(oldValue, newValue);
} else {
hasAlreadyMaxLength = false;
return newValue;
}
}
TextEditingValue _resetSelection(TextEditingValue newValue) {
hasAlreadyMaxLength = true;
var sRunes = newValue.text.runes;
String result;
int i = 0;
for (i = 0; i < sRunes.length; i++) {
if (String.fromCharCodes(sRunes, 0, sRunes.length - i).length <= maxLength) {
result = String.fromCharCodes(sRunes, 0, sRunes.length - i);
if (result.runes.last == 105) {
//如果删除后剩下的还有一个空格,继续删除
result = String.fromCharCodes(result.runes, 0, result.runes.length - 1);
}
break;
}
}
TextSelection temp = newValue.selection.copyWith(
baseOffset: result.length,
extentOffset: result.length,
);
TextRange fixRange = newValue.composing;
if (newValue.composing.end > result.length) {
fixRange = TextRange(start: fixRange.start - i, end: result.length);
}
return TextEditingValue(text: result, selection: temp, composing: fixRange);
}
TextEditingValue _initOldDataSelection(TextEditingValue oldValue, TextEditingValue newValue) {
TextSelection actualSelection = newValue.selection;
actualSelection = newValue.selection.copyWith(
baseOffset: oldValue.text.length,
extentOffset: oldValue.text.length,
);
//ios:当TextRange不为-1时候,下次update会把start和end直接的变量值全部丢弃。当你确定内容不变时候,请把他们变成-1
TextRange fixRange = TextRange(start: -1, end: -1);
return TextEditingValue(text: oldValue.text, selection: actualSelection, composing: fixRange);
}
}
遇到的问题
一、iOS自带原生输入框,当用户输入拼音,点击中文的时候,输入框中会显示类似:w d我的。这种明显处于bug的问题让我一个安卓菜鸡一脸懵逼。后面仔细看了源码的参数,才发现我的TextEditingValue少了一个composing属性,这个属性就是用来告诉 iOS 的native层哪些文案还在编辑,等用户选中要的文案后便可删除。所以后面我给加了composing属性。
出问题代码(就用_actionMaxLengthState举例子,其他函数看上面解决办法即可):
void _actionMaxLengthState(String newText) {
if (newText.length >= widget.selfMaxLength) {
……
} else {
//对controller的text设置,一定要是对value改变,要不然直接设置text,selection就是为-1默认值。
widget.controller.value = TextEditingValue(text: newText, selection: widget.controller.value.selection);
hasAlreadyMaxLength = false;
lastMaxContent = "";
}
}
修复后的代码:
void _actionMaxLengthState(String newText) {
if (newText.length >= widget.selfMaxLength) {
……
} else {
//对controller的text设置,一定要是对value改变,要不然直接设置text,selection就是为-1默认值。
widget.controller.value =
TextEditingValue(text: newText, selection: widget.controller.value.selection, composing: widget.controller.value.composing);
hasAlreadyMaxLength = false;
lastMaxContent = "";
}
}
二、在第一个问题解决后,紧接着在iOS上遇到第二个问题。当用户多次输入超长内容后(内容中包含表情),会出现表情后的文案全部消失。这里出现的问题还是composing,没有完全理解含义,直接使用native返回的composing所致。
出问题代码(_initOldDataSelection方法):
void _initOldDataSelection(String newText) {
……
widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: widget.controller.value.composing);
}
修复后的代码(修复后的意思即是用户多次超长的内容,我帮用户恢复,属于文本不在编辑状态,iOS的native层不能把我的内容删除):
void _initOldDataSelection(String newText) {
……
//ios:当TextRange不为-1时候,下次update会把start和end直接的变量值全部丢弃。当你确定内容不变时候,请把他们变成-1
TextRange fixRange = TextRange(start: -1, end: -1);
widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: fixRange);
}
三、文案剪切出现崩溃。原因是我通过substring()方法来剪切,众所周知表情都是由多个字符拼接而成的表达式,如果刚好最后一个是表情,我把其中表情一个字符剪切掉了,导致无法正常显示表情符号,自然会崩溃。
出问题代码(_initOldDataSelection方法):
void _resetSelection(String newText) {
String result = newText.substring(0, widget.selfMaxLength);
……
}
修复的代码:
void _resetSelection(String newText) {
var sRunes = newText.runes;
String result;
for (int i = 0; i < sRunes.length; i++) {
if (String.fromCharCodes(sRunes, 0, sRunes.length - i).length <= widget.selfMaxLength) {
result = String.fromCharCodes(sRunes, 0, sRunes.length - i);
break;
}
}
……
}
四、在第一次达到最大长度时候,出现数组越界的情况。这个也是因为没有正确处理好TextRange的start和end,直接无脑用native层返回的start和end。所以裁切后的text的TextRange我们要根据实际长度定义end值。
出问题代码(_resetSelection方法):
void _resetSelection(String newText) {
……
widget.controller.value = TextEditingValue(text: result, selection: temp, composing: widget.controller.value.composing);
lastMaxContent = result;
}
修复的代码:
void _resetSelection(String newText) {
……
//result最后展示在屏幕上的文案
TextRange fixRange = widget.controller.value.composing;
if (widget.controller.value.composing.end > result.length) {
fixRange = TextRange(start: fixRange.start, end: result.length);
}
widget.controller.value = TextEditingValue(text: result, selection: temp, composing: fixRange);
lastMaxContent = result;
}
五、切换输入法时候,文案恢复修改前。这个属于业务层用法出错,每次build都重新生成一个textController,导致源码修改的controller的text,每次build都被丢掉了。
错误方法:给一个TextField的textController每次一build都新建一个新的controller。
@override
Widget build(BuildContext context) {
return Container(
child: TextField(
……
controller: TextEditingController.fromValue(TextEditingValue(
text: _txtController.text,
selection: TextSelection.fromPosition(
TextPosition(affinity: TextAffinity.downstream, offset: _txtController.text.length)))),
)
)
}
正确方法:
@override
void initState() {
super.initState();
_txtController.text = widget.defaultName;
_txtController.value = TextEditingValue(
text: _txtController.text,
selection: TextSelection.fromPosition(TextPosition(affinity: TextAffinity.downstream, offset: _txtController.text.length)));
}
@override
Widget build(BuildContext context) {
return Container(
child: TextField(
inputFormatters: [
MTLengthLimitingTextInputFormatter(15),
],
……
)
)
}
总结
- flutter官网确实有很多存在的问题,但这些问题正为我们提供研究源码的动力。
网友评论