Track lastKnownRemoteTextEditingValue separately from received data (#49406)
This commit is contained in:
parent
25f798a1aa
commit
83d4d63a71
@ -1204,7 +1204,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
// TextInputClient implementation:
|
// TextInputClient implementation:
|
||||||
|
|
||||||
TextEditingValue _lastKnownRemoteTextEditingValue;
|
// _lastFormattedUnmodifiedTextEditingValue tracks the last value
|
||||||
|
// that the formatter ran on and is used to prevent double-formatting.
|
||||||
|
TextEditingValue _lastFormattedUnmodifiedTextEditingValue;
|
||||||
|
// _receivedRemoteTextEditingValue is the direct value last passed in
|
||||||
|
// updateEditingValue. This value does not get updated with the formatted
|
||||||
|
// version.
|
||||||
|
TextEditingValue _receivedRemoteTextEditingValue;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextEditingValue get currentTextEditingValue => _value;
|
TextEditingValue get currentTextEditingValue => _value;
|
||||||
@ -1216,6 +1222,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
if (widget.readOnly) {
|
if (widget.readOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_receivedRemoteTextEditingValue = value;
|
||||||
if (value.text != _value.text) {
|
if (value.text != _value.text) {
|
||||||
hideToolbar();
|
hideToolbar();
|
||||||
_showCaretOnScreen();
|
_showCaretOnScreen();
|
||||||
@ -1224,7 +1231,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_obscureLatestCharIndex = _value.selection.baseOffset;
|
_obscureLatestCharIndex = _value.selection.baseOffset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_lastKnownRemoteTextEditingValue = value;
|
|
||||||
_formatAndSetValue(value);
|
_formatAndSetValue(value);
|
||||||
|
|
||||||
// To keep the cursor from blinking while typing, we want to restart the
|
// To keep the cursor from blinking while typing, we want to restart the
|
||||||
@ -1251,7 +1258,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Finalize editing, but don't give up focus because this keyboard
|
// Finalize editing, but don't give up focus because this keyboard
|
||||||
// action does not imply the user is done inputting information.
|
// action does not imply the user is done inputting information.
|
||||||
_finalizeEditing(false);
|
_finalizeEditing(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1351,9 +1358,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
if (!_hasInputConnection)
|
if (!_hasInputConnection)
|
||||||
return;
|
return;
|
||||||
final TextEditingValue localValue = _value;
|
final TextEditingValue localValue = _value;
|
||||||
if (localValue == _lastKnownRemoteTextEditingValue)
|
if (localValue == _receivedRemoteTextEditingValue)
|
||||||
return;
|
return;
|
||||||
_lastKnownRemoteTextEditingValue = localValue;
|
|
||||||
_textInputConnection.setEditingState(localValue);
|
_textInputConnection.setEditingState(localValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1414,7 +1420,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
if (!_hasInputConnection) {
|
if (!_hasInputConnection) {
|
||||||
final TextEditingValue localValue = _value;
|
final TextEditingValue localValue = _value;
|
||||||
_lastKnownRemoteTextEditingValue = localValue;
|
_lastFormattedUnmodifiedTextEditingValue = localValue;
|
||||||
_textInputConnection = TextInput.attach(
|
_textInputConnection = TextInput.attach(
|
||||||
this,
|
this,
|
||||||
TextInputConfiguration(
|
TextInputConfiguration(
|
||||||
@ -1454,7 +1460,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
if (_hasInputConnection) {
|
if (_hasInputConnection) {
|
||||||
_textInputConnection.close();
|
_textInputConnection.close();
|
||||||
_textInputConnection = null;
|
_textInputConnection = null;
|
||||||
_lastKnownRemoteTextEditingValue = null;
|
_lastFormattedUnmodifiedTextEditingValue = null;
|
||||||
|
_receivedRemoteTextEditingValue = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1472,7 +1479,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
if (_hasInputConnection) {
|
if (_hasInputConnection) {
|
||||||
_textInputConnection.connectionClosedReceived();
|
_textInputConnection.connectionClosedReceived();
|
||||||
_textInputConnection = null;
|
_textInputConnection = null;
|
||||||
_lastKnownRemoteTextEditingValue = null;
|
_lastFormattedUnmodifiedTextEditingValue = null;
|
||||||
|
_receivedRemoteTextEditingValue = null;
|
||||||
_finalizeEditing(true);
|
_finalizeEditing(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1616,17 +1624,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _formatAndSetValue(TextEditingValue value) {
|
void _formatAndSetValue(TextEditingValue value) {
|
||||||
|
// Check if the new value is the same as the current local value, or is the same
|
||||||
|
// as the post-formatting value of the previous pass.
|
||||||
final bool textChanged = _value?.text != value?.text;
|
final bool textChanged = _value?.text != value?.text;
|
||||||
if (textChanged && widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) {
|
final bool isRepeat = value?.text == _lastFormattedUnmodifiedTextEditingValue?.text;
|
||||||
|
if (textChanged && !isRepeat && widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) {
|
||||||
for (final TextInputFormatter formatter in widget.inputFormatters)
|
for (final TextInputFormatter formatter in widget.inputFormatters)
|
||||||
value = formatter.formatEditUpdate(_value, value);
|
value = formatter.formatEditUpdate(_value, value);
|
||||||
_value = value;
|
_value = value;
|
||||||
_updateRemoteEditingValueIfNeeded();
|
_updateRemoteEditingValueIfNeeded();
|
||||||
} else {
|
} else if (!isRepeat || !textChanged) {
|
||||||
_value = value;
|
_value = value;
|
||||||
}
|
}
|
||||||
if (textChanged && widget.onChanged != null)
|
if (textChanged && widget.onChanged != null)
|
||||||
widget.onChanged(value.text);
|
widget.onChanged(value.text);
|
||||||
|
_lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onCursorColorTick() {
|
void _onCursorColorTick() {
|
||||||
|
@ -4178,6 +4178,114 @@ void main() {
|
|||||||
}
|
}
|
||||||
expect(tester.testTextInput.editingState['text'], 'flutter is the best!...');
|
expect(tester.testTextInput.editingState['text'], 'flutter is the best!...');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('updateEditingValue filters multiple calls from formatter', (WidgetTester tester) async {
|
||||||
|
final MockTextFormatter formatter = MockTextFormatter();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MediaQuery(
|
||||||
|
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||||
|
child: Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: FocusScope(
|
||||||
|
node: focusScopeNode,
|
||||||
|
autofocus: true,
|
||||||
|
child: EditableText(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
maxLines: 1, // Sets text keyboard implicitly.
|
||||||
|
style: textStyle,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
inputFormatters: <TextInputFormatter>[formatter],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(EditableText));
|
||||||
|
await tester.showKeyboard(find.byType(EditableText));
|
||||||
|
controller.text = '';
|
||||||
|
await tester.idle();
|
||||||
|
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
expect(tester.testTextInput.editingState['text'], equals(''));
|
||||||
|
expect(state.wantKeepAlive, true);
|
||||||
|
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: ''));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'a'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aa'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aaa'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aa'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aaa'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aaaa'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aa'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aaaaaaa'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aa'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aaaaaaaaa'));
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'aaaaaaaaa')); // Skipped
|
||||||
|
|
||||||
|
const List<String> referenceLog = <String>[
|
||||||
|
'[1]: , a',
|
||||||
|
'[1]: normal aa',
|
||||||
|
'[2]: aa, aaa',
|
||||||
|
'[2]: normal aaaa',
|
||||||
|
'[3]: aaaa, aa',
|
||||||
|
'[3]: deleting a',
|
||||||
|
'[4]: a, aaa',
|
||||||
|
'[4]: normal aaaaaaaa',
|
||||||
|
'[5]: aaaaaaaa, aaaa',
|
||||||
|
'[5]: deleting aaa',
|
||||||
|
'[6]: aaa, aa',
|
||||||
|
'[6]: deleting aaaa',
|
||||||
|
'[7]: aaaa, aaaaaaa',
|
||||||
|
'[7]: normal aaaaaaaaaaaaaa',
|
||||||
|
'[8]: aaaaaaaaaaaaaa, aa',
|
||||||
|
'[8]: deleting aaaaaa',
|
||||||
|
'[9]: aaaaaa, aaaaaaaaa',
|
||||||
|
'[9]: normal aaaaaaaaaaaaaaaaaa',
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(formatter.log, referenceLog);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockTextFormatter extends TextInputFormatter {
|
||||||
|
MockTextFormatter() : _counter = 0, log = <String>[];
|
||||||
|
|
||||||
|
int _counter;
|
||||||
|
List<String> log;
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue,
|
||||||
|
TextEditingValue newValue,
|
||||||
|
) {
|
||||||
|
_counter++;
|
||||||
|
log.add('[$_counter]: ${oldValue.text}, ${newValue.text}');
|
||||||
|
TextEditingValue finalValue;
|
||||||
|
if (newValue.text.length < oldValue.text.length) {
|
||||||
|
finalValue = _handleTextDeletion(oldValue, newValue);
|
||||||
|
} else {
|
||||||
|
finalValue = _formatText(newValue);
|
||||||
|
}
|
||||||
|
return finalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TextEditingValue _handleTextDeletion(
|
||||||
|
TextEditingValue oldValue, TextEditingValue newValue) {
|
||||||
|
final String result = 'a' * (_counter - 2);
|
||||||
|
log.add('[$_counter]: deleting $result');
|
||||||
|
return TextEditingValue(text: result);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEditingValue _formatText(TextEditingValue value) {
|
||||||
|
final String result = 'a' * _counter * 2;
|
||||||
|
log.add('[$_counter]: normal $result');
|
||||||
|
return TextEditingValue(text: result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockTextSelectionControls extends Mock implements TextSelectionControls {
|
class MockTextSelectionControls extends Mock implements TextSelectionControls {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user