text_field.dart 36 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 21 22
import 'text_selection.dart';
import 'theme.dart';

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

25 26 27
/// Signature for the [TextField.buildCounter] callback.
typedef InputCounterWidgetBuilder = Widget Function(
  /// The build context for the TextField
28 29 30 31 32 33 34 35 36
  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,
});
37

38
/// A material design text field.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
///
/// 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
61 62 63
/// 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].
64 65 66 67
///
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
/// {@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.
///
/// ```dart
/// TextField(
///   obscureText: true,
///   decoration: InputDecoration(
///     border: OutlineInputBorder(),
///     labelText: 'Password',
///   ),
/// )
/// ```
/// {@end-tool}
///
84 85
/// See also:
///
86
///  * <https://material.io/design/components/text-fields.html>
87 88 89 90
///  * [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
91
///    [TextField]. The [EditableText] widget is rarely used directly unless
92
///    you are implementing an entirely different design language, such as
93
///    Cupertino.
94 95 96 97 98 99 100 101 102
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.
103 104
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
105
  /// the number of lines. By default, it is one, meaning this is a single-line
106
  /// text field. [maxLines] must not be zero.
107 108 109
  ///
  /// The [maxLength] property is set to null by default, which means the
  /// number of characters allowed in the text field is not restricted. If
110 111 112 113 114
  /// [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.
115 116
  ///
  /// After [maxLength] characters have been input, additional input
117
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
118 119 120 121 122 123 124
  /// 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.
125
  ///
126
  /// The [textAlign], [autofocus], [obscureText], [autocorrect],
127 128
  /// [maxLengthEnforced], [scrollPadding], [maxLines], and [maxLength]
  /// arguments must not be null.
129 130 131 132 133
  ///
  /// See also:
  ///
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
134
  const TextField({
135 136 137
    Key key,
    this.controller,
    this.focusNode,
138
    this.decoration = const InputDecoration(),
139 140
    TextInputType keyboardType,
    this.textInputAction,
141
    this.textCapitalization = TextCapitalization.none,
142
    this.style,
143
    this.strutStyle,
144
    this.textAlign = TextAlign.start,
145
    this.textDirection,
146 147 148 149
    this.autofocus = false,
    this.obscureText = false,
    this.autocorrect = true,
    this.maxLines = 1,
150 151
    this.minLines,
    this.expands = false,
152
    this.maxLength,
153
    this.maxLengthEnforced = true,
154
    this.onChanged,
155
    this.onEditingComplete,
156
    this.onSubmitted,
157
    this.inputFormatters,
158
    this.enabled,
159 160 161
    this.cursorWidth = 2.0,
    this.cursorRadius,
    this.cursorColor,
162
    this.keyboardAppearance,
163
    this.scrollPadding = const EdgeInsets.all(20.0),
164
    this.dragStartBehavior = DragStartBehavior.start,
165
    this.enableInteractiveSelection,
166
    this.onTap,
167
    this.buildCounter,
168
  }) : assert(textAlign != null),
169 170
       assert(autofocus != null),
       assert(obscureText != null),
171
       assert(autocorrect != null),
172
       assert(maxLengthEnforced != null),
173
       assert(scrollPadding != null),
174
       assert(dragStartBehavior != null),
175
       assert(maxLines == null || maxLines > 0),
176 177 178 179 180 181 182 183 184 185
       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.',
       ),
186
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
187
       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
188
       super(key: key);
189 190 191

  /// Controls the text being edited.
  ///
192
  /// If null, this widget will create its own [TextEditingController].
193 194
  final TextEditingController controller;

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
  /// 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); });
  /// ```
215 216
  ///
  /// If null, this widget will create its own [FocusNode].
217 218 219
  ///
  /// ## Keyboard
  ///
Gary Qian's avatar
Gary Qian committed
220
  /// Requesting the focus will typically cause the keyboard to be shown
221 222
  /// if it's not showing already.
  ///
223
  /// On Android, the user can hide the keyboard - without changing the focus -
224 225 226 227 228 229 230 231 232
  /// 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()].
233 234 235 236
  final FocusNode focusNode;

  /// The decoration to show around the text field.
  ///
237
  /// By default, draws a horizontal line under the text field but can be
238 239
  /// configured to show an icon, label, hint text, and error text.
  ///
240
  /// Specify null to remove the decoration entirely (including the
241 242 243
  /// extra padding introduced by the decoration to save space for the labels).
  final InputDecoration decoration;

244
  /// {@macro flutter.widgets.editableText.keyboardType}
245 246
  final TextInputType keyboardType;

247 248
  /// The type of action button to use for the keyboard.
  ///
