mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Skip reformatting and calling onChanged for composing region only changes. (#70883)
This commit is contained in:
parent
8e05e59c29
commit
746056a539
|
@ -69,12 +69,21 @@ const int _kObscureShowLatestCharCursorTicks = 3;
|
|||
/// text field. If you build a text field with a controller that already has
|
||||
/// [text], the text field will use that text as its initial value.
|
||||
///
|
||||
/// The [text] or [selection] properties can be set from within a listener
|
||||
/// added to this controller. If both properties need to be changed then the
|
||||
/// controller's [value] should be set instead.
|
||||
/// The [value] (as well as [text] and [selection]) of this controller can be
|
||||
/// updated from within a listener added to this controller. Be aware of
|
||||
/// infinite loops since the listener will also be notified of the changes made
|
||||
/// from within itself. Modifying the composing region from within a listener
|
||||
/// can also have a bad interaction with some input methods. Gboard, for
|
||||
/// example, will try to restore the composing region of the text if it was
|
||||
/// modified programmatically, creating an infinite loop of communications
|
||||
/// between the framework and the input method. Consider using
|
||||
/// [TextInputFormatter]s instead for as-you-type text modification.
|
||||
///
|
||||
/// Remember to [dispose] of the [TextEditingController] when it is no longer needed.
|
||||
/// This will ensure we discard any resources used by the object.
|
||||
/// If both the [text] or [selection] properties need to be changed, set the
|
||||
/// controller's [value] instead.
|
||||
///
|
||||
/// Remember to [dispose] of the [TextEditingController] when it is no longer
|
||||
/// needed. This will ensure we discard any resources used by the object.
|
||||
/// {@tool dartpad --template=stateful_widget_material}
|
||||
/// This example creates a [TextField] with a [TextEditingController] whose
|
||||
/// change listener forces the entered text to be lower case and keeps the
|
||||
|
@ -2183,14 +2192,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
|
||||
|
||||
void _formatAndSetValue(TextEditingValue value) {
|
||||
// Check if the new value is the same as the current local value, or is the same
|
||||
// as the pre-formatting value of the previous pass (repeat call).
|
||||
final bool textChanged = _value.text != value.text || _value.composing != value.composing;
|
||||
// Only apply input formatters if the text has changed (including uncommited
|
||||
// text in the composing region), or when the user committed the composing
|
||||
// text.
|
||||
// Gboard is very persistent in restoring the composing region. Applying
|
||||
// input formatters on composing-region-only changes (except clearing the
|
||||
// current composing region) is very infinite-loop-prone: the formatters
|
||||
// will keep trying to modify the composing region while Gboard will keep
|
||||
// trying to restore the original composing region.
|
||||
final bool textChanged = _value.text != value.text
|
||||
|| (!_value.composing.isCollapsed && value.composing.isCollapsed);
|
||||
|
||||
if (textChanged) {
|
||||
// Only format when the text has changed and there are available formatters.
|
||||
// Pass through the formatter regardless of repeat status if the input value is
|
||||
// different than the stored value.
|
||||
value = widget.inputFormatters?.fold<TextEditingValue>(
|
||||
value,
|
||||
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
|
||||
|
|
|
@ -5835,12 +5835,15 @@ void main() {
|
|||
expect(formatter.formatCallCount, 3);
|
||||
state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2))); // No text change, does not format
|
||||
expect(formatter.formatCallCount, 3);
|
||||
state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2), composing: TextRange(start: 1, end: 2))); // Composing change triggers reformat
|
||||
expect(formatter.formatCallCount, 4);
|
||||
|
||||
// Composing changes should not trigger reformat, as it could cause infinite loops on some IMEs.
|
||||
state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2), composing: TextRange(start: 1, end: 2)));
|
||||
expect(formatter.formatCallCount, 3);
|
||||
expect(formatter.lastOldValue.composing, const TextRange(start: -1, end: -1));
|
||||
expect(formatter.lastNewValue.composing, const TextRange(start: 1, end: 2)); // The new composing was registered in formatter.
|
||||
expect(formatter.lastNewValue.composing, const TextRange(start: -1, end: -1)); // The new composing was registered in formatter.
|
||||
// Clearing composing region should trigger reformat.
|
||||
state.updateEditingValue(const TextEditingValue(text: '01234', selection: TextSelection.collapsed(offset: 2))); // Formats, with oldValue containing composing region.
|
||||
expect(formatter.formatCallCount, 5);
|
||||
expect(formatter.formatCallCount, 4);
|
||||
expect(formatter.lastOldValue.composing, const TextRange(start: 1, end: 2));
|
||||
expect(formatter.lastNewValue.composing, const TextRange(start: -1, end: -1));
|
||||
|
||||
|
@ -5851,10 +5854,8 @@ void main() {
|
|||
'[2]: normal aaaa',
|
||||
'[3]: 012, 0123',
|
||||
'[3]: normal aaaaaa',
|
||||
'[4]: 0123, 0123',
|
||||
'[4]: 0123, 01234',
|
||||
'[4]: normal aaaaaaaa',
|
||||
'[5]: 0123, 01234',
|
||||
'[5]: normal aaaaaaaaaa',
|
||||
];
|
||||
|
||||
expect(formatter.log, referenceLog);
|
||||
|
|
Loading…
Reference in a new issue