text_field.dart 42.1 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/rendering.dart';
9 10
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
11
import 'package:flutter/foundation.dart';
12
import 'package:flutter/gestures.dart';
13

14
import 'debug.dart';
15
import 'feedback.dart';
16
import 'ink_well.dart' show InteractiveInkFeature;
17
import 'input_decorator.dart';
18
import 'material.dart';
19
import 'material_localizations.dart';
20
import 'selectable_text.dart' show iOSHorizontalOffset;
21 22 23
import 'text_selection.dart';
import 'theme.dart';

24
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization;
25

26 27 28
/// Signature for the [TextField.buildCounter] callback.
typedef InputCounterWidgetBuilder = Widget Function(
  /// The build context for the TextField
29 30 31 32 33 34 35 36 37
  BuildContext context, {
  /// The length of the string currently in the input.
  @required int currentLength,
  /// The maximum string length that can be entered into the TextField.
  @required int maxLength,
  /// Whether or not the TextField is currently focused.  Mainly provided for
  /// the [liveRegion] parameter in the [Semantics] widget for accessibility.
  @required bool isFocused,
});
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
  _TextFieldSelectionGestureDetectorBuilder({
    @required _TextFieldState state
  }) : _state = state,
       super(delegate: state);

  final _TextFieldState _state;

  @override
  void onTapDown(TapDownDetails details) {
    super.onTapDown(details);
    _state._startSplash(details.globalPosition);
  }

  @override
  void onForcePressStart(ForcePressDetails details) {
    super.onForcePressStart(details);
    if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
      editableText.showToolbar();
    }
  }

  @override
  void onForcePressEnd(ForcePressDetails details) {
    // Not required.
  }

  @override
  void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
    if (delegate.selectionEnabled) {
      switch (Theme.of(_state.context).platform) {
        case TargetPlatform.iOS:
          renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          renderEditable.selectWordsInRange(
            from: details.globalPosition - details.offsetFromOrigin,
            to: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
      }
    }
  }

  @override
  void onSingleTapUp(TapUpDetails details) {
    editableText.hideToolbar();
    if (delegate.selectionEnabled) {
      switch (Theme.of(_state.context).platform) {
        case TargetPlatform.iOS:
          renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          renderEditable.selectPosition(cause: SelectionChangedCause.tap);
          break;
      }
    }
    _state._requestKeyboard();
    _state._confirmCurrentSplash();
    if (_state.widget.onTap != null)
      _state.widget.onTap();
  }

  @override
  void onSingleTapCancel() {
    _state._cancelCurrentSplash();
  }

  @override
  void onSingleLongTapStart(LongPressStartDetails details) {
    if (delegate.selectionEnabled) {
      switch (Theme.of(_state.context).platform) {
        case TargetPlatform.iOS:
          renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
125
          renderEditable.selectWord(cause: SelectionChangedCause.longPress);
126 127 128 129 130 131 132 133 134 135 136 137 138 139
          Feedback.forLongPress(_state.context);
          break;
      }
    }
    _state._confirmCurrentSplash();
  }

  @override
  void onDragSelectionStart(DragStartDetails details) {
    super.onDragSelectionStart(details);
    _state._startSplash(details.globalPosition);
  }
}

140
/// A material design text field.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
///
/// 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
163 164 165
/// 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].
166 167 168 169
///
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
170 171 172
/// Remember to [dispose] of the [TextEditingController] when it is no longer needed.
/// This will ensure we discard any resources used by the object.
///
173 174 175 176 177
/// {@tool sample}
/// This example shows how to create a [TextField] that will obscure input. The
/// [InputDecoration] surrounds the field in a border using [OutlineInputBorder]
/// and adds a label.
///
178
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/text_field.png)
179
///
180 181 182 183 184 185 186 187 188 189 190
/// ```dart
/// TextField(
///   obscureText: true,
///   decoration: InputDecoration(
///     border: OutlineInputBorder(),
///     labelText: 'Password',
///   ),
/// )
/// ```
/// {@end-tool}
///
191 192 193 194 195 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 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
/// ## Reading values
///
/// A common way to read a value from a TextField is to use the [onSubmitted]
/// callback. This callback is applied to the text field's current value when
/// the user finishes editing.
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// This sample shows how to get a value from a TextField via the [onSubmitted]
/// callback.
///
/// ```dart
/// TextEditingController _controller;
///
/// void initState() {
///   super.initState();
///   _controller = TextEditingController();
/// }
///
/// void dispose() {
///   _controller.dispose();
///   super.dispose();
/// }
///
/// Widget build(BuildContext context) {
///   return Scaffold(
///     body: Center(
///       child: TextField(
///         controller: _controller,
///         onSubmitted: (String value) async {
///           await showDialog<void>(
///             context: context,
///             builder: (BuildContext context) {
///               return AlertDialog(
///                 title: const Text('Thanks!'),
///                 content: Text ('You typed "$value".'),
///                 actions: <Widget>[
///                   FlatButton(
///                     onPressed: () { Navigator.pop(context); },
///                     child: const Text('OK'),
///                   ),
///                 ],
///               );
///             },
///           );
///         },
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
/// For most applications the [onSubmitted] callback will be sufficient for
/// reacting to user input.
///
/// The [onEditingComplete] callback also runs when the user finishes editing.
/// It's different from [onSubmitted] because it has a default value which
/// updates the text controller and yields the keyboard focus. Applications that
/// require different behavior can override the default [onEditingComplete]
/// callback.
///
/// Keep in mind you can also always read the current string from a TextField's
/// [TextEditingController] using [TextEditingController.text].
///
256 257
/// See also:
///
258
///  * <https://material.io/design/components/text-fields.html>
259 260 261 262
///  * [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
263
///    [TextField]. The [EditableText] widget is rarely used directly unless
264
///    you are implementing an entirely different design language, such as
265
///    Cupertino.
266 267
///  * Learn how to use a [TextEditingController] in one of our
///    [cookbook recipe](https://flutter.dev/docs/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller)s.
268 269 270 271 272 273 274 275 276
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.
277 278
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
279
  /// the number of lines. By default, it is one, meaning this is a single-line
280
  /// text field. [maxLines] must not be zero.
281 282 283
  ///
  /// The [maxLength] property is set to null by default, which means the
  /// number of characters allowed in the text field is not restricted. If
284 285 286 287 288
  /// [maxLength] is set a character counter will be displayed below the
  /// field showing how many characters have been entered. If the value is
  /// set to a positive integer it will also display the maximum allowed
  /// number of characters to be entered.  If the value is set to
  /// [TextField.noMaxLength] then only the current length is displayed.
289 290
  ///
  /// After [maxLength] characters have been input, additional input
291
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
292 293 294 295 296 297 298
  /// 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.
299
  ///
300 301 302 303
  /// The text cursor is not shown if [showCursor] is false or if [showCursor]
  /// is null (the default) and [readOnly] is true.
  ///
  /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
304 305
  /// [maxLengthEnforced], [scrollPadding], [maxLines], and [maxLength]
  /// arguments must not be null.
306 307 308 309 310
  ///
  /// See also:
  ///
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
311
  const TextField({
312 313 314
    Key key,
    this.controller,
    this.focusNode,
315
    this.decoration = const InputDecoration(),
316 317
    TextInputType keyboardType,
    this.textInputAction,
318
    this.textCapitalization = TextCapitalization.none,
319
    this.style,
320
    this.strutStyle,
321
    this.textAlign = TextAlign.start,
322
    this.textAlignVertical,
323
    this.textDirection,
324
    this.readOnly = false,
325
    ToolbarOptions toolbarOptions,
326
    this.showCursor,
327 328 329 330
    this.autofocus = false,
    this.obscureText = false,
    this.autocorrect = true,
    this.maxLines = 1,
331 332
    this.minLines,
    this.expands = false,
333
    this.maxLength,
334
    this.maxLengthEnforced = true,
335
    this.onChanged,
336
    this.onEditingComplete,
337
    this.onSubmitted,
338
    this.inputFormatters,
339
    this.enabled,
340 341 342
    this.cursorWidth = 2.0,
    this.cursorRadius,
    this.cursorColor,
343
    this.keyboardAppearance,
344
    this.scrollPadding = const EdgeInsets.all(20.0),
345
    this.dragStartBehavior = DragStartBehavior.start,
346
    this.enableInteractiveSelection = true,
347
    this.onTap,
348
    this.buildCounter,
349
    this.scrollController,
350
    this.scrollPhysics,
351
  }) : assert(textAlign != null),
352
       assert(readOnly != null),
353 354
       assert(autofocus != null),
       assert(obscureText != null),
355
       assert(autocorrect != null),
356
       assert(enableInteractiveSelection != null),
357
       assert(maxLengthEnforced != null),
358
       assert(scrollPadding != null),
359
       assert(dragStartBehavior != null),
360
       assert(maxLines == null || maxLines > 0),
361 362 363 364 365 366 367 368 369 370
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
         'minLines can\'t be greater than maxLines',
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
371
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
372
       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
373 374 375 376 377 378 379 380 381 382 383
       toolbarOptions = toolbarOptions ?? obscureText ?
         const ToolbarOptions(
           selectAll: true,
           paste: true,
         ) :
         const ToolbarOptions(
           copy: true,
           cut: true,
           selectAll: true,
           paste: true,
         ),
384
       super(key: key);
385 386 387

  /// Controls the text being edited.
  ///
388
  /// If null, this widget will create its own [TextEditingController].
389 390
  final TextEditingController controller;

391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
  /// Defines the keyboard focus for this widget.
  ///
  /// The [focusNode] is a long-lived object that's typically managed by a
  /// [StatefulWidget] parent. See [FocusNode] for more information.
  ///
  /// To give the keyboard focus to this widget, provide a [focusNode] and then
  /// use the current [FocusScope] to request the focus:
  ///
  /// ```dart
  /// FocusScope.of(context).requestFocus(myFocusNode);
  /// ```
  ///
  /// This happens automatically when the widget is tapped.
  ///
  /// To be notified when the widget gains or loses the focus, add a listener
  /// to the [focusNode]:
  ///
  /// ```dart
  /// focusNode.addListener(() { print(myFocusNode.hasFocus); });
  /// ```
411 412
  ///
  /// If null, this widget will create its own [FocusNode].
413 414 415
  ///
  /// ## Keyboard
  ///
Gary Qian's avatar
Gary Qian committed
416
  /// Requesting the focus will typically cause the keyboard to be shown
417 418
  /// if it's not showing already.
  ///
419
  /// On Android, the user can hide the keyboard - without changing the focus -
420 421 422 423 424 425 426 427 428
  /// with the system back button. They can restore the keyboard's visibility
  /// by tapping on a text field.  The user might hide the keyboard and
  /// switch to a physical keyboard, or they might just need to get it
  /// out of the way for a moment, to expose something it's
  /// obscuring. In this case requesting the focus again will not
  /// cause the focus to change, and will not make the keyboard visible.
  ///
  /// This widget builds an [EditableText] and will ensure that the keyboard is
  /// showing when it is tapped by calling [EditableTextState.requestKeyboard()].
429 430 431 432
  final FocusNode focusNode;

  /// The decoration to show around the text field.
  ///
433
  /// By default, draws a horizontal line under the text field but can be
434 435
  /// configured to show an icon, label, hint text, and error text.
  ///
436
  /// Specify null to remove the decoration entirely (including the
437 438 439
  /// extra padding introduced by the decoration to save space for the labels).
  final InputDecoration decoration;

440
  /// {@macro flutter.widgets.editableText.keyboardType}
441 442
  final TextInputType keyboardType;

443 444
  /// The type of action button to use for the keyboard.
  ///
445 446
  /// Defaults to [TextInputAction.newline] if [keyboardType] is
  /// [TextInputType.multiline] and [TextInputAction.done] otherwise.
447 448
  final TextInputAction textInputAction;

449
  /// {@macro flutter.widgets.editableText.textCapitalization}
450 451
  final TextCapitalization textCapitalization;

452 453 454 455
  /// The style to use for the text being edited.
  ///
  /// This text style is also used as the base style for the [decoration].
  ///
456
  /// If null, defaults to the `subhead` text style from the current [Theme].
457 458
  final TextStyle style;

459 460 461
  /// {@macro flutter.widgets.editableText.strutStyle}
  final StrutStyle strutStyle;

462
  /// {@macro flutter.widgets.editableText.textAlign}
463 464
  final TextAlign textAlign;

