text_field.dart 19.3 KB
Newer Older
1 2 3 4
// 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.

5 6
import 'dart:collection';

xster's avatar
xster committed
7
import 'package:flutter/cupertino.dart';
8
import 'package:flutter/foundation.dart';
9
import 'package:flutter/rendering.dart';
10 11 12
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

13
import 'feedback.dart';
14
import 'ink_well.dart' show InteractiveInkFeature;
15
import 'input_decorator.dart';
16
import 'material.dart';
17 18 19 20 21
import 'text_selection.dart';
import 'theme.dart';

export 'package:flutter/services.dart' show TextInputType;

22
/// A material design text field.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
///
/// 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.
69 70
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
71 72
  /// 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
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
  /// [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.
89
  ///
Ian Hickson's avatar
Ian Hickson committed
90 91
  /// The [keyboardType], [textAlign], [autofocus], [obscureText], and
  /// [autocorrect] arguments must not be null.
92 93 94 95 96
  ///
  /// See also:
  ///
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
97
  const TextField({
98 99 100 101
    Key key,
    this.controller,
    this.focusNode,
    this.decoration: const InputDecoration(),
102
    TextInputType keyboardType: TextInputType.text,
103
    this.style,
Ian Hickson's avatar
Ian Hickson committed
104
    this.textAlign: TextAlign.start,
105 106
    this.autofocus: false,
    this.obscureText: false,
107
    this.autocorrect: true,
108
    this.maxLines: 1,
109 110
    this.maxLength,
    this.maxLengthEnforced: true,
111 112
    this.onChanged,
    this.onSubmitted,
113
    this.inputFormatters,
114
  }) : assert(keyboardType != null),
Ian Hickson's avatar
Ian Hickson committed
115
       assert(textAlign != null),
116 117
       assert(autofocus != null),
       assert(obscureText != null),
118
       assert(autocorrect != null),
119
       assert(maxLengthEnforced != null),
120
       assert(maxLines == null || maxLines > 0),
121
       assert(maxLength == null || maxLength > 0),
122
       keyboardType = maxLines == 1 ? keyboardType : TextInputType.multiline,
123
       super(key: key);
124 125 126

  /// Controls the text being edited.
  ///
127
  /// If null, this widget will create its own [TextEditingController].
128 129 130 131 132 133 134 135 136
  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.
  ///
137
  /// By default, draws a horizontal line under the text field but can be
138 139 140 141 142 143 144
  /// 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.
145
  ///
146 147 148
  /// 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.
149 150 151 152 153 154
  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].
  ///
155
  /// If null, defaults to the `subhead` text style from the current [Theme].
156 157
  final TextStyle style;

158
  /// How the text being edited should be aligned horizontally.
Ian Hickson's avatar
Ian Hickson committed
159 160
  ///
  /// Defaults to [TextAlign.start].
161 162 163
  final TextAlign textAlign;

  /// Whether this text field should focus itself if nothing else is already
164 165
  /// focused.
  ///
166
  /// If true, the keyboard will open as soon as this text field obtains focus.
167 168
  /// Otherwise, the keyboard is only shown after the user taps the text field.
  ///
169
  /// Defaults to false. Cannot be null.
170 171 172 173 174 175
  // 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).
  ///
176 177
  /// When this is set to true, all the characters in the text field are
  /// replaced by U+2022 BULLET characters (•).
178
  ///
179
  /// Defaults to false. Cannot be null.
180 181
  final bool obscureText;

182 183 184 185 186
  /// Whether to enable autocorrection.
  ///
  /// Defaults to true. Cannot be null.
  final bool autocorrect;

187 188 189 190
  /// 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.
191 192 193
  ///
  /// 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.
194 195
  final int maxLines;

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
  /// 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
226
  /// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will
227 228 229 230 231 232 233 234 235 236
  /// 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:
237
  ///
238 239 240 241 242 243 244 245 246 247 248 249
  ///  * [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;

250 251 252 253 254 255 256
  /// 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;

xster's avatar
xster committed
257 258 259
  /// Optional input validation and formatting overrides.
  ///
  /// Formatters are run in the provided order when the text input changes.
260 261
  final List<TextInputFormatter> inputFormatters;

262 263 264 265
  @override
  _TextFieldState createState() => new _TextFieldState();

  @override
266
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
267 268 269 270 271 272 273 274 275 276
    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));
277 278
    description.add(new IntProperty('maxLength', maxLength, defaultValue: null));
    description.add(new FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced'));
279 280 281
  }
}

282
class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin {
283 284
  final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();

285 286 287
  Set<InteractiveInkFeature> _splashes;
  InteractiveInkFeature _currentSplash;

288
  TextEditingController _controller;
289
  TextEditingController get _effectiveController => widget.controller ?? _controller;
290 291

