Reland "Track lastKnownRemoteTextEditingValue separately from received data" (#50307)

This commit is contained in:
Gary Qian 2020-02-10 20:55:48 -05:00 committed by GitHub
parent 1f498e2471
commit f769bcc5c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 129 additions and 9 deletions

View file

@ -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
@ -1269,7 +1276,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
break;
default:
// 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);
break;
}
@ -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() {

View file

@ -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 {