465 466 467
  /// {@macro flutter.material.inputDecorator.textAlignVertical}
  final TextAlignVertical textAlignVertical;

468 469 470
  /// {@macro flutter.widgets.editableText.textDirection}
  final TextDirection textDirection;

471
  /// {@macro flutter.widgets.editableText.autofocus}
472 473
  final bool autofocus;

474
  /// {@macro flutter.widgets.editableText.obscureText}
475 476
  final bool obscureText;

477
  /// {@macro flutter.widgets.editableText.autocorrect}
478 479
  final bool autocorrect;

480
  /// {@macro flutter.widgets.editableText.maxLines}
481 482
  final int maxLines;

483 484 485 486 487 488
  /// {@macro flutter.widgets.editableText.minLines}
  final int minLines;

  /// {@macro flutter.widgets.editableText.expands}
  final bool expands;

489 490 491
  /// {@macro flutter.widgets.editableText.readOnly}
  final bool readOnly;

492 493 494 495 496 497 498
  /// Configuration of toolbar options.
  ///
  /// If not set, select all and paste will default to be enabled. Copy and cut
  /// will be disabled if [obscureText] is true. If [readOnly] is true,
  /// paste and cut will be disabled regardless.
  final ToolbarOptions toolbarOptions;

499 500 501
  /// {@macro flutter.widgets.editableText.showCursor}
  final bool showCursor;

502 503
  /// If [maxLength] is set to this value, only the "current input length"
  /// part of the character counter is shown.
504
  static const int noMaxLength = -1;
505

506 507 508 509
  /// The maximum number of characters (Unicode scalar values) to allow in the
  /// text field.
  ///
  /// If set, a character counter will be displayed below the
510
  /// field showing how many characters have been entered. If set to a number
511
  /// greater than 0, it will also display the maximum number allowed. If set
512 513 514
  /// to [TextField.noMaxLength] then only the current character count is displayed.
  ///
  /// After [maxLength] characters have been input, additional input
515
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
516 517 518
  /// enforces the length with a [LengthLimitingTextInputFormatter], which is
  /// evaluated after the supplied [inputFormatters], if any.
  ///
519 520 521 522
  /// This value must be either null, [TextField.noMaxLength], or greater than 0.
  /// If null (the default) then there is no limit to the number of characters
  /// that can be entered. If set to [TextField.noMaxLength], then no limit will
  /// be enforced, but the number of characters entered will still be displayed.
523 524 525 526 527 528 529 530
  ///
  /// 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.
  ///
531 532
  /// ## Limitations
  ///
533
  /// The text field does not currently count Unicode grapheme clusters (i.e.
534 535 536 537 538 539 540 541 542
  /// 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
543
  /// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will
544 545 546 547 548 549 550 551 552 553
  /// 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:
554
  ///
555 556 557 558 559 560 561 562 563 564 565 566
  ///  * [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;

567
  /// {@macro flutter.widgets.editableText.onChanged}
568 569 570 571 572 573 574
  ///
  /// See also:
  ///
  ///  * [inputFormatters], which are called before [onChanged]
  ///    runs and can validate and change ("format") the input value.
  ///  * [onEditingComplete], [onSubmitted], [onSelectionChanged]:
  ///    which are more specialized input change notifications.
575 576
  final ValueChanged<String> onChanged;

577
  /// {@macro flutter.widgets.editableText.onEditingComplete}
578 579
  final VoidCallback onEditingComplete;

580
  /// {@macro flutter.widgets.editableText.onSubmitted}
581 582 583 584 585 586
  ///
  /// See also:
  ///
  ///  * [EditableText.onSubmitted] for an example of how to handle moving to
  ///    the next/previous field when using [TextInputAction.next] and
  ///    [TextInputAction.previous] for [textInputAction].
587 588
  final ValueChanged<String> onSubmitted;

589
  /// {@macro flutter.widgets.editableText.inputFormatters}
590 591
  final List<TextInputFormatter> inputFormatters;

592
  /// If false the text field is "disabled": it ignores taps and its
593 594 595 596 597 598
  /// [decoration] is rendered in grey.
  ///
  /// If non-null this property overrides the [decoration]'s
  /// [Decoration.enabled] property.
  final bool enabled;

599
  /// {@macro flutter.widgets.editableText.cursorWidth}
600 601
  final double cursorWidth;