  FocusNode _focusNode;
292
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= new FocusNode());
293

294 295 296 297 298
  bool get needsCounter => widget.maxLength != null
    && widget.decoration != null
    && widget.decoration.counterText == null;

  InputDecoration _getEffectiveDecoration() {
299 300 301
    final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
      .applyDefaults(Theme.of(context).inputDecorationTheme);

302
    if (!needsCounter)
303
      return effectiveDecoration;
304 305 306 307 308 309 310 311 312 313 314 315 316 317

    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);
  }

318 319 320
  @override
  void initState() {
    super.initState();
321
    if (widget.controller == null)
322 323 324 325
      _controller = new TextEditingController();
  }

  @override
326
  void didUpdateWidget(TextField oldWidget) {
327
    super.didUpdateWidget(oldWidget);
328
    if (widget.controller == null && oldWidget.controller != null)
329
      _controller = new TextEditingController.fromValue(oldWidget.controller.value);
330
    else if (widget.controller != null && oldWidget.controller == null)
331 332 333 334 335 336 337 338 339 340 341 342 343
      _controller = null;
  }

  @override
  void dispose() {
    _focusNode?.dispose();
    super.dispose();
  }

  void _requestKeyboard() {
    _editableTextKey.currentState?.requestKeyboard();
  }

344
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
345
    if (cause == SelectionChangedCause.longPress)
346 347 348
      Feedback.forLongPress(context);
  }

349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
  InteractiveInkFeature _createInkFeature(TapDownDetails details) {
    final MaterialInkController inkController = Material.of(context);
    final RenderBox referenceBox = InputDecorator.containerOf(_editableTextKey.currentContext);
    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<InteractiveInkFeature>();
    _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<InteractiveInkFeature> splashes = _splashes;
      _splashes = null;
      for (InteractiveInkFeature splash in splashes)
        splash.dispose();
      _currentSplash = null;
    }
    assert(_currentSplash == null);
    super.deactivate();
  }

437 438
  @override
  Widget build(BuildContext context) {
439
    super.build(context); // See AutomaticKeepAliveClientMixin.
440
    final ThemeData themeData = Theme.of(context);
441
    final TextStyle style = widget.style ?? themeData.textTheme.subhead;
442 443
    final TextEditingController controller = _effectiveController;
    final FocusNode focusNode = _effectiveFocusNode;
444 445 446
    final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
    if (widget.maxLength != null && widget.maxLengthEnforced)
      formatters.add(new LengthLimitingTextInputFormatter(widget.maxLength));
447 448 449 450 451 452

    Widget child = new RepaintBoundary(
      child: new EditableText(
        key: _editableTextKey,
        controller: controller,
        focusNode: focusNode,
453
        keyboardType: widget.keyboardType,
454
        style: style,
455 456 457
        textAlign: widget.textAlign,
        autofocus: widget.autofocus,
        obscureText: widget.obscureText,
458
        autocorrect: widget.autocorrect,
459
        maxLines: widget.maxLines,
460 461
        cursorColor: themeData.textSelectionColor,
        selectionColor: themeData.textSelectionColor,
xster's avatar
xster committed
462 463 464
        selectionControls: themeData.platform == TargetPlatform.iOS
            ? cupertinoTextSelectionControls
            : materialTextSelectionControls,
465 466
        onChanged: widget.onChanged,
        onSubmitted: widget.onSubmitted,
467
        onSelectionChanged: _handleSelectionChanged,
468
        inputFormatters: formatters,
469
        rendererIgnoresPointer: true,
470 471 472
      ),
    );

473
    if (widget.decoration != null) {
474 475 476 477
      child = new AnimatedBuilder(
        animation: new Listenable.merge(<Listenable>[ focusNode, controller ]),
        builder: (BuildContext context, Widget child) {
          return new InputDecorator(
478
            decoration: _getEffectiveDecoration(),
479 480
            baseStyle: widget.style,
            textAlign: widget.textAlign,
481 482 483 484 485 486 487 488 489
            isFocused: focusNode.hasFocus,
            isEmpty: controller.value.text.isEmpty,
            child: child,
          );
        },
        child: child,
      );
    }

490 491 492 493 494 495 496
    return new Semantics(
      onTap: () {
        if (!_controller.selection.isValid)
          _controller.selection = new TextSelection.collapsed(offset: _controller.text.length);
        _requestKeyboard();
      },
      child: new GestureDetector(
497 498 499 500 501
        behavior: HitTestBehavior.translucent,
        onTapDown: _handleTapDown,
        onTap: _handleTap,
        onTapCancel: _handleTapCancel,
        onLongPress: _handleLongPress,
502
        excludeFromSemantics: true,
503
        child: child,
504
      ),
505 506 507
    );
  }
}