blob: 12f5d2992d3e938eae140dfaf31907e83a1f9bf6 [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 '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,
);
}
}