602
  /// {@macro flutter.widgets.editableText.cursorRadius}
603 604 605
  final Radius cursorRadius;

  /// The color to use when painting the cursor.
606
  ///
607 608
  /// Defaults to [ThemeData.cursorColor] or [CupertinoTheme.primaryColor]
  /// depending on [ThemeData.platform].
609 610
  final Color cursorColor;

611
  /// The appearance of the keyboard.
612
  ///
613
  /// This setting is only honored on iOS devices.
614
  ///
615 616 617
  /// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
  final Brightness keyboardAppearance;

618
  /// {@macro flutter.widgets.editableText.scrollPadding}
619 620
  final EdgeInsets scrollPadding;

621 622 623
  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
  final bool enableInteractiveSelection;

624 625 626
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

627
  /// {@macro flutter.rendering.editable.selectionEnabled}
628
  bool get selectionEnabled => enableInteractiveSelection;
629

630 631
  /// {@template flutter.material.textfield.onTap}
  /// Called for each distinct tap except for every second tap of a double tap.
632
  ///
633
  /// The text field builds a [GestureDetector] to handle input events like tap,
634
  /// to trigger focus requests, to move the caret, adjust the selection, etc.
635
  /// Handling some of those events by wrapping the text field with a competing
636 637
  /// GestureDetector is problematic.
  ///
638
  /// To unconditionally handle taps, without interfering with the text field's
639 640
  /// internal gesture detector, provide this callback.
  ///
641
  /// If the text field is created with [enabled] false, taps will not be
642 643
  /// recognized.
  ///
644
  /// To be notified when the text field gains or loses the focus, provide a
645 646 647
  /// [focusNode] and add a listener to that.
  ///
  /// To listen to arbitrary pointer events without competing with the
648
  /// text field's internal gesture detector, use a [Listener].
649
  /// {@endtemplate}
650 651
  final GestureTapCallback onTap;

652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680
  /// Callback that generates a custom [InputDecorator.counter] widget.
  ///
  /// See [InputCounterWidgetBuilder] for an explanation of the passed in
  /// arguments.  The returned widget will be placed below the line in place of
  /// the default widget built when [counterText] is specified.
  ///
  /// The returned widget will be wrapped in a [Semantics] widget for
  /// accessibility, but it also needs to be accessible itself.  For example,
  /// if returning a Text widget, set the [semanticsLabel] property.
  ///
  /// {@tool sample}
  /// ```dart
  /// Widget counter(
  ///   BuildContext context,
  ///   {
  ///     int currentLength,
  ///     int maxLength,
  ///     bool isFocused,
  ///   }
  /// ) {
  ///   return Text(
  ///     '$currentLength of $maxLength characters',
  ///     semanticsLabel: 'character count',
  ///   );
  /// }
  /// ```
  /// {@end-tool}
  final InputCounterWidgetBuilder buildCounter;

681 682 683
  /// {@macro flutter.widgets.edtiableText.scrollPhysics}
  final ScrollPhysics scrollPhysics;

684 685 686
  /// {@macro flutter.widgets.editableText.scrollController}
  final ScrollController scrollController;

687
  @override
688
  _TextFieldState createState() => _TextFieldState();
689 690

  @override
691 692
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
693 694
    properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
695
    properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
696
    properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration()));
697 698 699 700
    properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text));
    properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
    properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
701
    properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
702
    properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
703 704
    properties.add(IntProperty('minLines', minLines, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
705
    properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
706 707 708 709
    properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced'));
    properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
    properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none));
    properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
710
    properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
711 712 713
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
    properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
714
    properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
715 716 717
    properties.add(DiagnosticsProperty<Brightness>('keyboardAppearance', keyboardAppearance, defaultValue: null));
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('scrollPadding', scrollPadding, defaultValue: const EdgeInsets.all(20.0)));
    properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
718
    properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
719
    properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
720 721 722
  }
}

723
class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
724 725 726
  Set<InteractiveInkFeature> _splashes;
  InteractiveInkFeature _currentSplash;

727
  TextEditingController _controller;
728
  TextEditingController get _effectiveController => widget.controller ?? _controller;
729 730

  FocusNode _focusNode;
731
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
732

