Commit d3415137 authored by Jason Simmons's avatar Jason Simmons Committed by GitHub

Temporarily show the latest entered character in an obscured text field (#11830)

Fixes https://github.com/flutter/flutter/issues/11817
parent bfa78851
...@@ -27,6 +27,10 @@ typedef void SelectionChangedCallback(TextSelection selection, bool longPress); ...@@ -27,6 +27,10 @@ typedef void SelectionChangedCallback(TextSelection selection, bool longPress);
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500); const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
// Number of cursor ticks during which the most recently entered character
// is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3;
/// A controller for an editable text field. /// A controller for an editable text field.
/// ///
/// Whenever the user modifies a text field with an associated /// Whenever the user modifies a text field with an associated
...@@ -337,8 +341,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -337,8 +341,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void updateEditingValue(TextEditingValue value) { void updateEditingValue(TextEditingValue value) {
if (value.text != _value.text) if (value.text != _value.text) {
_hideSelectionOverlayIfNeeded(); _hideSelectionOverlayIfNeeded();
if (widget.obscureText && value.text.length == _value.text.length + 1) {
_obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
_obscureLatestCharIndex = _value.selection.baseOffset;
}
}
_lastKnownRemoteTextEditingValue = value; _lastKnownRemoteTextEditingValue = value;
_formatAndSetValue(value); _formatAndSetValue(value);
if (widget.onChanged != null) if (widget.onChanged != null)
...@@ -516,8 +525,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -516,8 +525,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@visibleForTesting @visibleForTesting
Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod; Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
int _obscureShowCharTicksPending = 0;
int _obscureLatestCharIndex;
void _cursorTick(Timer timer) { void _cursorTick(Timer timer) {
_showCursor.value = !_showCursor.value; _showCursor.value = !_showCursor.value;
if (_obscureShowCharTicksPending > 0) {
setState(() { _obscureShowCharTicksPending--; });
}
} }
void _startCursorTimer() { void _startCursorTimer() {
...@@ -529,6 +544,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -529,6 +544,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_cursorTimer?.cancel(); _cursorTimer?.cancel();
_cursorTimer = null; _cursorTimer = null;
_showCursor.value = false; _showCursor.value = false;
_obscureShowCharTicksPending = 0;
} }
void _startOrStopCursorTimerIfNeeded() { void _startOrStopCursorTimerIfNeeded() {
...@@ -580,6 +596,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -580,6 +596,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0, textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
textAlign: widget.textAlign, textAlign: widget.textAlign,
obscureText: widget.obscureText, obscureText: widget.obscureText,
obscureShowCharacterAtIndex: _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null,
autocorrect: widget.autocorrect, autocorrect: widget.autocorrect,
offset: offset, offset: offset,
onSelectionChanged: _handleSelectionChanged, onSelectionChanged: _handleSelectionChanged,
...@@ -603,6 +620,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -603,6 +620,7 @@ class _Editable extends LeafRenderObjectWidget {
this.textScaleFactor, this.textScaleFactor,
this.textAlign, this.textAlign,
this.obscureText, this.obscureText,
this.obscureShowCharacterAtIndex,
this.autocorrect, this.autocorrect,
this.offset, this.offset,
this.onSelectionChanged, this.onSelectionChanged,
...@@ -618,6 +636,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -618,6 +636,7 @@ class _Editable extends LeafRenderObjectWidget {
final double textScaleFactor; final double textScaleFactor;
final TextAlign textAlign; final TextAlign textAlign;
final bool obscureText; final bool obscureText;
final int obscureShowCharacterAtIndex;
final bool autocorrect; final bool autocorrect;
final ViewportOffset offset; final ViewportOffset offset;
final SelectionChangedHandler onSelectionChanged; final SelectionChangedHandler onSelectionChanged;
...@@ -675,8 +694,12 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -675,8 +694,12 @@ class _Editable extends LeafRenderObjectWidget {
} }
String text = value.text; String text = value.text;
if (obscureText) if (obscureText) {
text = new String.fromCharCodes(new List<int>.filled(text.length, 0x2022)); text = new String.fromCharCodes(new List<int>.filled(text.length, 0x2022));
final int o = obscureShowCharacterAtIndex;
if (o != null && o >= 0 && o < text.length)
text = text.replaceRange(o, o + 1, value.text.substring(o, o + 1));
}
return new TextSpan(style: style, text: text); return new TextSpan(style: style, text: text);
} }
} }
...@@ -219,6 +219,24 @@ void main() { ...@@ -219,6 +219,24 @@ void main() {
)); ));
await tester.pump(); await tester.pump();
// Enter a character into the obscured field and verify that the character
// is temporarily shown to the user and then changed to a bullet.
const String newChar = 'X';
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testValue + newChar,
selection: const TextSelection.collapsed(offset: testValue.length + 1),
));
await tester.pump();
String editText = findRenderEditable(tester).text.text;
expect(editText.substring(editText.length - 1), newChar);
await tester.pump(const Duration(seconds: 2));
editText = findRenderEditable(tester).text.text;
expect(editText.substring(editText.length - 1), '\u2022');
}); });
testWidgets('Caret position is updated on tap', (WidgetTester tester) async { testWidgets('Caret position is updated on tap', (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment