// 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 'dart:collection'; import 'package:flutter/cupertino.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'debug.dart'; import 'feedback.dart'; import 'ink_well.dart' show InteractiveInkFeature; import 'input_decorator.dart'; import 'material.dart'; import 'text_selection.dart'; import 'theme.dart'; export 'package:flutter/services.dart' show TextInputType, TextInputAction; /// 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. When the [TextField] is /// tapped an ink splash that paints on the material is triggered, see /// [ThemeData.splashFactory]. /// /// To integrate the [TextField] into a [Form] with other [FormField] widgets, /// consider using [TextFormField]. /// /// See also: /// /// * /// * [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.textInputAction = TextInputAction.done, 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, this.enabled, }) : assert(keyboardType != null), assert(textInputAction != 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. /// /// Specify 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 type of action button to use for the keyboard. /// /// Defaults to [TextInputAction.done]. Must not be null. final TextInputAction textInputAction; /// 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 the `subhead` 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. /// /// ## Limitations /// /// 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 onChanged; /// Called when the user indicates that they are done editing the text in the /// field. final ValueChanged onSubmitted; /// Optional input validation and formatting overrides. /// /// Formatters are run in the provided order when the text input changes. final List inputFormatters; /// If false the textfield is "disabled": it ignores taps and its /// [decoration] is rendered in grey. /// /// If non-null this property overrides the [decoration]'s /// [Decoration.enabled] property. final bool enabled; @override _TextFieldState createState() => new _TextFieldState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(new DiagnosticsProperty('controller', controller, defaultValue: null)); properties.add(new DiagnosticsProperty('focusNode', focusNode, defaultValue: null)); properties.add(new DiagnosticsProperty('decoration', decoration)); properties.add(new DiagnosticsProperty('keyboardType', keyboardType, defaultValue: TextInputType.text)); properties.add(new DiagnosticsProperty('style', style, defaultValue: null)); properties.add(new DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); properties.add(new DiagnosticsProperty('obscureText', obscureText, defaultValue: false)); properties.add(new DiagnosticsProperty('autocorrect', autocorrect, defaultValue: false)); properties.add(new IntProperty('maxLines', maxLines, defaultValue: 1)); properties.add(new IntProperty('maxLength', maxLength, defaultValue: null)); properties.add(new FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced')); } } class _TextFieldState extends State with AutomaticKeepAliveClientMixin { final GlobalKey _editableTextKey = new GlobalKey(); Set _splashes; InteractiveInkFeature _currentSplash; 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() { final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration()) .applyDefaults(Theme.of(context).inputDecorationTheme) .copyWith( enabled: widget.enabled, ); if (!needsCounter) return effectiveDecoration; 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; final bool isEnabled = widget.enabled ?? widget.decoration?.enabled ?? true; final bool wasEnabled = oldWidget.enabled ?? oldWidget.decoration?.enabled ?? true; if (wasEnabled && !isEnabled) { _effectiveFocusNode.unfocus(); } } @override void dispose() { _focusNode?.dispose(); super.dispose(); } void _requestKeyboard() { _editableTextKey.currentState?.requestKeyboard(); } void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { if (cause == SelectionChangedCause.longPress) Feedback.forLongPress(context); } InteractiveInkFeature _createInkFeature(TapDownDetails details) { final MaterialInkController inkController = Material.of(context); final BuildContext editableContext = _editableTextKey.currentContext; final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject(); final Offset position = referenceBox.globalToLocal(details.globalPosition); final Color color = Theme.of(context).splashColor; InteractiveInkFeature splash; void handleRemoved() { if (_splashes != null) { assert(_splashes.contains(splash)); _splashes.remove(splash); if (_currentSplash == splash) _currentSplash = null; updateKeepAlive(); } // else we're probably in deactivate() } splash = Theme.of(context).splashFactory.create( controller: inkController, referenceBox: referenceBox, position: position, color: color, containedInkWell: true, // TODO(hansmuller): splash clip borderRadius should match the input decorator's border. borderRadius: BorderRadius.zero, onRemoved: handleRemoved, ); return splash; } RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable; void _handleTapDown(TapDownDetails details) { _renderEditable.handleTapDown(details); _startSplash(details); } void _handleTap() { _renderEditable.handleTap(); _requestKeyboard(); _confirmCurrentSplash(); } void _handleTapCancel() { _cancelCurrentSplash(); } void _handleLongPress() { _renderEditable.handleLongPress(); _confirmCurrentSplash(); } void _startSplash(TapDownDetails details) { if (_effectiveFocusNode.hasFocus) return; final InteractiveInkFeature splash = _createInkFeature(details); _splashes ??= new HashSet(); _splashes.add(splash); _currentSplash = splash; updateKeepAlive(); } void _confirmCurrentSplash() { _currentSplash?.confirm(); _currentSplash = null; } void _cancelCurrentSplash() { _currentSplash?.cancel(); } @override bool get wantKeepAlive => _splashes != null && _splashes.isNotEmpty; @override void deactivate() { if (_splashes != null) { final Set splashes = _splashes; _splashes = null; for (InteractiveInkFeature splash in splashes) splash.dispose(); _currentSplash = null; } assert(_currentSplash == null); super.deactivate(); } @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. assert(debugCheckHasMaterial(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 formatters = widget.inputFormatters ?? []; 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, textInputAction: widget.textInputAction, 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: _handleSelectionChanged, inputFormatters: formatters, rendererIgnoresPointer: true, ), ); if (widget.decoration != null) { child = new AnimatedBuilder( animation: new Listenable.merge([ 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 Semantics( onTap: () { if (!_effectiveController.selection.isValid) _effectiveController.selection = new TextSelection.collapsed(offset: _effectiveController.text.length); _requestKeyboard(); }, child: new IgnorePointer( ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true), child: new GestureDetector( behavior: HitTestBehavior.translucent, onTapDown: _handleTapDown, onTap: _handleTap, onTapCancel: _handleTapCancel, onLongPress: _handleLongPress, excludeFromSemantics: true, child: child, ), ), ); } }