問(wèn)題
問(wèn)題描述:當(dāng)Flutter的輸入框中支持上了表情符號(hào)(emoji)诲泌,無(wú)論你用maxLength還是inputFormatters屬性,都會(huì)出現(xiàn)長(zhǎng)度超過(guò)你給定的值或表達(dá)式坦胶,而且光標(biāo)還會(huì)在達(dá)到最后字符的時(shí)候往前移動(dòng)一個(gè)字符。以下都是圍繞當(dāng)輸入框有表情符號(hào)開展的晴楔。
問(wèn)題原因:原本我是以為光標(biāo)的rect數(shù)據(jù)顿苇,所以在text_painter.dart類看了Rect _getRectFromDownstream(int offset, Rect caretPrototype)方法,用來(lái)計(jì)算光標(biāo)的矩陣税弃。(文字屬性是downStream的方法走這個(gè)方法纪岁,有對(duì)應(yīng)的一個(gè)是upstream的方法的)然后在追查到最后的editable.dart的_paintCaretPaint方法繪制里,發(fā)現(xiàn)出錯(cuò)在rect矩陣數(shù)據(jù)里则果,繪制沒有任何問(wèn)題蜂科。然后回到計(jì)算光標(biāo)矩陣的地方顽决,在觀察到往前移動(dòng)的光標(biāo)異常問(wèn)題是因?yàn)閛ffset引起的。
根本原因:無(wú)論是maxLength還是inputFormatters最后都是以inputFormatters作為EditableText的構(gòu)造函數(shù)导匣,在EditableText沒有處理好限字符的問(wèn)題才菠。這里主要就是看LengthLimitingTextInputFormatter類了,因?yàn)镋ditableText都是用各種inputFormatters處理文案的問(wèn)題贡定。
字符比限制的數(shù)字多一個(gè)原因:由于沒有處理好最大字符數(shù)的問(wèn)題赋访,當(dāng)flutter/engine的原生輸入框數(shù)據(jù)返回時(shí)候就是用戶真實(shí)輸入的數(shù)據(jù),所以到了EditableText更新text也是不做長(zhǎng)度判斷缓待,所以才會(huì)導(dǎo)致比限制的數(shù)字多一個(gè)的問(wèn)題蚓耽。
光標(biāo)前移原因:這里是因?yàn)閒lutter層在計(jì)算最大字符數(shù)時(shí)候,觸發(fā)刷新旋炒,native又返回一次輸入框的數(shù)據(jù)步悠,這里可以在text.input.dart類中_handleTextInputInvocation(MethodCall methodCall)中返回?cái)?shù)據(jù)解析生成的TextEditingValue中光標(biāo)位置就是用戶輸入的maxLength。
如何解決
通過(guò)上面的問(wèn)題大概知道就是當(dāng)文字中混有表情符號(hào)(emoji)時(shí)候瘫镇,flutter計(jì)算text時(shí)候就會(huì)出現(xiàn)漏洞鼎兽。這里先補(bǔ)給下最重要的知識(shí)點(diǎn),全面認(rèn)識(shí)TextEditingValue類铣除。
TextEditingValue
TextEditingValue類是所有輸入框封裝文案與光標(biāo)的基類谚咬。TextEditingValue里面的屬性分別都有自己用處。
text:輸入框要展示的文案尚粘。
作用:展示文案的內(nèi)容择卦。
selection:TextSelection類(affinity:文字的TextAffinity屬性;baseOffset:字符開始的偏移量(有光標(biāo)情況:光標(biāo)的起始位置)郎嫁;extentOffset:字符插入的位置(有光標(biāo)情況:光標(biāo)的結(jié)束位置)
作用:用戶選擇插入字符的位置秉继。
composing:TextRange類(int start,int end)泽铛。start是文本編輯的起始位置秕噪,end是文本編輯的結(jié)束位置。
作用:用來(lái)判斷該文本是否還在編輯狀態(tài)厚宰,如果還在編輯狀態(tài)腌巾,native層在_handleTextInputInvocation返回時(shí)候,將不帶上TextRange的start和end標(biāo)識(shí)的字符串铲觉。(這個(gè)屬性專門用來(lái)處理iOS自帶的輸入框問(wèn)題澈蝙,會(huì)在章節(jié)《遇到的問(wèn)題》中描述,坑爹)
解決辦法
-
從源碼中能看到EditableText每次接收到新的字符串內(nèi)容撵幽,都會(huì)在_formatAndSetValue方法調(diào)用onChanged(String text)方法灯荧。其中可以看到這里inputFormatters沒有處理好帶表情的TextEditingValue,所以我們要做的就是在onChange方法中處理好TextEditingValue盐杂。
_formatAndSetValue方法
- 方法一(能達(dá)到超長(zhǎng)的回調(diào)逗载,也是比較挫的方法)
- 最好自己封裝一個(gè)XXXTextField類哆窿,統(tǒng)一處理這類問(wèn)題±髡澹或者自己寫一個(gè)inputFormatters挚躯,把官網(wǎng)的彌補(bǔ)下。現(xiàn)在我先介紹我自己比較挫的解決辦法先擦秽。
在封裝的類中增加兩個(gè)屬性
//最大長(zhǎng)度
final int selfMaxLength;
//達(dá)到最大長(zhǎng)度后的回調(diào)
final Function maxLengthCallBack
class _XXXTextFieldState extends State<XXXTextField> {
……
//是否已經(jīng)觸發(fā)過(guò)一次超長(zhǎng)
bool hasAlreadyMaxLength = false;
//第一次觸發(fā)超長(zhǎng)的文案
String lastMaxContent = "";
@override
void initState() {
if (widget.controller.text.length == widget.selfMaxLength) {
//UI帶進(jìn)來(lái)的文案超長(zhǎng)码荔,記錄
hasAlreadyMaxLength = true;
lastMaxContent = widget.controller.text;
} else {
//UI帶進(jìn)來(lái)的文案沒有超長(zhǎng)
hasAlreadyMaxLength = false;
lastMaxContent = "";
}
}
//maxLength or inputFormatters正常設(shè)置不影響
@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);
},
……
)
)
}
/***真正處理邏輯***/
//第一次文案超長(zhǎng)情況下,把光標(biāo)定位到最后一個(gè)符號(hào)后面感挥,裁切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) {
//如果刪除后剩下的還有一個(gè)空格缩搅,繼續(xù)刪除
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;
}
//當(dāng)用戶在超長(zhǎng)情況下,繼續(xù)怎樣輸入触幼,顯示第一次超長(zhǎng)的文案硼瓣,把光標(biāo)定位到最后個(gè)字符位置。
void _initOldDataSelection(String newText) {
TextSelection actualSelection = widget.controller.value.selection;
actualSelection = widget.controller.value.selection.copyWith(
baseOffset: lastMaxContent.length,
extentOffset: lastMaxContent.length,
);
//ios:當(dāng)TextRange不為-1時(shí)候置谦,下次update會(huì)把start和end直接的變量值全部丟棄堂鲤。當(dāng)你確定內(nèi)容不變時(shí)候,請(qǐng)把他們變成-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 {
//對(duì)controller的text設(shè)置霉祸,一定要是對(duì)value改變筑累,要不然直接設(shè)置text袱蜡,selection就是為-1默認(rèn)值丝蹭。
widget.controller.value =
TextEditingValue(text: newText, selection: widget.controller.value.selection, composing: widget.controller.value.composing);
//如果用戶刪除文案,達(dá)不到超長(zhǎng)效果坪蚁,就清除超長(zhǎng)數(shù)據(jù)
hasAlreadyMaxLength = false;
lastMaxContent = "";
}
}
}
-
調(diào)用時(shí)候就用封裝的XXXTextField類奔穿,然后添加selfMaxLength最大長(zhǎng)度和maxLengthCallBack最大長(zhǎng)度返回參數(shù)(callBack自己看業(yè)務(wù)吧)。
調(diào)用示例
- 方法二:
自定義LengthLimitingTextInputFormatter,道理和上面挫的方法差不多敏晤,這里就不一一解析了贱田。如果用這個(gè)也要獲取超長(zhǎng)回調(diào),直接設(shè)置個(gè)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;
///超過(guò)文本長(zhǎng)度回調(diào)
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) {
///第一次超長(zhǎng)
if(maxLengthCallBack != null) maxLengthCallBack();
return _resetSelection(newValue);
} else if (hasAlreadyMaxLength && maxLength != null && maxLength > 0 && newValue.text.length >= maxLength) {
///第二次往后超長(zhǎng)
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) {
//如果刪除后剩下的還有一個(gè)空格男摧,繼續(xù)刪除
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:當(dāng)TextRange不為-1時(shí)候,下次update會(huì)把start和end直接的變量值全部丟棄译打。當(dāng)你確定內(nèi)容不變時(shí)候耗拓,請(qǐng)把他們變成-1
TextRange fixRange = TextRange(start: -1, end: -1);
return TextEditingValue(text: oldValue.text, selection: actualSelection, composing: fixRange);
}
}
遇到的問(wèn)題
一、iOS自帶原生輸入框奏司,當(dāng)用戶輸入拼音乔询,點(diǎn)擊中文的時(shí)候,輸入框中會(huì)顯示類似:w d我的韵洋。這種明顯處于bug的問(wèn)題讓我一個(gè)安卓菜雞一臉懵逼竿刁。后面仔細(xì)看了源碼的參數(shù)黄锤,才發(fā)現(xiàn)我的TextEditingValue少了一個(gè)composing屬性,這個(gè)屬性就是用來(lái)告訴 iOS 的native層哪些文案還在編輯食拜,等用戶選中要的文案后便可刪除鸵熟。所以后面我給加了composing屬性。
出問(wèn)題代碼(就用_actionMaxLengthState舉例子监婶,其他函數(shù)看上面解決辦法即可):
void _actionMaxLengthState(String newText) {
if (newText.length >= widget.selfMaxLength) {
……
} else {
//對(duì)controller的text設(shè)置旅赢,一定要是對(duì)value改變,要不然直接設(shè)置text惑惶,selection就是為-1默認(rèn)值煮盼。
widget.controller.value = TextEditingValue(text: newText, selection: widget.controller.value.selection);
hasAlreadyMaxLength = false;
lastMaxContent = "";
}
}
修復(fù)后的代碼:
void _actionMaxLengthState(String newText) {
if (newText.length >= widget.selfMaxLength) {
……
} else {
//對(duì)controller的text設(shè)置,一定要是對(duì)value改變带污,要不然直接設(shè)置text僵控,selection就是為-1默認(rèn)值。
widget.controller.value =
TextEditingValue(text: newText, selection: widget.controller.value.selection, composing: widget.controller.value.composing);
hasAlreadyMaxLength = false;
lastMaxContent = "";
}
}
二鱼冀、在第一個(gè)問(wèn)題解決后报破,緊接著在iOS上遇到第二個(gè)問(wèn)題。當(dāng)用戶多次輸入超長(zhǎng)內(nèi)容后(內(nèi)容中包含表情)千绪,會(huì)出現(xiàn)表情后的文案全部消失充易。這里出現(xiàn)的問(wèn)題還是composing,沒有完全理解含義荸型,直接使用native返回的composing所致盹靴。
出問(wèn)題代碼(_initOldDataSelection方法):
void _initOldDataSelection(String newText) {
……
widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: widget.controller.value.composing);
}
修復(fù)后的代碼(修復(fù)后的意思即是用戶多次超長(zhǎng)的內(nèi)容,我?guī)陀脩艋謴?fù)瑞妇,屬于文本不在編輯狀態(tài)稿静,iOS的native層不能把我的內(nèi)容刪除):
void _initOldDataSelection(String newText) {
……
//ios:當(dāng)TextRange不為-1時(shí)候,下次update會(huì)把start和end直接的變量值全部丟棄辕狰。當(dāng)你確定內(nèi)容不變時(shí)候改备,請(qǐng)把他們變成-1
TextRange fixRange = TextRange(start: -1, end: -1);
widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: fixRange);
}
三、文案剪切出現(xiàn)崩潰蔓倍。原因是我通過(guò)substring()方法來(lái)剪切悬钳,眾所周知表情都是由多個(gè)字符拼接而成的表達(dá)式,如果剛好最后一個(gè)是表情偶翅,我把其中表情一個(gè)字符剪切掉了默勾,導(dǎo)致無(wú)法正常顯示表情符號(hào),自然會(huì)崩潰倒堕。
出問(wèn)題代碼(_initOldDataSelection方法):
void _resetSelection(String newText) {
String result = newText.substring(0, widget.selfMaxLength);
……
}
修復(fù)的代碼:
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;
}
}
……
}
四灾测、在第一次達(dá)到最大長(zhǎng)度時(shí)候,出現(xiàn)數(shù)組越界的情況。這個(gè)也是因?yàn)闆]有正確處理好TextRange的start和end媳搪,直接無(wú)腦用native層返回的start和end铭段。所以裁切后的text的TextRange我們要根據(jù)實(shí)際長(zhǎng)度定義end值。
出問(wèn)題代碼(_resetSelection方法):
void _resetSelection(String newText) {
……
widget.controller.value = TextEditingValue(text: result, selection: temp, composing: widget.controller.value.composing);
lastMaxContent = result;
}
修復(fù)的代碼:
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;
}
五秦爆、切換輸入法時(shí)候序愚,文案恢復(fù)修改前。這個(gè)屬于業(yè)務(wù)層用法出錯(cuò)等限,每次build都重新生成一個(gè)textController爸吮,導(dǎo)致源碼修改的controller的text,每次build都被丟掉了望门。
錯(cuò)誤方法:給一個(gè)TextField的textController每次一build都新建一個(gè)新的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),
],
……
)
)
}
總結(jié)
- flutter官網(wǎng)確實(shí)有很多存在的問(wèn)題,但這些問(wèn)題正為我們提供研究源碼的動(dòng)力筹误。