blob: 36a02821298def560eafc6930939f2c400efe635 [file] [log] [blame]
// 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 'material_localizations.dart';
import 'text_selection.dart';
import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization;
/// 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.
/// 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 [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.decoration = const InputDecoration(),
TextInputType keyboardType,
this.textCapitalization = TextCapitalization.none,,
this.textAlign = TextAlign.start,
this.autofocus = false,
this.obscureText = false,
this.autocorrect = true,
this.maxLines = 1,
this.maxLengthEnforced = true,
this.cursorWidth = 2.0,
this.scrollPadding = const EdgeInsets.all(20.0),
}) : assert(textAlign != null),
assert(autofocus != null),
assert(obscureText != null),
assert(autocorrect != null),
assert(maxLengthEnforced != null),
assert(scrollPadding != null),
assert(maxLines == null || maxLines > 0),
assert(maxLength == null || maxLength > 0),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : 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] if [maxLines] is one and
/// [TextInputType.multiline] otherwise.
final TextInputType keyboardType;
/// The type of action button to use for the keyboard.
/// Defaults to [TextInputAction.newline] if [keyboardType] is
/// [TextInputType.multiline] and [TextInputAction.done] otherwise.
final TextInputAction textInputAction;
/// Configures how the platform keyboard will select an uppercase or
/// lowercase keyboard.
/// Only supports text keyboards, other keyboard types will ignore this
/// configuration. Capitalization is locale-aware.
/// Defaults to [TextCapitalization.none]. Must not be null.
/// See also:
/// * [TextCapitalization], for a description of each capitalization behavior.
final TextCapitalization textCapitalization;
/// 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 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<String> onChanged;
/// Called when the user submits editable content (e.g., user presses the "done"
/// button on the keyboard).
/// The default implementation of [onEditingComplete] executes 2 different
/// behaviors based on the situation:
/// - When a completion action is pressed, such as "done", "go", "send", or
/// "search", the user's content is submitted to the [controller] and then
/// focus is given up.
/// - When a non-completion action is pressed, such as "next" or "previous",
/// the user's content is submitted to the [controller], but focus is not
/// given up because developers may want to immediately move focus to
/// another input widget within [onSubmitted].
/// Providing [onEditingComplete] prevents the aforementioned default behavior.
final VoidCallback onEditingComplete;
/// 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;
/// 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;
/// How thick the cursor will be.
/// Defaults to 2.0.
final double cursorWidth;
/// How rounded the corners of the cursor should be.
/// By default, the cursor has a null Radius
final Radius cursorRadius;
/// The color to use when painting the cursor.
final Color cursorColor;
/// The appearance of the keyboard.
/// This setting is only honored on iOS devices.
/// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
final Brightness keyboardAppearance;
/// Configures padding to edges surrounding a [Scrollable] when the Textfield scrolls into view.
/// When this widget receives focus and is not completely visible (for example scrolled partially
/// off the screen or overlapped by the keyboard)
/// then it will attempt to make itself visible by scrolling a surrounding [Scrollable], if one is present.
/// This value controls how far from the edges of a [Scrollable] the TextField will be positioned after the scroll.
/// Defaults to EdgeInserts.all(20.0).
final EdgeInsets scrollPadding;
_TextFieldState createState() => new _TextFieldState();
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties.add(new DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
properties.add(new DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
properties.add(new DiagnosticsProperty<InputDecoration>('decoration', decoration));
properties.add(new DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text));
properties.add(new DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
properties.add(new DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
properties.add(new DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
properties.add(new DiagnosticsProperty<bool>('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<TextField> with AutomaticKeepAliveClientMixin {
final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();
Set<InteractiveInkFeature> _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 MaterialLocalizations localizations = MaterialLocalizations.of(context);
final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
enabled: widget.enabled,
if (!needsCounter)
return effectiveDecoration;
final int currentLength = _effectiveController.value.text.runes.length;
final String counterText = '$currentLength/${widget.maxLength}';
final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength);
final String semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);
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,
semanticCounterText: semanticCounterText,
return effectiveDecoration.copyWith(
counterText: counterText,
semanticCounterText: semanticCounterText,
void initState() {
if (widget.controller == null)
_controller = new TextEditingController();
void didUpdateWidget(TextField 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) {
void dispose() {
void _requestKeyboard() {
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
if (cause == SelectionChangedCause.longPress)
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) {
if (_currentSplash == splash)
_currentSplash = null;
} // 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.
onRemoved: handleRemoved,
return splash;
RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable;
void _handleTapDown(TapDownDetails details) {
void _handleTap() {
void _handleTapCancel() {
void _handleLongPress() {
void _startSplash(TapDownDetails details) {
if (_effectiveFocusNode.hasFocus)
final InteractiveInkFeature splash = _createInkFeature(details);
_splashes ??= new HashSet<InteractiveInkFeature>();
_currentSplash = splash;
void _confirmCurrentSplash() {
_currentSplash = null;
void _cancelCurrentSplash() {
bool get wantKeepAlive => _splashes != null && _splashes.isNotEmpty;
void deactivate() {
if (_splashes != null) {
final Set<InteractiveInkFeature> splashes = _splashes;
_splashes = null;
for (InteractiveInkFeature splash in splashes)
_currentSplash = null;
assert(_currentSplash == null);
Widget build(BuildContext context) {; // See AutomaticKeepAliveClientMixin.
final ThemeData themeData = Theme.of(context);
final TextStyle style = ?? themeData.textTheme.subhead;
final Brightness keyboardAppearance = widget.keyboardAppearance ?? themeData.primaryColorBrightness;
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,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: style,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
maxLines: widget.maxLines,
selectionColor: themeData.textSelectionColor,
selectionControls: themeData.platform == TargetPlatform.iOS
? cupertinoTextSelectionControls
: materialTextSelectionControls,
onChanged: widget.onChanged,
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
onSelectionChanged: _handleSelectionChanged,
inputFormatters: formatters,
rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius,
cursorColor: widget.cursorColor ?? Theme.of(context).cursorColor,
scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance,
if (widget.decoration != null) {
child = new AnimatedBuilder(
animation: new Listenable.merge(<Listenable>[ focusNode, controller ]),
builder: (BuildContext context, Widget child) {
return new InputDecorator(
decoration: _getEffectiveDecoration(),
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);
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,