Skip to content

Commit de8cf8b

Browse files
authored
Customizable obscuringCharacter (flutter#55415)
1 parent 4552af1 commit de8cf8b

File tree

6 files changed

+74
-6
lines changed

6 files changed

+74
-6
lines changed

packages/flutter/lib/src/cupertino/text_field.dart

+7
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ class CupertinoTextField extends StatefulWidget {
241241
ToolbarOptions toolbarOptions,
242242
this.showCursor,
243243
this.autofocus = false,
244+
this.obscuringCharacter = '•',
244245
this.obscureText = false,
245246
this.autocorrect = true,
246247
SmartDashesType smartDashesType,
@@ -272,6 +273,7 @@ class CupertinoTextField extends StatefulWidget {
272273
}) : assert(textAlign != null),
273274
assert(readOnly != null),
274275
assert(autofocus != null),
276+
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
275277
assert(obscureText != null),
276278
assert(autocorrect != null),
277279
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
@@ -428,6 +430,9 @@ class CupertinoTextField extends StatefulWidget {
428430
/// {@macro flutter.widgets.editableText.autofocus}
429431
final bool autofocus;
430432

433+
/// {@macro flutter.widgets.editableText.obscuringCharacter}
434+
final String obscuringCharacter;
435+
431436
/// {@macro flutter.widgets.editableText.obscureText}
432437
final bool obscureText;
433438

@@ -601,6 +606,7 @@ class CupertinoTextField extends StatefulWidget {
601606
properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text));
602607
properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
603608
properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
609+
properties.add(DiagnosticsProperty<String>('obscuringCharacter', obscuringCharacter, defaultValue: '•'));
604610
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
605611
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
606612
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
@@ -921,6 +927,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
921927
strutStyle: widget.strutStyle,
922928
textAlign: widget.textAlign,
923929
autofocus: widget.autofocus,
930+
obscuringCharacter: widget.obscuringCharacter,
924931
obscureText: widget.obscureText,
925932
autocorrect: widget.autocorrect,
926933
smartDashesType: widget.smartDashesType,

packages/flutter/lib/src/material/text_field.dart

+7
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ class TextField extends StatefulWidget {
318318
ToolbarOptions toolbarOptions,
319319
this.showCursor,
320320
this.autofocus = false,
321+
this.obscuringCharacter = '•',
321322
this.obscureText = false,
322323
this.autocorrect = true,
323324
SmartDashesType smartDashesType,
@@ -350,6 +351,7 @@ class TextField extends StatefulWidget {
350351
}) : assert(textAlign != null),
351352
assert(readOnly != null),
352353
assert(autofocus != null),
354+
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
353355
assert(obscureText != null),
354356
assert(autocorrect != null),
355357
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
@@ -476,6 +478,9 @@ class TextField extends StatefulWidget {
476478
/// {@macro flutter.widgets.editableText.autofocus}
477479
final bool autofocus;
478480

481+
/// {@macro flutter.widgets.editableText.obscuringCharacter}
482+
final String obscuringCharacter;
483+
479484
/// {@macro flutter.widgets.editableText.obscureText}
480485
final bool obscureText;
481486

@@ -727,6 +732,7 @@ class TextField extends StatefulWidget {
727732
properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text));
728733
properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
729734
properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
735+
properties.add(DiagnosticsProperty<String>('obscuringCharacter', obscuringCharacter, defaultValue: '•'));
730736
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
731737
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
732738
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
@@ -1021,6 +1027,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
10211027
textAlign: widget.textAlign,
10221028
textDirection: widget.textDirection,
10231029
autofocus: widget.autofocus,
1030+
obscuringCharacter: widget.obscuringCharacter,
10241031
obscureText: widget.obscureText,
10251032
autocorrect: widget.autocorrect,
10261033
smartDashesType: widget.smartDashesType,

packages/flutter/lib/src/material/text_form_field.dart

+3
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class TextFormField extends FormField<String> {
146146
bool readOnly = false,
147147
ToolbarOptions toolbarOptions,
148148
bool showCursor,
149+
String obscuringCharacter = '•',
149150
bool obscureText = false,
150151
bool autocorrect = true,
151152
SmartDashesType smartDashesType,
@@ -177,6 +178,7 @@ class TextFormField extends FormField<String> {
177178
assert(textAlign != null),
178179
assert(autofocus != null),
179180
assert(readOnly != null),
181+
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
180182
assert(obscureText != null),
181183
assert(autocorrect != null),
182184
assert(enableSuggestions != null),
@@ -230,6 +232,7 @@ class TextFormField extends FormField<String> {
230232
toolbarOptions: toolbarOptions,
231233
readOnly: readOnly,
232234
showCursor: showCursor,
235+
obscuringCharacter: obscuringCharacter,
233236
obscureText: obscureText,
234237
autocorrect: autocorrect,
235238
smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),

packages/flutter/lib/src/rendering/editable.dart

+17-3
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
213213
bool readOnly = false,
214214
bool forceLine = true,
215215
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
216+
String obscuringCharacter = '•',
216217
bool obscureText = false,
217218
Locale locale,
218219
double cursorWidth = 1.0,
@@ -247,6 +248,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
247248
assert(ignorePointer != null),
248249
assert(textWidthBasis != null),
249250
assert(paintCursorAboveText != null),
251+
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
250252
assert(obscureText != null),
251253
assert(textSelectionDelegate != null),
252254
assert(cursorWidth != null && cursorWidth >= 0.0),
@@ -284,6 +286,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
284286
_selectionWidthStyle = selectionWidthStyle,
285287
_startHandleLayerLink = startHandleLayerLink,
286288
_endHandleLayerLink = endHandleLayerLink,
289+
_obscuringCharacter = obscuringCharacter,
287290
_obscureText = obscureText,
288291
_readOnly = readOnly,
289292
_forceLine = forceLine,
@@ -295,9 +298,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
295298
_promptRectPaint.color = promptRectColor;
296299
}
297300

298-
/// Character used to obscure text if [obscureText] is true.
299-
static const String obscuringCharacter = '•';
300-
301301
/// Called when the selection changes.
302302
///
303303
/// If this is null, then selection changes will be ignored.
@@ -344,6 +344,20 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
344344
markNeedsTextLayout();
345345
}
346346

347+
/// Character used for obscuring text if [obscureText] is true.
348+
///
349+
/// Cannot be null, and must have a length of exactly one.
350+
String get obscuringCharacter => _obscuringCharacter;
351+
String _obscuringCharacter;
352+
set obscuringCharacter(String value) {
353+
if (_obscuringCharacter == value) {
354+
return;
355+
}
356+
assert(value != null && value.length == 1);
357+
_obscuringCharacter = value;
358+
markNeedsLayout();
359+
}
360+
347361
/// Whether to hide the text being edited (e.g., for passwords).
348362
bool get obscureText => _obscureText;
349363
bool _obscureText;

packages/flutter/lib/src/widgets/editable_text.dart

+18-2
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ class EditableText extends StatefulWidget {
357357
@required this.controller,
358358
@required this.focusNode,
359359
this.readOnly = false,
360+
this.obscuringCharacter = '•',
360361
this.obscureText = false,
361362
this.autocorrect = true,
362363
SmartDashesType smartDashesType,
@@ -413,6 +414,7 @@ class EditableText extends StatefulWidget {
413414
this.autofillHints,
414415
}) : assert(controller != null),
415416
assert(focusNode != null),
417+
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
416418
assert(obscureText != null),
417419
assert(autocorrect != null),
418420
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
@@ -464,11 +466,20 @@ class EditableText extends StatefulWidget {
464466
/// Controls whether this widget has keyboard focus.
465467
final FocusNode focusNode;
466468

469+
/// {@template flutter.widgets.editableText.obscuringCharacter}
470+
/// Character used for obscuring text if [obscureText] is true.
471+
///
472+
/// Must be only a single character.
473+
///
474+
/// Defaults to the character U+2022 BULLET (•).
475+
/// {@endtemplate}
476+
final String obscuringCharacter;
477+
467478
/// {@template flutter.widgets.editableText.obscureText}
468479
/// Whether to hide the text being edited (e.g., for passwords).
469480
///
470481
/// When this is set to true, all the characters in the text field are
471-
/// replaced by U+2022 BULLET characters (•).
482+
/// replaced by [obscuringCharacter].
472483
///
473484
/// Defaults to false. Cannot be null.
474485
/// {@endtemplate}
@@ -2029,6 +2040,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
20292040
textDirection: _textDirection,
20302041
locale: widget.locale,
20312042
textWidthBasis: widget.textWidthBasis,
2043+
obscuringCharacter: widget.obscuringCharacter,
20322044
obscureText: widget.obscureText,
20332045
autocorrect: widget.autocorrect,
20342046
smartDashesType: widget.smartDashesType,
@@ -2063,7 +2075,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
20632075
TextSpan buildTextSpan() {
20642076
if (widget.obscureText) {
20652077
String text = _value.text;
2066-
text = RenderEditable.obscuringCharacter * text.length;
2078+
text = widget.obscuringCharacter * text.length;
20672079
final int o =
20682080
_obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
20692081
if (o != null && o >= 0 && o < text.length)
@@ -2101,6 +2113,7 @@ class _Editable extends LeafRenderObjectWidget {
21012113
this.textAlign,
21022114
@required this.textDirection,
21032115
this.locale,
2116+
this.obscuringCharacter,
21042117
this.obscureText,
21052118
this.autocorrect,
21062119
this.smartDashesType,
@@ -2144,6 +2157,7 @@ class _Editable extends LeafRenderObjectWidget {
21442157
final TextAlign textAlign;
21452158
final TextDirection textDirection;
21462159
final Locale locale;
2160+
final String obscuringCharacter;
21472161
final bool obscureText;
21482162
final TextWidthBasis textWidthBasis;
21492163
final bool autocorrect;
@@ -2192,6 +2206,7 @@ class _Editable extends LeafRenderObjectWidget {
21922206
onSelectionChanged: onSelectionChanged,
21932207
onCaretChanged: onCaretChanged,
21942208
ignorePointer: rendererIgnoresPointer,
2209+
obscuringCharacter: obscuringCharacter,
21952210
obscureText: obscureText,
21962211
textWidthBasis: textWidthBasis,
21972212
cursorWidth: cursorWidth,
@@ -2234,6 +2249,7 @@ class _Editable extends LeafRenderObjectWidget {
22342249
..onCaretChanged = onCaretChanged
22352250
..ignorePointer = rendererIgnoresPointer
22362251
..textWidthBasis = textWidthBasis
2252+
..obscuringCharacter = obscuringCharacter
22372253
..obscureText = obscureText
22382254
..cursorWidth = cursorWidth
22392255
..cursorRadius = cursorRadius

packages/flutter/test/widgets/editable_text_test.dart

+22-1
Original file line numberDiff line numberDiff line change
@@ -2281,7 +2281,7 @@ void main() {
22812281
),
22822282
));
22832283

2284-
const String expectedValue = '••••••••••••••••••••••••';
2284+
final String expectedValue = '•' * originalText.length;
22852285

22862286
expect(
22872287
semantics,
@@ -2368,6 +2368,27 @@ void main() {
23682368
semantics.dispose();
23692369
});
23702370

2371+
testWidgets('password fields can have their obscuring character customized', (WidgetTester tester) async {
2372+
const String originalText = 'super-secret-password!!1';
2373+
controller.text = originalText;
2374+
2375+
const String obscuringCharacter = '#';
2376+
await tester.pumpWidget(MaterialApp(
2377+
home: EditableText(
2378+
backgroundCursorColor: Colors.grey,
2379+
controller: controller,
2380+
obscuringCharacter: obscuringCharacter,
2381+
obscureText: true,
2382+
focusNode: focusNode,
2383+
style: textStyle,
2384+
cursorColor: cursorColor,
2385+
),
2386+
));
2387+
2388+
final String expectedValue = obscuringCharacter * originalText.length;
2389+
expect(findRenderEditable(tester).text.text, expectedValue);
2390+
});
2391+
23712392
group('a11y copy/cut/paste', () {
23722393
Future<void> _buildApp(MockTextSelectionControls controls, WidgetTester tester) {
23732394
return tester.pumpWidget(MaterialApp(

0 commit comments

Comments
 (0)