249 250
  /// Defaults to [TextInputAction.newline] if [keyboardType] is
  /// [TextInputType.multiline] and [TextInputAction.done] otherwise.
251 252
  final TextInputAction textInputAction;

253
  /// {@macro flutter.widgets.editableText.textCapitalization}
254 255
  final TextCapitalization textCapitalization;

256 257 258 259
  /// The style to use for the text being edited.
  ///
  /// This text style is also used as the base style for the [decoration].
  ///
260
  /// If null, defaults to the `subhead` text style from the current [Theme].
261 262
  final TextStyle style;

263 264 265
  /// {@macro flutter.widgets.editableText.strutStyle}
  final StrutStyle strutStyle;

266
  /// {@macro flutter.widgets.editableText.textAlign}
267 268
  final TextAlign textAlign;

269 270 271
  /// {@macro flutter.widgets.editableText.textDirection}
  final TextDirection textDirection;

272
  /// {@macro flutter.widgets.editableText.autofocus}
273 274
  final bool autofocus;

275
  /// {@macro flutter.widgets.editableText.obscureText}
276 277
  final bool obscureText;

278
  /// {@macro flutter.widgets.editableText.autocorrect}
279 280
  final bool autocorrect;

281
  /// {@macro flutter.widgets.editableText.maxLines}
282 283
  final int maxLines;

284 285 286 287 288 289
  /// {@macro flutter.widgets.editableText.minLines}
  final int minLines;

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

290 291
  /// If [maxLength] is set to this value, only the "current input length"
  /// part of the character counter is shown.
292
  static const int noMaxLength = -1;
293

294 295 296 297
  /// The maximum number of characters (Unicode scalar values) to allow in the
  /// text field.
  ///
  /// If set, a character counter will be displayed below the
298
  /// field showing how many characters have been entered. If set to a number
299
  /// greater than 0, it will also display the maximum number allowed. If set
300 301 302
  /// to [TextField.noMaxLength] then only the current character count is displayed.
  ///
  /// After [maxLength] characters have been input, additional input
303
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
304 305 306
  /// enforces the length with a [LengthLimitingTextInputFormatter], which is
  /// evaluated after the supplied [inputFormatters], if any.
  ///
307 308 309 310
  /// 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.
311 312 313 314 315 316 317 318
  ///
  /// 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.
  ///
319 320
  /// ## Limitations
  ///
321
  /// The text field does not currently count Unicode grapheme clusters (i.e.
322 323 324 325 326 327 328 329 330
  /// 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
331
  /// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will
332 333 334 335 336 337 338 339 340 341
  /// 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:
342
  ///
343 344 345 346 347 348 349 350 351 352 353 354
  ///  * [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;

355
  /// {@macro flutter.widgets.editableText.onChanged}
356 357 358 359 360 361 362
  ///
  /// 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.
363 364
  final ValueChanged<String> onChanged;

365
  /// {@macro flutter.widgets.editableText.onEditingComplete}
366 367
  final VoidCallback onEditingComplete;

368
  /// {@macro flutter.widgets.editableText.onSubmitted}
369 370
  final ValueChanged<String> onSubmitted;

371
  /// {@macro flutter.widgets.editableText.inputFormatters}
372 373
  final List<TextInputFormatter> inputFormatters;

374
  /// If false the text field is "disabled": it ignores taps and its
375 376 377 378 379 380
  /// [decoration] is rendered in grey.
  ///
  /// If non-null this property overrides the [decoration]'s
  /// [Decoration.enabled] property.
  final bool enabled;

381
  /// {@macro flutter.widgets.editableText.cursorWidth}
382 383
  final double cursorWidth;

384
  /// {@macro flutter.widgets.editableText.cursorRadius}
385 386 387
  final Radius cursorRadius;

  /// The color to use when painting the cursor.
388 389
  ///
  /// Defaults to the theme's `cursorColor` when null.
390 391
  final Color cursorColor;

392
  /// The appearance of the keyboard.
393
  ///
394
  /// This setting is only honored on iOS devices.
395
  ///
396 397 398
  /// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
  final Brightness keyboardAppearance;

399
  /// {@macro flutter.widgets.editableText.scrollPadding}
400 401
  final EdgeInsets scrollPadding;

402 403 404
  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
  final bool enableInteractiveSelection;

405 406 407
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

408 409 410 411 412
  /// {@macro flutter.rendering.editable.selectionEnabled}
  bool get selectionEnabled {
    return enableInteractiveSelection ?? !obscureText;
  }

413
  /// Called when the user taps on this text field.
414
  ///
415
  /// The text field builds a [GestureDetector] to handle input events like tap,
