Unverified Commit 677f218e authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added InputDecorationTheme (#14177)

parent 38ce59f0
......@@ -28,6 +28,13 @@ import 'package:flutter/widgets.dart';
/// rounded rectangle around the input decorator's container.
/// * [InputDecoration], which is used to configure an [InputDecorator].
abstract class InputBorder extends ShapeBorder {
/// No input border.
///
/// Use this value with [InputDecoration.border] to specify that no border
/// should be drawn. The [InputDecoration.collapsed] constructor sets
/// its border to this value.
static const InputBorder none = const _NoInputBorder();
/// Creates a border for an [InputDecorator].
///
/// The [borderSide] parameter must not be null. Applications typically do
......@@ -72,6 +79,43 @@ abstract class InputBorder extends ShapeBorder {
});
}
// Used to create the InputBorder.none singleton.
class _NoInputBorder extends InputBorder {
const _NoInputBorder() : super(borderSide: BorderSide.none);
@override
_NoInputBorder copyWith({ BorderSide borderSide }) => const _NoInputBorder();
@override
bool get isOutline => false;
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
@override
_NoInputBorder scale(double t) => const _NoInputBorder();
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return new Path()..addRect(rect);
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()..addRect(rect);
}
@override
void paint(Canvas canvas, Rect rect, {
double gapStart,
double gapExtent: 0.0,
double gapPercentage: 0.0,
TextDirection textDirection,
}) {
// Do not paint.
}
}
/// Draws a horizontal line at the bottom of an [InputDecorator]'s container.
///
/// The input decorator's "container" is the optionally filled area above the
......
......@@ -402,6 +402,7 @@ enum _DecorationSlot {
class _Decoration {
const _Decoration({
@required this.contentPadding,
@required this.isCollapsed,
@required this.floatingLabelHeight,
@required this.floatingLabelProgress,
this.border,
......@@ -418,10 +419,12 @@ class _Decoration {
this.counter,
this.container,
}) : assert(contentPadding != null),
assert(isCollapsed != null),
assert(floatingLabelHeight != null),
assert(floatingLabelProgress != null);
final EdgeInsets contentPadding;
final bool isCollapsed;
final double floatingLabelHeight;
final double floatingLabelProgress;
final InputBorder border;
......@@ -895,9 +898,9 @@ class _RenderDecoration extends RenderBox {
final double right = overallWidth - contentPadding.right;
height = layout.containerHeight;
baseline = decoration.border == null || decoration.border.isOutline
? layout.outlineBaseline
: layout.inputBaseline;
baseline = decoration.isCollapsed || !decoration.border.isOutline
? layout.inputBaseline
: layout.outlineBaseline;
if (icon != null) {
final double x = textDirection == TextDirection.rtl ? overallWidth - icon.size.width : 0.0;
......@@ -1250,7 +1253,7 @@ class InputDecorator extends StatefulWidget {
/// The [isFocused] and [isEmpty] arguments must not be null.
const InputDecorator({
Key key,
@required this.decoration,
this.decoration,
this.baseStyle,
this.textAlign,
this.isFocused: false,
......@@ -1261,12 +1264,17 @@ class InputDecorator extends StatefulWidget {
super(key: key);
/// The text and styles to use when decorating the child.
///
/// If null, `const InputDecoration()` is used. Null [InputDecoration]
/// properties are initialized with the corresponding values from
/// [ThemeData.inputDecorationTheme].
final InputDecoration decoration;
/// The style on which to base the label, hint, counter, and error styles
/// if the [decoration] does not provide explicit styles.
///
/// If null, defaults to a text style from the current [Theme].
/// If null, `baseStyle` defaults to the `subhead` style from the
/// current [Theme], see [ThemeData.textTheme].
final TextStyle baseStyle;
/// How the text in the decoration should be aligned horizontally.
......@@ -1341,6 +1349,12 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_effectiveDecoration = null;
}
@override
void dispose() {
_floatingLabelController.dispose();
......@@ -1354,7 +1368,14 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
});
}
InputDecoration get decoration => widget.decoration;
InputDecoration _effectiveDecoration;
InputDecoration get decoration {
_effectiveDecoration ??= widget.decoration.applyDefaults(
Theme.of(context).inputDecorationTheme
);
return _effectiveDecoration;
}
TextAlign get textAlign => widget.textAlign;
bool get isFocused => widget.isFocused;
bool get isEmpty => widget.isEmpty;
......@@ -1362,6 +1383,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
@override
void didUpdateWidget(InputDecorator old) {
super.didUpdateWidget(old);
if (widget.decoration != old.decoration)
_effectiveDecoration = null;
if (widget._labelIsFloating != old._labelIsFloating) {
if (widget._labelIsFloating)
_floatingLabelController.forward();
......@@ -1392,7 +1416,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
}
Color _getFillColor(ThemeData themeData) {
if (!decoration.filled)
if (decoration.filled != true) // filled == null same as filled == false
return Colors.transparent;
if (decoration.fillColor != null)
return decoration.fillColor;
......@@ -1418,12 +1442,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
// then the label appears where the hint would.
bool get _hasInlineLabel => !isFocused && isEmpty && decoration.labelText != null;
// The style for the inline label or hint when they're displayed "inline", i.e.
// when they appear in place of the empty text field.
TextStyle _getInlineLabelStyle(ThemeData themeData) {
// The base style for the inline label or hint when they're displayed "inline",
// 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)
.merge(decoration.hintStyle);
.copyWith(color: themeData.hintColor);
}
TextStyle _getFloatingLabelStyle(ThemeData themeData) {
......@@ -1445,7 +1468,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
}
double get _borderWeight {
if (decoration.isCollapsed || decoration.border == null || !decoration.enabled)
if (decoration.isCollapsed || decoration.border == InputBorder.none || !decoration.enabled)
return 0.0;
return isFocused ? 2.0 : 1.0;
}
......@@ -1459,21 +1482,22 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TextStyle inlineStyle = _getInlineLabelStyle(themeData);
final TextStyle inlineStyle = _getInlineStyle(themeData);
final TextStyle hintStyle = inlineStyle.merge(decoration.hintStyle);
final Widget hint = decoration.hintText == null ? null : new AnimatedOpacity(
opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new Text(
decoration.hintText,
style: inlineStyle,
style: hintStyle,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
),
);
final InputBorder border = decoration.border == null ? null : decoration.border.copyWith(
final InputBorder border = decoration.border.copyWith(
borderSide: new BorderSide(
color: _getBorderColor(themeData),
width: _borderWeight,
......@@ -1490,6 +1514,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
child: containerFill,
);
final TextStyle inlineLabelStyle = inlineStyle.merge(decoration.labelStyle);
final Widget label = decoration.labelText == null ? null : new _Shaker(
animation: _shakingLabelController.view,
child: new AnimatedDefaultTextStyle(
......@@ -1497,7 +1522,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
curve: _kTransitionCurve,
style: widget._labelIsFloating
? _getFloatingLabelStyle(themeData)
: _getInlineLabelStyle(themeData),
: inlineLabelStyle,
child: new Text(
decoration.labelText,
overflow: TextOverflow.ellipsis,
......@@ -1513,7 +1538,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
opacity: widget._labelIsFloating ? 1.0 : 0.0,
child: new Text(
decoration.prefixText,
style: decoration.prefixStyle ?? inlineStyle
style: decoration.prefixStyle ?? hintStyle
),
);
......@@ -1524,12 +1549,13 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
opacity: widget._labelIsFloating ? 1.0 : 0.0,
child: new Text(
decoration.suffixText,
style: decoration.suffixStyle ?? inlineStyle
style: decoration.suffixStyle ?? hintStyle
),
);
final Color activeColor = _getActiveColor(themeData);
final double iconSize = decoration.isDense ? 18.0 : 24.0;
final bool decorationIsDense = decoration.isDense == true; // isDense == null, same as false
final double iconSize = decorationIsDense ? 18.0 : 24.0;
final Color iconColor = isFocused ? activeColor : Colors.black45;
final Widget icon = decoration.icon == null ? null :
......@@ -1583,24 +1609,24 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
if (decoration.isCollapsed) {
floatingLabelHeight = 0.0;
contentPadding = decoration.contentPadding ?? EdgeInsets.zero;
} else if (decoration.border == null || !decoration.border.isOutline) {
} else if (!decoration.border.isOutline) {
// 4.0: the vertical gap between the inline elements and the floating label.
floatingLabelHeight = 4.0 + 0.75 * inlineStyle.fontSize;
if (decoration.filled) {
contentPadding = decoration.contentPadding ?? (decoration.isDense
floatingLabelHeight = 4.0 + 0.75 * inlineLabelStyle.fontSize;
if (decoration.filled == true) { // filled == null same as filled == false
contentPadding = decoration.contentPadding ?? (decorationIsDense
? const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0)
: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 12.0));
} else {
// Not left or right padding for underline borders that aren't filled
// is a small concession to backwards compatibility. This eliminates
// the most noticeable layout change introduced by #13734.
contentPadding = decoration.contentPadding ?? (decoration.isDense
contentPadding = decoration.contentPadding ?? (decorationIsDense
? const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0)
: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 12.0));
}
} else {
floatingLabelHeight = 0.0;
contentPadding = decoration.contentPadding ?? (decoration.isDense
contentPadding = decoration.contentPadding ?? (decorationIsDense
? const EdgeInsets.fromLTRB(12.0, 20.0, 12.0, 12.0)
: const EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 16.0));
}
......@@ -1608,6 +1634,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
return new _Decorator(
decoration: new _Decoration(
contentPadding: contentPadding,
isCollapsed: decoration.isCollapsed,
floatingLabelHeight: floatingLabelHeight,
floatingLabelProgress: _floatingLabelController.value,
border: decoration.border,
......@@ -1648,8 +1675,12 @@ class InputDecoration {
/// Creates a bundle of the border, labels, icons, and styles used to
/// decorate a Material Design text field.
///
/// The [isDense], [filled], and [enabled] arguments must not
/// be null.
/// Unless specified by [ThemeData.inputDecorationTheme],
/// [InputDecorator] defaults [isDense] to true, and [filled] to false,
/// Similarly, the default border is an instance of [UnderlineInputBorder].
/// If [border] is [InputBorder.none] then no border is drawn.
///
/// The [enabled] argument must not be null.
const InputDecoration({
this.icon,
this.labelText,
......@@ -1660,7 +1691,7 @@ class InputDecoration {
this.hintStyle,
this.errorText,
this.errorStyle,
this.isDense: false,
this.isDense,
this.contentPadding,
this.prefixIcon,
this.prefixText,
......@@ -1670,18 +1701,15 @@ class InputDecoration {
this.suffixStyle,
this.counterText,
this.counterStyle,
this.filled: false,
this.filled,
this.fillColor,
this.border: const UnderlineInputBorder(),
this.border,
this.enabled: true,
}) : assert(isDense != null),
assert(filled != null),
assert(enabled != null),
isCollapsed = false;
}) : assert(enabled != null), isCollapsed = false;
/// Defines an [InputDecorator] that is the same size as the input field.
///
/// This type of input decoration only includes the border.
/// This type of input decoration does not include a border by default.
///
/// Sets the [isCollapsed] property to true.
const InputDecoration.collapsed({
......@@ -1689,10 +1717,9 @@ class InputDecoration {
this.hintStyle,
this.filled: false,
this.fillColor,
this.border: const UnderlineInputBorder(),
this.border: InputBorder.none,
this.enabled: true,
}) : assert(filled != null),
assert(enabled != null),
}) : assert(enabled != null),
icon = null,
labelText = null,
labelStyle = null,
......@@ -1993,6 +2020,28 @@ class InputDecoration {
);
}
/// Used by widgets like [TextField] and [InputDecorator] to create a new
/// [InputDecoration] with default values taken from the [theme].
///
/// Only null valued properties from this [InputDecoration] are replaced
/// by the corresponding values from [theme].
InputDecoration applyDefaults(InputDecorationTheme theme) {
return copyWith(
labelStyle: labelStyle ?? theme.labelStyle,
helperStyle: helperStyle ?? theme.helperStyle,
hintStyle: hintStyle ?? theme.hintStyle,
errorStyle: errorStyle ?? theme.errorStyle,
isDense: isDense ?? theme.isDense,
contentPadding: contentPadding ?? theme.contentPadding,
prefixStyle: prefixStyle ?? theme.prefixStyle,
suffixStyle: suffixStyle ?? theme.suffixStyle,
counterStyle: counterStyle ?? theme.counterStyle,
filled: filled ?? theme.filled,
fillColor: fillColor ?? theme.fillColor,
border: border ?? theme.border,
);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
......@@ -2071,7 +2120,7 @@ class InputDecoration {
description.add('hintText: "$hintText"');
if (errorText != null)
description.add('errorText: "$errorText"');
if (isDense)
if (isDense ?? false)
description.add('isDense: $isDense');
if (contentPadding != null)
description.add('contentPadding: $contentPadding');
......@@ -2093,7 +2142,7 @@ class InputDecoration {
description.add('counterText: $counterText');
if (counterStyle != null)
description.add('counterStyle: $counterStyle');
if (filled)
if (filled == true) // filled == null same as filled == false
description.add('filled: true');
if (fillColor != null)
description.add('fillColor: $fillColor');
......@@ -2104,3 +2153,151 @@ class InputDecoration {
return 'InputDecoration(${description.join(', ')})';
}
}
/// Defines the default appearance of [InputDecorator]s.
///
/// This class is used to define the value of [ThemeData.inputDecorationTheme].
/// The [InputDecorator], [TextField], and [TextFormField] widgets use
/// the current input decoration theme to initialize null [InputDecoration]
/// properties.
///
/// The [InputDecoration.applyDefaults] method is used to combine a input
/// decoration theme with an [InputDecoration] object.
@immutable
class InputDecorationTheme {
/// Creates a value for [ThemeData.inputDecorationTheme] that
/// defines default values for [InputDecorator].
///
/// The values of [isDense], [isCollapsed], [isFilled], and [border] must
/// not be null.
const InputDecorationTheme({
this.labelStyle,
this.helperStyle,
this.hintStyle,
this.errorStyle,
this.isDense: false,
this.contentPadding,
this.isCollapsed: false,
this.prefixStyle,
this.suffixStyle,
this.counterStyle,
this.filled: false,
this.fillColor,
this.border: const UnderlineInputBorder(),
}) : assert(isDense != null),
assert(isCollapsed != null),
assert(filled != null),
assert(border != null);
/// The style to use for [InputDecoration.labelText] when the label is
/// above (i.e., vertically adjacent to) the input field.
///
/// When the [labelText] is on top of the input field, the text uses the
/// [hintStyle] instead.
///
/// If null, defaults to a value derived from the base [TextStyle] for the
/// input field and the current [Theme].
final TextStyle labelStyle;
/// The style to use for [InputDecoration.helperText].
final TextStyle helperStyle;
/// The style to use for the [InputDecoration.hintText].
///
/// Also used for the [labelText] when the [labelText] is displayed on
/// top of the input field (i.e., at the same location on the screen where
/// text my be entered in the input field).
///
/// If null, defaults to a value derived from the base [TextStyle] for the
/// input field and the current [Theme].
final TextStyle hintStyle;
/// The style to use for the [InputDecoration.errorText].
///
/// If null, defaults of a value derived from the base [TextStyle] for the
/// input field and the current [Theme].
final TextStyle errorStyle;
/// Whether the input decorator's child is part of a dense form (i.e., uses
/// less vertical space).
///
/// Defaults to false.
final bool isDense;
/// The padding for the input decoration's container.
///
/// The decoration's container is the area which is filled if
/// [InputDecoration.isFilled] is true and bordered per the [border].
/// It's the area adjacent to [InputDecoration.icon] and above the
/// [InputDecoration.icon] and above the widgets that contain
/// [InputDecoration.helperText], [InputDecoration.errorText], and
/// [InputDecoration.counterText].
///
/// By default the `contentPadding` reflects [isDense] and the type of the
/// [border]. If [isCollapsed] is true then `contentPadding` is
/// [EdgeInsets.zero].
final EdgeInsets contentPadding;
/// Whether the decoration is the same size as the input field.
///
/// A collapsed decoration cannot have [InputDecoration.labelText],
/// [InputDecoration.errorText], or an [InputDecoration.icon].
final bool isCollapsed;
/// The style to use for the [InputDecoration.prefixText].
///
/// If null, defaults to the [hintStyle].
final TextStyle prefixStyle;
/// The style to use for the [InputDecoration.suffixText].
///
/// If null, defaults to the [hintStyle].
final TextStyle suffixStyle;
/// The style to use for the [InputDecoration.counterText].
///
/// If null, defaults to the [helperStyle].
final TextStyle counterStyle;
/// If true the decoration's container is filled with [fillColor].
///
/// Typically this field set to true if [border] is
/// [const UnderlineInputBorder()].
///
/// The decoration's container is the area, defined by the border's
/// [InputBorder.getOuterPath], which is filled if [isFilled] is
/// true and bordered per the [border].
///
/// This property is false by default.
final bool filled;
/// The color to fill the decoration's container with, if [filled] is true.
///
/// By default the fillColor is based on the current [Theme].
///
/// The decoration's container is the area, defined by the border's
/// [InputBorder.getOuterPath], which is filled if [isFilled] is
/// true and bordered per the [border].
final Color fillColor;
/// The border to draw around the decoration's container.
///
/// The decoration's container is the area which is filled if [isFilled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [InputDecoration.icon] and above the widgets that contain
/// [InputDecoration.helperText], [InputDecoration.errorText], and
/// [InputDecoration.counterText].
///
/// The default value of this property is `const UnderlineInputBorder()`.
///
/// The border's bounds, i.e. the value of `border.getOuterPath()`, defines
/// the area to be filled.
///
/// See also:
/// * [InputBorder.none], which doesn't draw a border.
/// * [UnderlineInputBorder], which draws a horizontal line at the
/// bottom of the input decorator's container.
/// * [OutlineInputBorder], an [InputDecorator] border which draws a
/// rounded rectangle around the input decorator's container.
final InputBorder border;
}
......@@ -296,10 +296,12 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
&& widget.decoration.counterText == null;
InputDecoration _getEffectiveDecoration() {
final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
.applyDefaults(Theme.of(context).inputDecorationTheme);
if (!needsCounter)
return widget.decoration;
return effectiveDecoration;
final InputDecoration effectiveDecoration = widget?.decoration ?? const InputDecoration();
final String counterText = '${_effectiveController.value.text.runes.length} / ${widget.maxLength}';
if (_effectiveController.value.text.runes.length > widget.maxLength) {
final ThemeData themeData = Theme.of(context);
......
......@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'input_decorator.dart';
import 'text_field.dart';
import 'theme.dart';
/// A [FormField] that contains a [TextField].
///
......@@ -71,10 +72,12 @@ class TextFormField extends FormField<String> {
validator: validator,
builder: (FormFieldState<String> field) {
final _TextFormFieldState state = field;
final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
return new TextField(
controller: state._effectiveController,
focusNode: focusNode,
decoration: decoration.copyWith(errorText: field.errorText),
decoration: effectiveDecoration.copyWith(errorText: field.errorText),
keyboardType: keyboardType,
style: style,
textAlign: textAlign,
......
......@@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart';
import 'typography.dart';
/// Describes the contrast needs of a color.
......@@ -101,6 +102,7 @@ class ThemeData {
TextTheme textTheme,
TextTheme primaryTextTheme,
TextTheme accentTextTheme,
InputDecorationTheme inputDecorationTheme,
IconThemeData iconTheme,
IconThemeData primaryIconTheme,
IconThemeData accentIconTheme,
......@@ -135,6 +137,7 @@ class ThemeData {
indicatorColor ??= accentColor == primaryColor ? Colors.white : accentColor;
hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000);
errorColor ??= Colors.red[700];
inputDecorationTheme ??= const InputDecorationTheme();
iconTheme ??= isDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
accentIconTheme ??= accentIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
......@@ -176,6 +179,7 @@ class ThemeData {
textTheme: textTheme,
primaryTextTheme: primaryTextTheme,
accentTextTheme: accentTextTheme,
inputDecorationTheme: inputDecorationTheme,
iconTheme: iconTheme,
primaryIconTheme: primaryIconTheme,
accentIconTheme: accentIconTheme,
......@@ -217,6 +221,7 @@ class ThemeData {
@required this.textTheme,
@required this.primaryTextTheme,
@required this.accentTextTheme,
@required this.inputDecorationTheme,
@required this.iconTheme,
@required this.primaryIconTheme,
@required this.accentIconTheme,
......@@ -248,6 +253,7 @@ class ThemeData {
assert(textTheme != null),
assert(primaryTextTheme != null),
assert(accentTextTheme != null),
assert(inputDecorationTheme != null),
assert(iconTheme != null),
assert(primaryIconTheme != null),
assert(accentIconTheme != null),
......@@ -388,6 +394,12 @@ class ThemeData {
/// A text theme that contrasts with the accent color.
final TextTheme accentTextTheme;
/// The default [InputDecoration] values for [InputDecorator], [TextField],
/// and [TextFormField] are based on this theme.
///
/// See [InputDecoration.applyDefaults].
final InputDecorationTheme inputDecorationTheme;
/// An icon theme that contrasts with the card and canvas colors.
final IconThemeData iconTheme;
......@@ -431,6 +443,7 @@ class ThemeData {
TextTheme textTheme,
TextTheme primaryTextTheme,
TextTheme accentTextTheme,
InputDecorationTheme inputDecorationTheme,
IconThemeData iconTheme,
IconThemeData primaryIconTheme,
IconThemeData accentIconTheme,
......@@ -464,6 +477,7 @@ class ThemeData {
textTheme: textTheme ?? this.textTheme,
primaryTextTheme: primaryTextTheme ?? this.primaryTextTheme,
accentTextTheme: accentTextTheme ?? this.accentTextTheme,
inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
iconTheme: iconTheme ?? this.iconTheme,
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
accentIconTheme: accentIconTheme ?? this.accentIconTheme,
......@@ -582,6 +596,7 @@ class ThemeData {
textTheme: TextTheme.lerp(a.textTheme, b.textTheme, t),
primaryTextTheme: TextTheme.lerp(a.primaryTextTheme, b.primaryTextTheme, t),
accentTextTheme: TextTheme.lerp(a.accentTextTheme, b.accentTextTheme, t),
inputDecorationTheme: t < 0.5 ? a.inputDecorationTheme : b.inputDecorationTheme,
iconTheme: IconThemeData.lerp(a.iconTheme, b.iconTheme, t),
primaryIconTheme: IconThemeData.lerp(a.primaryIconTheme, b.primaryIconTheme, t),
accentIconTheme: IconThemeData.lerp(a.accentIconTheme, b.accentIconTheme, t),
......@@ -621,6 +636,7 @@ class ThemeData {
(otherData.textTheme == textTheme) &&
(otherData.primaryTextTheme == primaryTextTheme) &&
(otherData.accentTextTheme == accentTextTheme) &&
(otherData.inputDecorationTheme == inputDecorationTheme) &&
(otherData.iconTheme == iconTheme) &&
(otherData.primaryIconTheme == primaryIconTheme) &&
(otherData.accentIconTheme == accentIconTheme) &&
......@@ -659,6 +675,7 @@ class ThemeData {
primaryTextTheme,
accentTextTheme,
iconTheme,
inputDecorationTheme,
primaryIconTheme,
accentIconTheme,
platform,
......
......@@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
Widget buildInputDecorator({
InputDecoration decoration: const InputDecoration(),
InputDecorationTheme inputDecorationTheme,
TextDirection textDirection: TextDirection.ltr,
bool isEmpty: false,
bool isFocused: false,
......@@ -19,6 +20,12 @@ Widget buildInputDecorator({
}) {
return new MaterialApp(
home: new Material(
child: new Builder(
builder: (BuildContext context) {
return new Theme(
data: Theme.of(context).copyWith(
inputDecorationTheme: inputDecorationTheme,
),
child: new Align(
alignment: Alignment.topLeft,
child: new Directionality(
......@@ -32,6 +39,9 @@ Widget buildInputDecorator({
),
),
),
);
},
),
),
);
}
......@@ -394,14 +404,14 @@ void main() {
expect(getBorderWeight(tester), 2.0);
});
testWidgets('InputDecorator with null border', (WidgetTester tester) async {
testWidgets('InputDecorator with no input border', (WidgetTester tester) async {
// Label is visible, hint is not (opacity 0.0).
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
// isFocused: false (default)
decoration: const InputDecoration(
border: null,
border: InputBorder.none,
),
),
);
......@@ -898,6 +908,131 @@ void main() {
expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 64.0));
});
testWidgets('InputDecorationTheme outline border', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true, // label appears, vertically centered
// isFocused: false (default)
inputDecorationTheme: const InputDecorationTheme(
border: const OutlineInputBorder(),
),
decoration: const InputDecoration(
labelText: 'label',
),
),
);
// Overall height for this InputDecorator is 56dps. Layout is:
// 20 - top padding
// 16 - label (ahem font size 16dps)
// 20 - bottom padding
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 1.0);
});
testWidgets('InputDecorationTheme outline border, dense layout', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true, // label appears, vertically centered
// isFocused: false (default)
inputDecorationTheme: const InputDecorationTheme(
border: const OutlineInputBorder(),
isDense: true,
),
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
),
),
);
// Overall height for this InputDecorator is 56dps. Layout is:
// 16 - top padding
// 16 - label (ahem font size 16dps)
// 16 - bottom padding
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 48.0));
expect(tester.getTopLeft(find.text('label')).dy, 16.0);
expect(tester.getBottomLeft(find.text('label')).dy, 32.0);
expect(getBorderBottom(tester), 48.0);
expect(getBorderWeight(tester), 1.0);
});
testWidgets('InputDecorationTheme style overrides', (WidgetTester tester) async {
const TextStyle style16 = const TextStyle(fontFamily: 'Ahem', fontSize: 16.0);
final TextStyle labelStyle = style16.merge(const TextStyle(color: Colors.red));
final TextStyle hintStyle = style16.merge(const TextStyle(color: Colors.green));
final TextStyle prefixStyle = style16.merge(const TextStyle(color: Colors.blue));
final TextStyle suffixStyle = style16.merge(const TextStyle(color: Colors.purple));
const TextStyle style12 = const TextStyle(fontFamily: 'Ahem', fontSize: 12.0);
final TextStyle helperStyle = style12.merge(const TextStyle(color: Colors.orange));
final TextStyle counterStyle = style12.merge(const TextStyle(color: Colors.orange));
// This test also verifies that the default InputDecorator provides a
// "small concession to backwards compatibility" by not padding on
// the left and right. If filled is true or an outline border is
// provided then the horizontal padding is included.
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true, // label appears, vertically centered
// isFocused: false (default)
inputDecorationTheme: new InputDecorationTheme(
labelStyle: labelStyle,
hintStyle: hintStyle,
prefixStyle: prefixStyle,
suffixStyle: suffixStyle,
helperStyle: helperStyle,
counterStyle: counterStyle,
// filled: false (default) - don't pad by left/right 12dps
),
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
prefixText: 'prefix',
suffixText: 'suffix',
helperText: 'helper',
counterText: 'counter',
),
),
);
// Overall height for this InputDecorator is 76dps. Layout is:
// 12 - top padding
// 12 - floating label (ahem font size 16dps * 0.75 = 12)
// 4 - floating label / input text gap
// 16 - prefix/hint/input/suffix text (ahem font size 16dps)
// 12 - bottom padding
// 8 - below the border padding
// 12 - help/error/counter text (ahem font size 12dps)
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 76.0));
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 1.0);
expect(tester.getTopLeft(find.text('helper')), const Offset(0.0, 64.0));
expect(tester.getTopRight(find.text('counter')), const Offset(800.0, 64.0));
// Verify that the styles were passed along
expect(tester.widget<Text>(find.text('prefix')).style.color, prefixStyle.color);
expect(tester.widget<Text>(find.text('suffix')).style.color, suffixStyle.color);
expect(tester.widget<Text>(find.text('helper')).style.color, helperStyle.color);
expect(tester.widget<Text>(find.text('counter')).style.color, counterStyle.color);
TextStyle getLabelStyle() {
return tester.firstWidget<AnimatedDefaultTextStyle>(
find.ancestor(
of: find.text('label'),
matching: find.byType(AnimatedDefaultTextStyle)
)
).style;
}
expect(getLabelStyle().color, labelStyle.color);
});
testWidgets('InputDecorator.toString()', (WidgetTester tester) async {
final Widget child = const InputDecorator(
key: const Key('key'),
......@@ -910,11 +1045,11 @@ void main() {
);
expect(
child.toString(),
"InputDecorator-[<'key'>](decoration: InputDecoration(border: UnderlineInputBorder()), baseStyle: TextStyle(<all styles inherited>), isFocused: false, isEmpty: false)",
"InputDecorator-[<'key'>](decoration: InputDecoration(), baseStyle: TextStyle(<all styles inherited>), isFocused: false, isEmpty: false)",
);
});
testWidgets('InputDecorator with null border and label', (WidgetTester tester) async {
testWidgets('InputDecorator with empty border and label', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/14165
await tester.pumpWidget(
buildInputDecorator(
......@@ -922,7 +1057,7 @@ void main() {
// isFocused: false (default)
decoration: const InputDecoration(
labelText: 'label',
border: null,
border: InputBorder.none,
),
),
);
......@@ -932,4 +1067,84 @@ void main() {
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
});
testWidgets('InputDecorationTheme.inputDecoration', (WidgetTester tester) async {
const TextStyle themeStyle = const TextStyle(color: Colors.green);
const TextStyle decorationStyle = const TextStyle(color: Colors.blue);
// InputDecorationTheme arguments define InputDecoration properties.
InputDecoration decoration = const InputDecoration().applyDefaults(
const InputDecorationTheme(
labelStyle: themeStyle,
helperStyle: themeStyle,
hintStyle: themeStyle,
errorStyle: themeStyle,
isDense: true,
contentPadding: const EdgeInsets.all(1.0),
prefixStyle: themeStyle,
suffixStyle: themeStyle,
counterStyle: themeStyle,
filled: true,
fillColor: Colors.red,
border: InputBorder.none,
)
);
expect(decoration.labelStyle, themeStyle);
expect(decoration.helperStyle, themeStyle);
expect(decoration.hintStyle, themeStyle);
expect(decoration.errorStyle, themeStyle);
expect(decoration.isDense, true);
expect(decoration.contentPadding, const EdgeInsets.all(1.0));
expect(decoration.prefixStyle, themeStyle);
expect(decoration.suffixStyle, themeStyle);
expect(decoration.counterStyle, themeStyle);
expect(decoration.filled, true);
expect(decoration.fillColor, Colors.red);
expect(decoration.border, InputBorder.none);
// InputDecoration (baseDecoration) defines InputDecoration properties
decoration = const InputDecoration(
labelStyle: decorationStyle,
helperStyle: decorationStyle,
hintStyle: decorationStyle,
errorStyle: decorationStyle,
isDense: false,
contentPadding: const EdgeInsets.all(4.0),
prefixStyle: decorationStyle,
suffixStyle: decorationStyle,
counterStyle: decorationStyle,
filled: false,
fillColor: Colors.blue,
border: const OutlineInputBorder(),
).applyDefaults(
const InputDecorationTheme(
labelStyle: themeStyle,
helperStyle: themeStyle,
hintStyle: themeStyle,
errorStyle: themeStyle,
isDense: true,
contentPadding: const EdgeInsets.all(1.0),
prefixStyle: themeStyle,
suffixStyle: themeStyle,
counterStyle: themeStyle,
filled: true,
fillColor: Colors.red,
border: InputBorder.none,
),
);
expect(decoration.labelStyle, decorationStyle);
expect(decoration.helperStyle, decorationStyle);
expect(decoration.hintStyle, decorationStyle);
expect(decoration.errorStyle, decorationStyle);
expect(decoration.isDense, false);
expect(decoration.contentPadding, const EdgeInsets.all(4.0));
expect(decoration.prefixStyle, decorationStyle);
expect(decoration.suffixStyle, decorationStyle);
expect(decoration.counterStyle, decorationStyle);
expect(decoration.filled, false);
expect(decoration.fillColor, Colors.blue);
expect(decoration.border, const OutlineInputBorder());
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment