mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Reland "Track lastKnownRemoteTextEditingValue separately from received data" (#50307)
This commit is contained in:
parent
1f498e2471
commit
f769bcc5c4
|
@ -1222,7 +1222,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
|
||||
// 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
|
||||
TextEditingValue get currentTextEditingValue => _value;
|
||||
|
@ -1234,6 +1240,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
if (widget.readOnly) {
|
||||
return;
|
||||
}
|
||||
_receivedRemoteTextEditingValue = value;
|
||||
if (value.text != _value.text) {
|
||||
hideToolbar();
|
||||
_showCaretOnScreen();
|
||||
|
@ -1242,7 +1249,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
_obscureLatestCharIndex = _value.selection.baseOffset;
|
||||
}
|
||||
}
|
||||
_lastKnownRemoteTextEditingValue = value;
|
||||
|
||||
_formatAndSetValue(value);
|
||||
|
||||
// To keep the cursor from blinking while typing, we want to restart the
|
||||
|
@ -1369,9 +1376,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
if (!_hasInputConnection)
|
||||
return;
|
||||
final TextEditingValue localValue = _value;
|
||||
if (localValue == _lastKnownRemoteTextEditingValue)
|
||||
if (localValue == _receivedRemoteTextEditingValue)
|
||||
return;
|
||||
_lastKnownRemoteTextEditingValue = localValue;
|
||||
_textInputConnection.setEditingState(localValue);
|
||||
}
|
||||
|
||||
|
@ -1432,7 +1438,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
}
|
||||
if (!_hasInputConnection) {
|
||||
final TextEditingValue localValue = _value;
|
||||
_lastKnownRemoteTextEditingValue = localValue;
|
||||
_lastFormattedUnmodifiedTextEditingValue = localValue;
|
||||
_textInputConnection = TextInput.attach(
|
||||
this,
|
||||
TextInputConfiguration(
|
||||
|
@ -1472,7 +1478,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
if (_hasInputConnection) {
|
||||
_textInputConnection.close();
|
||||
_textInputConnection = null;
|
||||
_lastKnownRemoteTextEditingValue = null;
|
||||
_lastFormattedUnmodifiedTextEditingValue = null;
|
||||
_receivedRemoteTextEditingValue = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1490,7 +1497,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
if (_hasInputConnection) {
|
||||
_textInputConnection.connectionClosedReceived();
|
||||
_textInputConnection = null;
|
||||
_lastKnownRemoteTextEditingValue = null;
|
||||
_lastFormattedUnmodifiedTextEditingValue = null;
|
||||
_receivedRemoteTextEditingValue = null;
|
||||
_finalizeEditing(true);
|
||||
}
|
||||
}
|
||||
|
@ -1634,8 +1642,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
}
|
||||
|
||||
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;
|
||||
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)
|
||||
value = formatter.formatEditUpdate(_value, value);
|
||||
_value = value;
|
||||
|
@ -1645,6 +1656,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
}
|
||||
if (textChanged && widget.onChanged != null)
|
||||
widget.onChanged(value.text);
|
||||
_lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue;
|
||||
}
|
||||
|
||||
void _onCursorColorTick() {
|
||||
|
|
|
@ -4209,6 +4209,114 @@ void main() {
|
|||
final dynamic exception = tester.takeException();
|
||||
expect(exception, isNull);
|
||||
});
|
||||
|
||||
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 {
|
||||
|
|
Loading…
Reference in a new issue