416
  /// to trigger focus requests, to move the caret, adjust the selection, etc.
417
  /// Handling some of those events by wrapping the text field with a competing
418 419
  /// GestureDetector is problematic.
  ///
420
  /// To unconditionally handle taps, without interfering with the text field's
421 422
  /// internal gesture detector, provide this callback.
  ///
423
  /// If the text field is created with [enabled] false, taps will not be
424 425
  /// recognized.
  ///
426
  /// To be notified when the text field gains or loses the focus, provide a
427 428 429
  /// [focusNode] and add a listener to that.
  ///
  /// To listen to arbitrary pointer events without competing with the
430
  /// text field's internal gesture detector, use a [Listener].
431 432
  final GestureTapCallback onTap;

433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
  /// 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;

462
  @override
463
  _TextFieldState createState() => _TextFieldState();
464 465

  @override
466 467
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
468 469
    properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
470
    properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
471
    properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration()));
472 473 474 475
    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));
476
    properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
477
    properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
478 479
    properties.add(IntProperty('minLines', minLines, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
480
    properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
481 482 483 484 485 486 487 488 489 490 491
    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));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
    properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
    properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null));
    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'));
492 493 494
  }
}

495
class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin {
496
  final GlobalKey<EditableTextState> _editableTextKey = GlobalKey<EditableTextState>();
497

498 499 500
  Set<InteractiveInkFeature> _splashes;
  InteractiveInkFeature _currentSplash;

501
  TextEditingController _controller;
502
  TextEditingController get _effectiveController => widget.controller ?? _controller;
503 504

  FocusNode _focusNode;
505
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
506

507 508 509 510 511
  bool get needsCounter => widget.maxLength != null
    && widget.decoration != null
    && widget.decoration.counterText == null;

  InputDecoration _getEffectiveDecoration() {
512
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
513
    final ThemeData themeData = Theme.of(context);
514
    final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
515
      .applyDefaults(themeData.inputDecorationTheme)
516 517
      .copyWith(
        enabled: widget.enabled,
518
        hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
519
      );
520

521 522
    // No need to build anything if counter or counterText were given directly.
    if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null)
523
      return effectiveDecoration;
524

525 526
    // If buildCounter was provided, use it to generate a counter widget.
    Widget counter;
527
    final int currentLength = _effectiveController.value.text.runes.length;
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
    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

548 549 550
    String counterText = '$currentLength';
    String semanticCounterText = '';

551 552 553
    // Handle a real maxLength (positive number)
    if (widget.maxLength > 0) {
      // Show the maxLength in the counter
554 555 556 557
      counterText += '/${widget.maxLength}';
      final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength);
      semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);

558 559 560 561 562 563 564 565 566 567
      // 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,
        );
      }
568
    }
569

570 571 572 573
    return effectiveDecoration.copyWith(
      counterText: counterText,
      semanticCounterText: semanticCounterText,
    );
574 575
  }

576 577 578
  @override
  void initState() {
    super.initState();
579
    if (widget.controller == null)
580
      _controller = TextEditingController();
581 582 583
  }

  @override
584
  void didUpdateWidget(TextField oldWidget) {
585
    super.didUpdateWidget(oldWidget);
586
    if (widget.controller == null && oldWidget.controller != null)
587
      _controller = TextEditingController.fromValue(oldWidget.controller.value);
588
    else if (widget.controller != null && oldWidget.controller == null)
589
      _controller = null;
590 591 592 593 594
    final bool isEnabled = widget.enabled ?? widget.decoration?.enabled ?? true;
    final bool wasEnabled = oldWidget.enabled ?? oldWidget.decoration?.enabled ?? true;
    if (wasEnabled && !isEnabled) {
      _effectiveFocusNode.unfocus();
    }
595 596 597 598 599 600 601 602 603 604 605 606
  }

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

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

607 608 609 610 611 612 613 614 615 616 617 618 619 620 621
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
    // iOS cursor doesn't move via a selection handle. The scroll happens
    // directly from new text selection changes.
    switch (Theme.of(context).platform) {
      case TargetPlatform.iOS:
        if (cause == SelectionChangedCause.longPress) {
          _editableTextKey.currentState?.bringIntoView(selection.base);
        }
        return;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        // Do nothing.
    }
  }

