Added InputDecorationTheme (#14177)
This commit is contained in:
parent
38ce59f039
commit
677f218ef6
@ -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() {
|
||||
if (!needsCounter)
|
||||
return widget.decoration;
|
||||
final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
|
||||
.applyDefaults(Theme.of(context).inputDecorationTheme);
|
||||
|
||||
if (!needsCounter)
|
||||
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,18 +20,27 @@ Widget buildInputDecorator({
|
||||
}) {
|
||||
return new MaterialApp(
|
||||
home: new Material(
|
||||
child: new Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: new Directionality(
|
||||
textDirection: textDirection,
|
||||
child: new InputDecorator(
|
||||
decoration: decoration,
|
||||
isEmpty: isEmpty,
|
||||
isFocused: isFocused,
|
||||
baseStyle: baseStyle,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
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(
|
||||
textDirection: textDirection,
|
||||
child: new InputDecorator(
|
||||
decoration: decoration,
|
||||
isEmpty: isEmpty,
|
||||
isFocused: isFocused,
|
||||
baseStyle: baseStyle,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -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());
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user