733 734
  bool _isHovering = false;

735 736 737 738
  bool get needsCounter => widget.maxLength != null
    && widget.decoration != null
    && widget.decoration.counterText == null;

739 740
  bool _showSelectionHandles = false;

741 742 743 744 745 746 747 748 749 750 751 752 753
  _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;

  // API for TextSelectionGestureDetectorBuilderDelegate.
  @override
  bool forcePressEnabled;

  @override
  final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();

  @override
  bool get selectionEnabled => widget.selectionEnabled;
  // End of API for TextSelectionGestureDetectorBuilderDelegate.

754 755
  bool get _isEnabled =>  widget.enabled ?? widget.decoration?.enabled ?? true;

756 757
  int get _currentLength => _effectiveController.value.text.runes.length;

758
  InputDecoration _getEffectiveDecoration() {
759
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
760
    final ThemeData themeData = Theme.of(context);
761
    final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
762
      .applyDefaults(themeData.inputDecorationTheme)
763 764
      .copyWith(
        enabled: widget.enabled,
765
        hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
766
      );
767

768 769
    // No need to build anything if counter or counterText were given directly.
    if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null)
770
      return effectiveDecoration;
771

772 773
    // If buildCounter was provided, use it to generate a counter widget.
    Widget counter;
774
    final int currentLength = _currentLength;
775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
    if (effectiveDecoration.counter == null
        && effectiveDecoration.counterText == null
        && widget.buildCounter != null) {
      final bool isFocused = _effectiveFocusNode.hasFocus;
      counter = Semantics(
        container: true,
        liveRegion: isFocused,
        child: widget.buildCounter(
          context,
          currentLength: currentLength,
          maxLength: widget.maxLength,
          isFocused: isFocused,
        ),
      );
      return effectiveDecoration.copyWith(counter: counter);
    }

    if (widget.maxLength == null)
      return effectiveDecoration; // No counter widget

795 796 797
    String counterText = '$currentLength';
    String semanticCounterText = '';

798 799 800
    // Handle a real maxLength (positive number)
    if (widget.maxLength > 0) {
      // Show the maxLength in the counter
801 802 803 804
      counterText += '/${widget.maxLength}';
      final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength);
      semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);

805 806 807 808 809 810 811 812 813 814
      // Handle length exceeds maxLength
      if (_effectiveController.value.text.runes.length > widget.maxLength) {
        return effectiveDecoration.copyWith(
          errorText: effectiveDecoration.errorText ?? '',
          counterStyle: effectiveDecoration.errorStyle
            ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor),
          counterText: counterText,
          semanticCounterText: semanticCounterText,
        );
      }
815
    }
816

817 818 819 820
    return effectiveDecoration.copyWith(
      counterText: counterText,
      semanticCounterText: semanticCounterText,
    );
821 822
  }

823 824 825
  @override
  void initState() {
    super.initState();
826
    _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
827
    if (widget.controller == null) {
828
      _controller = TextEditingController();
829 830
    }
    _effectiveFocusNode.canRequestFocus = _isEnabled;
831 832 833
  }

  @override
834
  void didUpdateWidget(TextField oldWidget) {
835
    super.didUpdateWidget(oldWidget);
836
    if (widget.controller == null && oldWidget.controller != null)
837
      _controller = TextEditingController.fromValue(oldWidget.controller.value);
838
    else if (widget.controller != null && oldWidget.controller == null)
839
      _controller = null;
840
    _effectiveFocusNode.canRequestFocus = _isEnabled;
841 842 843 844 845
    if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly) {
      if(_effectiveController.selection.isCollapsed) {
        _showSelectionHandles = !widget.readOnly;
      }
    }
846 847 848 849 850 851 852 853
  }

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

854
  EditableTextState get _editableText => editableTextKey.currentState;
855

856
  void _requestKeyboard() {
857 858 859 860 861 862
    _editableText?.requestKeyboard();
  }

  bool _shouldShowSelectionHandles(SelectionChangedCause cause) {
    // When the text field is activated by something that doesn't trigger the
    // selection overlay, we shouldn't show the handles either.
863
    if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
864 865 866 867 868
      return false;

    if (cause == SelectionChangedCause.keyboard)
      return false;

869 870 871
    if (widget.readOnly && _effectiveController.selection.isCollapsed)
      return false;

872 873 874 875 876 877 878
    if (cause == SelectionChangedCause.longPress)
      return true;

    if (_effectiveController.text.isNotEmpty)
      return true;

    return false;
879 880
  }