622
  InteractiveInkFeature _createInkFeature(Offset globalPosition) {
623
    final MaterialInkController inkController = Material.of(context);
624
    final ThemeData themeData = Theme.of(context);
Hans Muller's avatar
Hans Muller committed
625 626
    final BuildContext editableContext = _editableTextKey.currentContext;
    final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject();
627
    final Offset position = referenceBox.globalToLocal(globalPosition);
628
    final Color color = themeData.splashColor;
629 630 631 632 633 634 635 636 637 638 639 640

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

641
    splash = themeData.splashFactory.create(
642 643 644 645 646 647 648 649
      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,
650
      textDirection: Directionality.of(context),
651 652 653 654 655 656 657 658 659
    );

    return splash;
  }

  RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable;

  void _handleTapDown(TapDownDetails details) {
    _renderEditable.handleTapDown(details);
660
    _startSplash(details.globalPosition);
661 662
  }

663 664
  void _handleForcePressStarted(ForcePressDetails details) {
    if (widget.selectionEnabled) {
665 666 667 668 669
      _renderEditable.selectWordsInRange(
        from: details.globalPosition,
        cause: SelectionChangedCause.forcePress,
      );
      _editableTextKey.currentState.showToolbar();
670 671 672
    }
  }

673 674 675 676 677 678 679 680 681 682 683 684
  void _handleSingleTapUp(TapUpDetails details) {
    if (widget.selectionEnabled) {
      switch (Theme.of(context).platform) {
        case TargetPlatform.iOS:
          _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          _renderEditable.selectPosition(cause: SelectionChangedCause.tap);
          break;
      }
    }
685 686
    _requestKeyboard();
    _confirmCurrentSplash();
687 688
    if (widget.onTap != null)
      widget.onTap();
689 690
  }

691
  void _handleSingleTapCancel() {
692 693 694
    _cancelCurrentSplash();
  }

695
  void _handleSingleLongTapStart(LongPressStartDetails details) {
696 697 698
    if (widget.selectionEnabled) {
      switch (Theme.of(context).platform) {
        case TargetPlatform.iOS:
699 700 701 702
          _renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
703 704 705 706
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          _renderEditable.selectWord(cause: SelectionChangedCause.longPress);
707
          Feedback.forLongPress(context);
708 709 710
          break;
      }
    }
711 712 713
    _confirmCurrentSplash();
  }

714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738
  void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
    if (widget.selectionEnabled) {
      switch (Theme.of(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;
      }
    }
  }

  void _handleSingleLongTapEnd(LongPressEndDetails details) {
    _editableTextKey.currentState.showToolbar();
  }

739
  void _handleDoubleTapDown(TapDownDetails details) {
740 741 742 743
    if (widget.selectionEnabled) {
      _renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
      _editableTextKey.currentState.showToolbar();
    }
744 745
  }

746
  void _handleMouseDragSelectionStart(DragStartDetails details) {
747 748 749 750 751 752 753
    _renderEditable.selectPositionAt(
      from: details.globalPosition,
      cause: SelectionChangedCause.drag,
    );
    _startSplash(details.globalPosition);
  }

754
  void _handleMouseDragSelectionUpdate(
755 756 757 758 759 760 761 762 763 764 765
      DragStartDetails startDetails,
      DragUpdateDetails updateDetails,
  ) {
    _renderEditable.selectPositionAt(
      from: startDetails.globalPosition,
      to: updateDetails.globalPosition,
      cause: SelectionChangedCause.drag,
    );
  }

  void _startSplash(Offset globalPosition) {
766 767
    if (_effectiveFocusNode.hasFocus)
      return;
768
    final InteractiveInkFeature splash = _createInkFeature(globalPosition);
769
    _splashes ??= HashSet<InteractiveInkFeature>();
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799
    _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();
  }

