Support for disabling TextField, TextFormField (#16027)


diff --git a/examples/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart b/examples/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart
index 3d5b2d4..02a6e67 100644
--- a/examples/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart
+++ b/examples/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart
@@ -138,8 +138,10 @@
             padding: const EdgeInsets.all(16.0),
             children: <Widget>[
               new TextField(
+                enabled: true,
                 decoration: const InputDecoration(
                   labelText: 'Event name',
+                  border: const OutlineInputBorder(),
                 ),
                 style: Theme.of(context).textTheme.display1,
               ),
diff --git a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart
index 6493455..4262bfb 100644
--- a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart
+++ b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart
@@ -249,17 +249,21 @@
                   fieldKey: _passwordFieldKey,
                   helperText: 'No more than 8 characters.',
                   labelText: 'Password *',
-                  onSaved: (String value) { person.password = value; },
+                  onFieldSubmitted: (String value) {
+                    setState(() {
+                      person.password = value;
+                    });
+                  },
                 ),
                 const SizedBox(height: 24.0),
                 new TextFormField(
+                  enabled: person.password != null && person.password.isNotEmpty,
                   decoration: const InputDecoration(
                     border: const UnderlineInputBorder(),
                     filled: true,
                     labelText: 'Re-type password',
                   ),
                   maxLength: 8,
-                  onFieldSubmitted: (String value) { person.password = value; },
                   obscureText: true,
                   validator: _validatePassword,
                 ),
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index 1038c97..358527c 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -1459,6 +1459,9 @@
   }
 
   Color _getDefaultIconColor(ThemeData themeData) {
+    if (!decoration.enabled)
+      return themeData.disabledColor;
+
     switch (themeData.brightness) {
       case Brightness.dark:
         return Colors.white70;
@@ -1478,7 +1481,7 @@
   // i.e. when they appear in place of the empty text field.
   TextStyle _getInlineStyle(ThemeData themeData) {
     return themeData.textTheme.subhead.merge(widget.baseStyle)
-      .copyWith(color: themeData.hintColor);
+      .copyWith(color: decoration.enabled ? themeData.hintColor : themeData.disabledColor);
   }
 
   TextStyle _getFloatingLabelStyle(ThemeData themeData) {
@@ -1487,16 +1490,18 @@
       : _getActiveColor(themeData);
     final TextStyle style = themeData.textTheme.subhead.merge(widget.baseStyle);
     return style
-      .copyWith(color: color)
+      .copyWith(color: decoration.enabled ? color : themeData.disabledColor)
       .merge(decoration.labelStyle);
   }
 
   TextStyle _getHelperStyle(ThemeData themeData) {
-    return themeData.textTheme.caption.copyWith(color: themeData.hintColor).merge(decoration.helperStyle);
+    final Color color = decoration.enabled ? themeData.hintColor : Colors.transparent;
+    return themeData.textTheme.caption.copyWith(color: color).merge(decoration.helperStyle);
   }
 
   TextStyle _getErrorStyle(ThemeData themeData) {
-    return themeData.textTheme.caption.copyWith(color: themeData.errorColor).merge(decoration.errorStyle);
+    final Color color = decoration.enabled ? themeData.errorColor : Colors.transparent;
+    return themeData.textTheme.caption.copyWith(color: color).merge(decoration.errorStyle);
   }
 
   double get _borderWeight {
@@ -1506,6 +1511,11 @@
   }
 
   Color _getBorderColor(ThemeData themeData) {
+    if (!decoration.enabled) {
+      if (decoration.filled == true && !decoration.border.isOutline)
+        return Colors.transparent;
+      return themeData.disabledColor;
+    }
     return decoration.errorText == null
       ? _getActiveColor(themeData)
       : themeData.errorColor;
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index 9d01644..5d37747 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -111,6 +111,7 @@
     this.onChanged,
     this.onSubmitted,
     this.inputFormatters,
+    this.enabled,
   }) : assert(keyboardType != null),
        assert(textAlign != null),
        assert(autofocus != null),
@@ -137,7 +138,7 @@
   /// By default, draws a horizontal line under the text field but can be
   /// configured to show an icon, label, hint text, and error text.
   ///
-  /// Set this field to null to remove the decoration entirely (including the
+  /// Specify null to remove the decoration entirely (including the
   /// extra padding introduced by the decoration to save space for the labels).
   final InputDecoration decoration;
 
@@ -261,6 +262,13 @@
   /// Formatters are run in the provided order when the text input changes.
   final List<TextInputFormatter> inputFormatters;
 
+  /// If false the textfield is "disabled": it ignores taps and its
+  /// [decoration] is rendered in grey.
+  ///
+  /// If non-null this property overrides the [decoration]'s
+  /// [Decoration.enabled] property.
+  final bool enabled;
+
   @override
   _TextFieldState createState() => new _TextFieldState();
 
@@ -299,7 +307,10 @@
 
   InputDecoration _getEffectiveDecoration() {
     final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
-      .applyDefaults(Theme.of(context).inputDecorationTheme);
+      .applyDefaults(Theme.of(context).inputDecorationTheme)
+      .copyWith(
+        enabled: widget.enabled,
+      );
 
     if (!needsCounter)
       return effectiveDecoration;
@@ -495,14 +506,17 @@
           _controller.selection = new TextSelection.collapsed(offset: _controller.text.length);
         _requestKeyboard();
       },
-      child: new GestureDetector(
-        behavior: HitTestBehavior.translucent,
-        onTapDown: _handleTapDown,
-        onTap: _handleTap,
-        onTapCancel: _handleTapCancel,
-        onLongPress: _handleLongPress,
-        excludeFromSemantics: true,
-        child: child,
+      child: new IgnorePointer(
+        ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
+        child: new GestureDetector(
+          behavior: HitTestBehavior.translucent,
+          onTapDown: _handleTapDown,
+          onTap: _handleTap,
+          onTapCancel: _handleTapCancel,
+          onLongPress: _handleLongPress,
+          excludeFromSemantics: true,
+          child: child,
+        ),
       ),
     );
   }
diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart
index f33423d..86de88c 100644
--- a/packages/flutter/lib/src/material/text_form_field.dart
+++ b/packages/flutter/lib/src/material/text_form_field.dart
@@ -67,6 +67,7 @@
     FormFieldSetter<String> onSaved,
     FormFieldValidator<String> validator,
     List<TextInputFormatter> inputFormatters,
+    bool enabled,
   }) : assert(initialValue == null || controller == null),
        assert(keyboardType != null),
        assert(textAlign != null),
@@ -101,6 +102,7 @@
         onChanged: field.didChange,
         onSubmitted: onFieldSubmitted,
         inputFormatters: inputFormatters,
+        enabled: enabled,
       );
     },
   );
diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart
index d088675..f7b339e 100644
--- a/packages/flutter/test/material/input_decorator_test.dart
+++ b/packages/flutter/test/material/input_decorator_test.dart
@@ -60,17 +60,21 @@
   return box.size.height;
 }
 
-double getBorderWeight(WidgetTester tester) {
+BorderSide getBorderSide(WidgetTester tester) {
   if (!tester.any(findBorderPainter()))
-    return 0.0;
+    return null;
   final CustomPaint customPaint = tester.widget(findBorderPainter());
   final dynamic/* _InputBorderPainter */ inputBorderPainter = customPaint.foregroundPainter;
   final dynamic/*_InputBorderTween */ inputBorderTween = inputBorderPainter.border;
   final Animation<double> animation = inputBorderPainter.borderAnimation;
   final dynamic/*_InputBorder */ border = inputBorderTween.evaluate(animation);
-  return border.borderSide.width;
+  return border.borderSide;
 }
 
+double getBorderWeight(WidgetTester tester) => getBorderSide(tester)?.width;
+
+Color getBorderColor(WidgetTester tester) => getBorderSide(tester)?.color;
+
 double getHintOpacity(WidgetTester tester) {
   final Opacity opacityWidget = tester.widget<Opacity>(
     find.ancestor(
@@ -190,7 +194,8 @@
     expect(getBorderBottom(tester), 56.0);
     expect(getBorderWeight(tester), 2.0);
 
-    // enabled: false causes the border to disappear
+    // enabled: false produces a hairline border if filled: false (the default)
+    // The widget's size and layout is the same as for enabled: true.
     await tester.pumpWidget(
       buildInputDecorator(
         isEmpty: true,
@@ -208,6 +213,27 @@
     expect(tester.getTopLeft(find.text('label')).dy, 20.0);
     expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
     expect(getBorderWeight(tester), 0.0);
+
+    // enabled: false produces a transparent border if filled: true.
+    // The widget's size and layout is the same as for enabled: true.
+    await tester.pumpWidget(
+      buildInputDecorator(
+        isEmpty: true,
+        isFocused: false,
+        decoration: const InputDecoration(
+          labelText: 'label',
+          enabled: false,
+          filled: true,
+        ),
+      ),
+    );
+    await tester.pumpAndSettle();
+    expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
+    expect(tester.getTopLeft(find.text('text')).dy, 28.0);
+    expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
+    expect(tester.getTopLeft(find.text('label')).dy, 20.0);
+    expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
+    expect(getBorderColor(tester), Colors.transparent);
   });
 
   // Overall height for this InputDecorator is 40.0dps