| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'feedback.dart'; |
| import 'input_decorator.dart'; |
| import 'text_selection.dart'; |
| import 'theme.dart'; |
| |
| export 'package:flutter/services.dart' show TextInputType; |
| |
| /// A material design text field. |
| /// |
| /// A text field lets the user enter text, either with hardware keyboard or with |
| /// an onscreen keyboard. |
| /// |
| /// The text field calls the [onChanged] callback whenever the user changes the |
| /// text in the field. If the user indicates that they are done typing in the |
| /// field (e.g., by pressing a button on the soft keyboard), the text field |
| /// calls the [onSubmitted] callback. |
| /// |
| /// To control the text that is displayed in the text field, use the |
| /// [controller]. For example, to set the initial value of the text field, use |
| /// a [controller] that already contains some text. The [controller] can also |
| /// control the selection and composing region (and to observe changes to the |
| /// text, selection, and composing region). |
| /// |
| /// By default, a text field has a [decoration] that draws a divider below the |
| /// text field. You can use the [decoration] property to control the decoration, |
| /// for example by adding a label or an icon. If you set the [decoration] |
| /// property to null, the decoration will be removed entirely, including the |
| /// extra padding introduced by the decoration to save space for the labels. |
| /// |
| /// If [decoration] is non-null (which is the default), the text field requires |
| /// one of its ancestors to be a [Material] widget. |
| /// |
| /// To integrate the [TextField] into a [Form] with other [FormField] widgets, |
| /// consider using [TextFormField]. |
| /// |
| /// See also: |
| /// |
| /// * <https://material.google.com/components/text-fields.html> |
| /// * [TextFormField], which integrates with the [Form] widget. |
| /// * [InputDecorator], which shows the labels and other visual elements that |
| /// surround the actual text editing widget. |
| /// * [EditableText], which is the raw text editing control at the heart of a |
| /// [TextField]. (The [EditableText] widget is rarely used directly unless |
| /// you are implementing an entirely different design language, such as |
| /// Cupertino.) |
| class TextField extends StatefulWidget { |
| /// Creates a Material Design text field. |
| /// |
| /// If [decoration] is non-null (which is the default), the text field requires |
| /// one of its ancestors to be a [Material] widget. |
| /// |
| /// To remove the decoration entirely (including the extra padding introduced |
| /// by the decoration to save space for the labels), set the [decoration] to |
| /// null. |
| /// |
| /// The [maxLines] property can be set to null to remove the restriction on |
| /// the number of lines. By default, it is one, meaning this is a single-line |
| /// text field. [maxLines] must not be zero. If [maxLines] is not one, then |
| /// [keyboardType] is ignored, and the [TextInputType.multiline] keyboard |
| /// type is used. |
| /// |
| /// The [maxLength] property is set to null by default, which means the |
| /// number of characters allowed in the text field is not restricted. If |
| /// [maxLength] is set, a character counter will be displayed below the |
| /// field, showing how many characters have been entered and how many are |
| /// allowed. After [maxLength] characters have been input, additional input |
| /// is ignored, unless [maxLengthEnforced] is set to false. The TextField |
| /// enforces the length with a [LengthLimitingTextInputFormatter], which is |
| /// evaluated after the supplied [inputFormatters], if any. The [maxLength] |
| /// value must be either null or greater than zero. |
| /// |
| /// If [maxLengthEnforced] is set to false, then more than [maxLength] |
| /// characters may be entered, and the error counter and divider will |
| /// switch to the [decoration.errorStyle] when the limit is exceeded. |
| /// |
| /// The [keyboardType], [textAlign], [autofocus], [obscureText], and |
| /// [autocorrect] arguments must not be null. |
| /// |
| /// See also: |
| /// |
| /// * [maxLength], which discusses the precise meaning of "number of |
| /// characters" and how it may differ from the intuitive meaning. |
| const TextField({ |
| Key key, |
| this.controller, |
| this.focusNode, |
| this.decoration: const InputDecoration(), |
| TextInputType keyboardType: TextInputType.text, |
| this.style, |
| this.textAlign: TextAlign.start, |
| this.autofocus: false, |
| this.obscureText: false, |
| this.autocorrect: true, |
| this.maxLines: 1, |
| this.maxLength, |
| this.maxLengthEnforced: true, |
| this.onChanged, |
| this.onSubmitted, |
| this.inputFormatters, |
| }) : assert(keyboardType != null), |
| assert(textAlign != null), |
| assert(autofocus != null), |
| assert(obscureText != null), |
| assert(autocorrect != null), |
| assert(maxLengthEnforced != null), |
| assert(maxLines == null || maxLines > 0), |
| assert(maxLength == null || maxLength > 0), |
| keyboardType = maxLines == 1 ? keyboardType : TextInputType.multiline, |
| super(key: key); |
| |
| /// Controls the text being edited. |
| /// |
| /// If null, this widget will create its own [TextEditingController]. |
| final TextEditingController controller; |
| |
| /// Controls whether this widget has keyboard focus. |
| /// |
| /// If null, this widget will create its own [FocusNode]. |
| final FocusNode focusNode; |
| |
| /// The decoration to show around the text field. |
| /// |
| /// By default, draws a horizontal line under the text field but can be |
| /// configured to show an icon, label, hint text, and error text. |
| /// |
| /// Set this field to null to remove the decoration entirely (including the |
| /// extra padding introduced by the decoration to save space for the labels). |
| final InputDecoration decoration; |
| |
| /// The type of keyboard to use for editing the text. |
| /// |
| /// Defaults to [TextInputType.text]. Must not be null. If |
| /// [maxLines] is not one, then [keyboardType] is ignored, and the |
| /// [TextInputType.multiline] keyboard type is used. |
| final TextInputType keyboardType; |
| |
| /// The style to use for the text being edited. |
| /// |
| /// This text style is also used as the base style for the [decoration]. |
| /// |
| /// If null, defaults to a text style from the current [Theme]. |
| final TextStyle style; |
| |
| /// How the text being edited should be aligned horizontally. |
| /// |
| /// Defaults to [TextAlign.start]. |
| final TextAlign textAlign; |
| |
| /// Whether this text field should focus itself if nothing else is already |
| /// focused. |
| /// |
| /// If true, the keyboard will open as soon as this text field obtains focus. |
| /// Otherwise, the keyboard is only shown after the user taps the text field. |
| /// |
| /// Defaults to false. Cannot be null. |
| // See https://github.com/flutter/flutter/issues/7035 for the rationale for this |
| // keyboard behavior. |
| final bool autofocus; |
| |
| /// Whether to hide the text being edited (e.g., for passwords). |
| /// |
| /// When this is set to true, all the characters in the text field are |
| /// replaced by U+2022 BULLET characters (•). |
| /// |
| /// Defaults to false. Cannot be null. |
| final bool obscureText; |
| |
| /// Whether to enable autocorrection. |
| /// |
| /// Defaults to true. Cannot be null. |
| final bool autocorrect; |
| |
| /// The maximum number of lines for the text to span, wrapping if necessary. |
| /// |
| /// If this is 1 (the default), the text will not wrap, but will scroll |
| /// horizontally instead. |
| /// |
| /// If this is null, there is no limit to the number of lines. If it is not |
| /// null, the value must be greater than zero. |
| final int maxLines; |
| |
| /// The maximum number of characters (Unicode scalar values) to allow in the |
| /// text field. |
| /// |
| /// If set, a character counter will be displayed below the |
| /// field, showing how many characters have been entered and how many are |
| /// allowed. After [maxLength] characters have been input, additional input |
| /// is ignored, unless [maxLengthEnforced] is set to false. The TextField |
| /// enforces the length with a [LengthLimitingTextInputFormatter], which is |
| /// evaluated after the supplied [inputFormatters], if any. |
| /// |
| /// This value must be either null or greater than zero. If set to null |
| /// (the default), there is no limit to the number of characters allowed. |
| /// |
| /// Whitespace characters (e.g. newline, space, tab) are included in the |
| /// character count. |
| /// |
| /// If [maxLengthEnforced] is set to false, then more than [maxLength] |
| /// characters may be entered, but the error counter and divider will |
| /// switch to the [decoration.errorStyle] when the limit is exceeded. |
| /// |
| /// The TextField does not currently count Unicode grapheme clusters (i.e. |
| /// characters visible to the user), it counts Unicode scalar values, which |
| /// leaves out a number of useful possible characters (like many emoji and |
| /// composed characters), so this will be inaccurate in the presence of those |
| /// characters. If you expect to encounter these kinds of characters, be |
| /// generous in the maxLength used. |
| /// |
| /// For instance, the character "ö" can be represented as '\u{006F}\u{0308}', |
| /// which is the letter "o" followed by a composed diaeresis "¨", or it can |
| /// be represented as '\u{00F6}', which is the Unicode scalar value "LATIN |
| /// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will |
| /// count two characters, and the second case will be counted as one |
| /// character, even though the user can see no difference in the input. |
| /// |
| /// Similarly, some emoji are represented by multiple scalar values. The |
| /// Unicode "THUMBS UP SIGN + MEDIUM SKIN TONE MODIFIER", "👍🏽", should be |
| /// counted as a single character, but because it is a combination of two |
| /// Unicode scalar values, '\u{1F44D}\u{1F3FD}', it is counted as two |
| /// characters. |
| /// |
| /// See also: |
| /// |
| /// * [LengthLimitingTextInputFormatter] for more information on how it |
| /// counts characters, and how it may differ from the intuitive meaning. |
| final int maxLength; |
| |
| /// If true, prevents the field from allowing more than [maxLength] |
| /// characters. |
| /// |
| /// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to |
| /// enforce the limit, or merely provide a character counter and warning when |
| /// [maxLength] is exceeded. |
| final bool maxLengthEnforced; |
| |
| /// Called when the text being edited changes. |
| final ValueChanged<String> onChanged; |
| |
| /// Called when the user indicates that they are done editing the text in the |
| /// field. |
| final ValueChanged<String> onSubmitted; |
| |
| /// Optional input validation and formatting overrides. |
| /// |
| /// Formatters are run in the provided order when the text input changes. |
| final List<TextInputFormatter> inputFormatters; |
| |
| @override |
| _TextFieldState createState() => new _TextFieldState(); |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder description) { |
| super.debugFillProperties(description); |
| description.add(new DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null)); |
| description.add(new DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null)); |
| description.add(new DiagnosticsProperty<InputDecoration>('decoration', decoration)); |
| description.add(new EnumProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text)); |
| description.add(new DiagnosticsProperty<TextStyle>('style', style, defaultValue: null)); |
| description.add(new DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false)); |
| description.add(new DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); |
| description.add(new DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: false)); |
| description.add(new IntProperty('maxLines', maxLines, defaultValue: 1)); |
| description.add(new IntProperty('maxLength', maxLength, defaultValue: null)); |
| description.add(new FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced')); |
| } |
| } |
| |
| class _TextFieldState extends State<TextField> { |
| final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>(); |
| |
| TextEditingController _controller; |
| TextEditingController get _effectiveController => widget.controller ?? _controller; |
| |
| FocusNode _focusNode; |
| FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= new FocusNode()); |
| |
| bool get needsCounter => widget.maxLength != null |
| && widget.decoration != null |
| && widget.decoration.counterText == null; |
| |
| InputDecoration _getEffectiveDecoration() { |
| if (!needsCounter) |
| return widget.decoration; |
| |
| final InputDecoration effectiveDecoration = widget?.decoration ?? const InputDecoration(); |
| final String counterText = '${_effectiveController.value.text.runes.length} / ${widget.maxLength}'; |
| if (_effectiveController.value.text.runes.length > widget.maxLength) { |
| final ThemeData themeData = Theme.of(context); |
| return effectiveDecoration.copyWith( |
| errorText: effectiveDecoration.errorText ?? '', |
| counterStyle: effectiveDecoration.errorStyle |
| ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor), |
| counterText: counterText, |
| ); |
| } |
| return effectiveDecoration.copyWith(counterText: counterText); |
| } |
| |
| @override |
| void initState() { |
| super.initState(); |
| if (widget.controller == null) |
| _controller = new TextEditingController(); |
| } |
| |
| @override |
| void didUpdateWidget(TextField oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.controller == null && oldWidget.controller != null) |
| _controller = new TextEditingController.fromValue(oldWidget.controller.value); |
| else if (widget.controller != null && oldWidget.controller == null) |
| _controller = null; |
| } |
| |
| @override |
| void dispose() { |
| _focusNode?.dispose(); |
| super.dispose(); |
| } |
| |
| void _requestKeyboard() { |
| _editableTextKey.currentState?.requestKeyboard(); |
| } |
| |
| void _onSelectionChanged(BuildContext context, bool longPress) { |
| if (longPress) |
| Feedback.forLongPress(context); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final ThemeData themeData = Theme.of(context); |
| final TextStyle style = widget.style ?? themeData.textTheme.subhead; |
| final TextEditingController controller = _effectiveController; |
| final FocusNode focusNode = _effectiveFocusNode; |
| final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[]; |
| if (widget.maxLength != null && widget.maxLengthEnforced) |
| formatters.add(new LengthLimitingTextInputFormatter(widget.maxLength)); |
| |
| Widget child = new RepaintBoundary( |
| child: new EditableText( |
| key: _editableTextKey, |
| controller: controller, |
| focusNode: focusNode, |
| keyboardType: widget.keyboardType, |
| style: style, |
| textAlign: widget.textAlign, |
| autofocus: widget.autofocus, |
| obscureText: widget.obscureText, |
| autocorrect: widget.autocorrect, |
| maxLines: widget.maxLines, |
| cursorColor: themeData.textSelectionColor, |
| selectionColor: themeData.textSelectionColor, |
| selectionControls: themeData.platform == TargetPlatform.iOS |
| ? cupertinoTextSelectionControls |
| : materialTextSelectionControls, |
| onChanged: widget.onChanged, |
| onSubmitted: widget.onSubmitted, |
| onSelectionChanged: (TextSelection _, bool longPress) => _onSelectionChanged(context, longPress), |
| inputFormatters: formatters, |
| ), |
| ); |
| |
| if (widget.decoration != null) { |
| child = new AnimatedBuilder( |
| animation: new Listenable.merge(<Listenable>[ focusNode, controller ]), |
| builder: (BuildContext context, Widget child) { |
| return new InputDecorator( |
| decoration: _getEffectiveDecoration(), |
| baseStyle: widget.style, |
| textAlign: widget.textAlign, |
| isFocused: focusNode.hasFocus, |
| isEmpty: controller.value.text.isEmpty, |
| child: child, |
| ); |
| }, |
| child: child, |
| ); |
| } |
| |
| return new GestureDetector( |
| behavior: HitTestBehavior.opaque, |
| onTap: _requestKeyboard, |
| child: child, |
| ); |
| } |
| } |