800 801
  @override
  Widget build(BuildContext context) {
802
    super.build(context); // See AutomaticKeepAliveClientMixin.
803
    assert(debugCheckHasMaterial(context));
804 805
    // TODO(jonahwilliams): uncomment out this check once we have migrated tests.
    // assert(debugCheckHasMaterialLocalizations(context));
806
    assert(debugCheckHasDirectionality(context));
807 808
    assert(
      !(widget.style != null && widget.style.inherit == false &&
809
        (widget.style.fontSize == null || widget.style.textBaseline == null)),
810 811 812
      'inherit false style must supply fontSize and textBaseline',
    );

813
    final ThemeData themeData = Theme.of(context);
814
    final TextStyle style = themeData.textTheme.subhead.merge(widget.style);
815
    final Brightness keyboardAppearance = widget.keyboardAppearance ?? themeData.primaryColorBrightness;
816 817
    final TextEditingController controller = _effectiveController;
    final FocusNode focusNode = _effectiveFocusNode;
818 819
    final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
    if (widget.maxLength != null && widget.maxLengthEnforced)
820
      formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
821

822 823
    bool forcePressEnabled;
    TextSelectionControls textSelectionControls;
824 825 826 827 828 829
    bool paintCursorAboveText;
    bool cursorOpacityAnimates;
    Offset cursorOffset;
    Color cursorColor = widget.cursorColor;
    Radius cursorRadius = widget.cursorRadius;

830 831 832 833
    switch (themeData.platform) {
      case TargetPlatform.iOS:
        forcePressEnabled = true;
        textSelectionControls = cupertinoTextSelectionControls;
834 835 836 837 838 839 840 841 842 843
        paintCursorAboveText = true;
        cursorOpacityAnimates = true;
        cursorColor ??= CupertinoTheme.of(context).primaryColor;
        cursorRadius ??= const Radius.circular(2.0);
        // An eyeballed value that moves the cursor slightly left of where it is
        // rendered for text on Android so its positioning more accurately matches the
        // native iOS text cursor positioning.
        //
        // This value is in device pixels, not logical pixels as is typically used
        // throughout the codebase.
844
        const int _iOSHorizontalOffset = -2;
845
        cursorOffset = Offset(_iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
846
        break;
847

848 849 850 851
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        forcePressEnabled = false;
        textSelectionControls = materialTextSelectionControls;
852 853 854
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
        cursorColor ??= themeData.cursorColor;
855 856 857
        break;
    }

858 859
    Widget child = RepaintBoundary(
      child: EditableText(
860 861 862
        key: _editableTextKey,
        controller: controller,
        focusNode: focusNode,
863
        keyboardType: widget.keyboardType,
864
        textInputAction: widget.textInputAction,
865
        textCapitalization: widget.textCapitalization,
866
        style: style,
867
        strutStyle: widget.strutStyle,
868
        textAlign: widget.textAlign,
869
        textDirection: widget.textDirection,
870 871
        autofocus: widget.autofocus,
        obscureText: widget.obscureText,
872
        autocorrect: widget.autocorrect,
873
        maxLines: widget.maxLines,
874 875
        minLines: widget.minLines,
        expands: widget.expands,
876
        selectionColor: themeData.textSelectionColor,
877
        selectionControls: widget.selectionEnabled ? textSelectionControls : null,
878
        onChanged: widget.onChanged,
879
        onSelectionChanged: _handleSelectionChanged,
880
        onEditingComplete: widget.onEditingComplete,
881
        onSubmitted: widget.onSubmitted,
882
        inputFormatters: formatters,
883
        rendererIgnoresPointer: true,
884
        cursorWidth: widget.cursorWidth,
885 886 887 888 889
        cursorRadius: cursorRadius,
        cursorColor: cursorColor,
        cursorOpacityAnimates: cursorOpacityAnimates,
        cursorOffset: cursorOffset,
        paintCursorAboveText: paintCursorAboveText,
890
        backgroundCursorColor: CupertinoColors.inactiveGray,
891
        scrollPadding: widget.scrollPadding,
892
        keyboardAppearance: keyboardAppearance,
893
        enableInteractiveSelection: widget.enableInteractiveSelection,
894
        dragStartBehavior: widget.dragStartBehavior,
895 896 897
      ),
    );

898
    if (widget.decoration != null) {
899 900
      child = AnimatedBuilder(
        animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
901
        builder: (BuildContext context, Widget child) {
902
          return InputDecorator(
903
            decoration: _getEffectiveDecoration(),
904 905
            baseStyle: widget.style,
            textAlign: widget.textAlign,
906 907
            isFocused: focusNode.hasFocus,
            isEmpty: controller.value.text.isEmpty,
908
            expands: widget.expands,
909 910 911 912 913 914 915
            child: child,
          );
        },
        child: child,
      );
    }

916
    return Semantics(
917
      onTap: () {
918
        if (!_effectiveController.selection.isValid)
919
          _effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
920 921
        _requestKeyboard();
      },
922
      child: IgnorePointer(
923
        ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
924
        child: TextSelectionGestureDetector(
925
          onTapDown: _handleTapDown,
926
          onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null,
927 928
          onSingleTapUp: _handleSingleTapUp,
          onSingleTapCancel: _handleSingleTapCancel,
929 930 931
          onSingleLongTapStart: _handleSingleLongTapStart,
          onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
          onSingleLongTapEnd: _handleSingleLongTapEnd,
932
          onDoubleTapDown: _handleDoubleTapDown,
933 934
          onDragSelectionStart: _handleMouseDragSelectionStart,
          onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
935
          behavior: HitTestBehavior.translucent,
936 937
          child: child,
        ),
938
      ),
939 940
    );
  }
941
}