881
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
882 883 884 885 886
    final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
    if (willShowSelectionHandles != _showSelectionHandles) {
      setState(() {
        _showSelectionHandles = willShowSelectionHandles;
      });
887 888
    }

889 890 891
    switch (Theme.of(context).platform) {
      case TargetPlatform.iOS:
        if (cause == SelectionChangedCause.longPress) {
892
          _editableText?.bringIntoView(selection.base);
893 894 895 896 897 898 899 900
        }
        return;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        // Do nothing.
    }
  }

901 902 903 904 905 906 907
  /// Toggle the toolbar when a selection handle is tapped.
  void _handleSelectionHandleTapped() {
    if (_effectiveController.selection.isCollapsed) {
      _editableText.toggleToolbar();
    }
  }

908
  InteractiveInkFeature _createInkFeature(Offset globalPosition) {
909
    final MaterialInkController inkController = Material.of(context);
910
    final ThemeData themeData = Theme.of(context);
911
    final BuildContext editableContext = editableTextKey.currentContext;
Hans Muller's avatar
Hans Muller committed
912
    final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject();
913
    final Offset position = referenceBox.globalToLocal(globalPosition);
914
    final Color color = themeData.splashColor;
915 916 917 918 919 920 921 922 923 924 925 926

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

927
    splash = themeData.splashFactory.create(
928 929 930 931 932 933 934 935
      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,
936
      textDirection: Directionality.of(context),
937 938 939 940 941
    );

    return splash;
  }

942
  void _startSplash(Offset globalPosition) {
943 944
    if (_effectiveFocusNode.hasFocus)
      return;
945
    final InteractiveInkFeature splash = _createInkFeature(globalPosition);
946
    _splashes ??= HashSet<InteractiveInkFeature>();
947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976
    _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();
  }

977 978
  void _handleMouseEnter(PointerEnterEvent event) => _handleHover(true);
  void _handleMouseExit(PointerExitEvent event) => _handleHover(false);
979 980 981 982 983 984 985 986 987

  void _handleHover(bool hovering) {
    if (hovering != _isHovering) {
      setState(() {
        return _isHovering = hovering;
      });
    }
  }

988 989
  @override
  Widget build(BuildContext context) {
990
    super.build(context); // See AutomaticKeepAliveClientMixin.
991
    assert(debugCheckHasMaterial(context));
992 993
    // TODO(jonahwilliams): uncomment out this check once we have migrated tests.
    // assert(debugCheckHasMaterialLocalizations(context));
994
    assert(debugCheckHasDirectionality(context));
995 996
    assert(
      !(widget.style != null && widget.style.inherit == false &&
997
        (widget.style.fontSize == null || widget.style.textBaseline == null)),
998 999 1000
      'inherit false style must supply fontSize and textBaseline',
    );

1001
    final ThemeData themeData = Theme.of(context);
1002
    final TextStyle style = themeData.textTheme.subhead.merge(widget.style);
1003
    final Brightness keyboardAppearance = widget.keyboardAppearance ?? themeData.primaryColorBrightness;
1004 1005
    final TextEditingController controller = _effectiveController;
    final FocusNode focusNode = _effectiveFocusNode;
1006 1007
    final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
    if (widget.maxLength != null && widget.maxLengthEnforced)
1008
      formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
1009

1010
    TextSelectionControls textSelectionControls;
1011 1012 1013 1014 1015 1016
    bool paintCursorAboveText;
    bool cursorOpacityAnimates;
    Offset cursorOffset;
    Color cursorColor = widget.cursorColor;
    Radius cursorRadius = widget.cursorRadius;

1017 1018 1019 1020
    switch (themeData.platform) {
      case TargetPlatform.iOS:
        forcePressEnabled = true;
        textSelectionControls = cupertinoTextSelectionControls;
1021 1022 1023 1024
        paintCursorAboveText = true;
        cursorOpacityAnimates = true;
        cursorColor ??= CupertinoTheme.of(context).primaryColor;
        cursorRadius ??= const Radius.circular(2.0);
1025
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
1026
        break;
1027

1028 1029 1030 1031
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        forcePressEnabled = false;
        textSelectionControls = materialTextSelectionControls;
1032 1033 1034
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
        cursorColor ??= themeData.cursorColor;
1035 1036 1037
        break;
    }

1038 1039
    Widget child = RepaintBoundary(
      child: EditableText(
1040
        key: editableTextKey,
1041
        readOnly: widget.readOnly,
1042
        toolbarOptions: widget.toolbarOptions,
1043 1044
        showCursor: widget.showCursor,
        showSelectionHandles: _showSelectionHandles,
1045 1046
        controller: controller,
        focusNode: focusNode,
1047
        keyboardType: widget.keyboardType,
1048
        textInputAction: widget.textInputAction,
1049
        textCapitalization: widget.textCapitalization,
1050
        style: style,
1051
        strutStyle: widget.strutStyle,
1052
        textAlign: widget.textAlign,
1053
        textDirection: widget.textDirection,
1054 1055
        autofocus: widget.autofocus,
        obscureText: widget.obscureText,
1056
        autocorrect: widget.autocorrect,
1057
        maxLines: widget.maxLines,
1058 1059
        minLines: widget.minLines,
        expands: widget.expands,
1060
        selectionColor: themeData.textSelectionColor,
1061
        selectionControls: widget.selectionEnabled ? textSelectionControls : null,
1062
        onChanged: widget.onChanged,
1063
        onSelectionChanged: _handleSelectionChanged,
1064
        onEditingComplete: widget.onEditingComplete,
1065
        onSubmitted: widget.onSubmitted,
1066
        onSelectionHandleTapped: _handleSelectionHandleTapped,
1067
        inputFormatters: formatters,
1068
        rendererIgnoresPointer: true,
1069
        cursorWidth: widget.cursorWidth,
1070 1071 1072 1073 1074
        cursorRadius: cursorRadius,
        cursorColor: cursorColor,
        cursorOpacityAnimates: cursorOpacityAnimates,
        cursorOffset: cursorOffset,
        paintCursorAboveText: paintCursorAboveText,
1075
        backgroundCursorColor: CupertinoColors.inactiveGray,
1076
        scrollPadding: widget.scrollPadding,
1077
        keyboardAppearance: keyboardAppearance,
1078
        enableInteractiveSelection: widget.enableInteractiveSelection,
1079
        dragStartBehavior: widget.dragStartBehavior,
1080
        scrollController: widget.scrollController,
1081
        scrollPhysics: widget.scrollPhysics,
1082 1083 1084
      ),
    );

1085
    if (widget.decoration != null) {
1086 1087
      child = AnimatedBuilder(
        animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
1088
        builder: (BuildContext context, Widget child) {
1089
          return InputDecorator(
1090
            decoration: _getEffectiveDecoration(),
1091 1092
            baseStyle: widget.style,
            textAlign: widget.textAlign,
1093
            textAlignVertical: widget.textAlignVertical,
1094
            isHovering: _isHovering,
1095 1096
            isFocused: focusNode.hasFocus,
            isEmpty: controller.value.text.isEmpty,
1097
            expands: widget.expands,
1098 1099 1100 1101 1102 1103
            child: child,
          );
        },
        child: child,
      );
    }
1104 1105
    return IgnorePointer(
      ignoring: !_isEnabled,
1106 1107 1108
      child: MouseRegion(
        onEnter: _handleMouseEnter,
        onExit: _handleMouseExit,
1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124
        child: AnimatedBuilder(
          animation: controller, // changes the _currentLength
          builder: (BuildContext context, Widget child) {
            return Semantics(
              maxValueLength: widget.maxLengthEnforced && widget.maxLength != null && widget.maxLength > 0
                  ? widget.maxLength
                  : null,
              currentValueLength: _currentLength,
              onTap: () {
                if (!_effectiveController.selection.isValid)
                  _effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
                _requestKeyboard();
              },
              child: child,
            );
          },
1125
          child: _selectionGestureDetectorBuilder.buildGestureDetector(
1126 1127 1128
            behavior: HitTestBehavior.translucent,
            child: child,
          ),
1129
        ),
1130
      ),
1131 1132
    );
  }
1133
}