blob: dc346ffbb289d7f51611cc134bf775ca36fc28bb [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'input_border.dart';
import 'theme.dart';
import 'theme_data.dart';
const Duration _kTransitionDuration = Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
// Defines the gap in the InputDecorator's outline border where the
// floating label will appear.
class _InputBorderGap extends ChangeNotifier {
double _start;
double get start => _start;
set start(double value) {
if (value != _start) {
_start = value;
notifyListeners();
}
}
double _extent = 0.0;
double get extent => _extent;
set extent(double value) {
if (value != _extent) {
_extent = value;
notifyListeners();
}
}
@override
bool operator ==(Object other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
return other is _InputBorderGap
&& other.start == start
&& other.extent == extent;
}
@override
int get hashCode => hashValues(start, extent);
}
// Used to interpolate between two InputBorders.
class _InputBorderTween extends Tween<InputBorder> {
_InputBorderTween({InputBorder begin, InputBorder end}) : super(begin: begin, end: end);
@override
InputBorder lerp(double t) => ShapeBorder.lerp(begin, end, t) as InputBorder;
}
// Passes the _InputBorderGap parameters along to an InputBorder's paint method.
class _InputBorderPainter extends CustomPainter {
_InputBorderPainter({
@required Listenable repaint,
@required this.borderAnimation,
@required this.border,
@required this.gapAnimation,
@required this.gap,
@required this.textDirection,
@required this.fillColor,
@required this.hoverAnimation,
@required this.hoverColorTween,
}) : super(repaint: repaint);
final Animation<double> borderAnimation;
final _InputBorderTween border;
final Animation<double> gapAnimation;
final _InputBorderGap gap;
final TextDirection textDirection;
final Color fillColor;
final ColorTween hoverColorTween;
final Animation<double> hoverAnimation;
Color get blendedColor => Color.alphaBlend(hoverColorTween.evaluate(hoverAnimation), fillColor);
@override
void paint(Canvas canvas, Size size) {
final InputBorder borderValue = border.evaluate(borderAnimation);
final Rect canvasRect = Offset.zero & size;
final Color blendedFillColor = blendedColor;
if (blendedFillColor.alpha > 0) {
canvas.drawPath(
borderValue.getOuterPath(canvasRect, textDirection: textDirection),
Paint()
..color = blendedFillColor
..style = PaintingStyle.fill,
);
}
borderValue.paint(
canvas,
canvasRect,
gapStart: gap.start,
gapExtent: gap.extent,
gapPercentage: gapAnimation.value,
textDirection: textDirection,
);
}
@override
bool shouldRepaint(_InputBorderPainter oldPainter) {
return borderAnimation != oldPainter.borderAnimation
|| hoverAnimation != oldPainter.hoverAnimation
|| gapAnimation != oldPainter.gapAnimation
|| border != oldPainter.border
|| gap != oldPainter.gap
|| textDirection != oldPainter.textDirection;
}
}
// An analog of AnimatedContainer, which can animate its shaped border, for
// _InputBorder. This specialized animated container is needed because the
// _InputBorderGap, which is computed at layout time, is required by the
// _InputBorder's paint method.
class _BorderContainer extends StatefulWidget {
const _BorderContainer({
Key key,
@required this.border,
@required this.gap,
@required this.gapAnimation,
@required this.fillColor,
@required this.hoverColor,
@required this.isHovering,
this.child,
}) : assert(border != null),
assert(gap != null),
assert(fillColor != null),
super(key: key);
final InputBorder border;
final _InputBorderGap gap;
final Animation<double> gapAnimation;
final Color fillColor;
final Color hoverColor;
final bool isHovering;
final Widget child;
@override
_BorderContainerState createState() => _BorderContainerState();
}
class _BorderContainerState extends State<_BorderContainer> with TickerProviderStateMixin {
static const Duration _kHoverDuration = Duration(milliseconds: 15);
AnimationController _controller;
AnimationController _hoverColorController;
Animation<double> _borderAnimation;
_InputBorderTween _border;
Animation<double> _hoverAnimation;
ColorTween _hoverColorTween;
@override
void initState() {
super.initState();
_hoverColorController = AnimationController(
duration: _kHoverDuration,
value: widget.isHovering ? 1.0 : 0.0,
vsync: this,
);
_controller = AnimationController(
duration: _kTransitionDuration,
vsync: this,
);
_borderAnimation = CurvedAnimation(
parent: _controller,
curve: _kTransitionCurve,
);
_border = _InputBorderTween(
begin: widget.border,
end: widget.border,
);
_hoverAnimation = CurvedAnimation(
parent: _hoverColorController,
curve: Curves.linear,
);
_hoverColorTween = ColorTween(begin: Colors.transparent, end: widget.hoverColor);
}
@override
void dispose() {
_controller.dispose();
_hoverColorController.dispose();
super.dispose();
}
@override
void didUpdateWidget(_BorderContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.border != oldWidget.border) {
_border = _InputBorderTween(
begin: oldWidget.border,
end: widget.border,
);
_controller
..value = 0.0
..forward();
}
if (widget.hoverColor != oldWidget.hoverColor) {
_hoverColorTween = ColorTween(begin: Colors.transparent, end: widget.hoverColor);
}
if (widget.isHovering != oldWidget.isHovering) {
if (widget.isHovering) {
_hoverColorController.forward();
} else {
_hoverColorController.reverse();
}
}
}
@override
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: _InputBorderPainter(
repaint: Listenable.merge(<Listenable>[
_borderAnimation,
widget.gap,
_hoverColorController,
]),
borderAnimation: _borderAnimation,
border: _border,
gapAnimation: widget.gapAnimation,
gap: widget.gap,
textDirection: Directionality.of(context),
fillColor: widget.fillColor,
hoverColorTween: _hoverColorTween,
hoverAnimation: _hoverAnimation,
),
child: widget.child,
);
}
}
// Used to "shake" the floating label to the left to the left and right
// when the errorText first appears.
class _Shaker extends AnimatedWidget {
const _Shaker({
Key key,
Animation<double> animation,
this.child,
}) : super(key: key, listenable: animation);
final Widget child;
Animation<double> get animation => listenable as Animation<double>;
double get translateX {
const double shakeDelta = 4.0;
final double t = animation.value;
if (t <= 0.25)
return -t * shakeDelta;
else if (t < 0.75)
return (t - 0.5) * shakeDelta;
else
return (1.0 - t) * 4.0 * shakeDelta;
}
@override
Widget build(BuildContext context) {
return Transform(
transform: Matrix4.translationValues(translateX, 0.0, 0.0),
child: child,
);
}
}
// Display the helper and error text. When the error text appears
// it fades and the helper text fades out. The error text also
// slides upwards a little when it first appears.
class _HelperError extends StatefulWidget {
const _HelperError({
Key key,
this.textAlign,
this.helperText,
this.helperStyle,
this.helperMaxLines,
this.errorText,
this.errorStyle,
this.errorMaxLines,
}) : super(key: key);
final TextAlign textAlign;
final String helperText;
final TextStyle helperStyle;
final int helperMaxLines;
final String errorText;
final TextStyle errorStyle;
final int errorMaxLines;
@override
_HelperErrorState createState() => _HelperErrorState();
}
class _HelperErrorState extends State<_HelperError> with SingleTickerProviderStateMixin {
// If the height of this widget and the counter are zero ("empty") at
// layout time, no space is allocated for the subtext.
static const Widget empty = SizedBox();
AnimationController _controller;
Widget _helper;
Widget _error;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: _kTransitionDuration,
vsync: this,
);
if (widget.errorText != null) {
_error = _buildError();
_controller.value = 1.0;
} else if (widget.helperText != null) {
_helper = _buildHelper();
}
_controller.addListener(_handleChange);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleChange() {
setState(() {
// The _controller's value has changed.
});
}
@override
void didUpdateWidget(_HelperError old) {
super.didUpdateWidget(old);
final String newErrorText = widget.errorText;
final String newHelperText = widget.helperText;
final String oldErrorText = old.errorText;
final String oldHelperText = old.helperText;
final bool errorTextStateChanged = (newErrorText != null) != (oldErrorText != null);
final bool helperTextStateChanged = newErrorText == null && (newHelperText != null) != (oldHelperText != null);
if (errorTextStateChanged || helperTextStateChanged) {
if (newErrorText != null) {
_error = _buildError();
_controller.forward();
} else if (newHelperText != null) {
_helper = _buildHelper();
_controller.reverse();
} else {
_controller.reverse();
}
}
}
Widget _buildHelper() {
assert(widget.helperText != null);
return Semantics(
container: true,
child: Opacity(
opacity: 1.0 - _controller.value,
child: Text(
widget.helperText,
style: widget.helperStyle,
textAlign: widget.textAlign,
overflow: TextOverflow.ellipsis,
maxLines: widget.helperMaxLines,
),
),
);
}
Widget _buildError() {
assert(widget.errorText != null);
return Semantics(
container: true,
liveRegion: true,
child: Opacity(
opacity: _controller.value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: const Offset(0.0, -0.25),
end: const Offset(0.0, 0.0),
).evaluate(_controller.view),
child: Text(
widget.errorText,
style: widget.errorStyle,
textAlign: widget.textAlign,
overflow: TextOverflow.ellipsis,
maxLines: widget.errorMaxLines,
),
),
),
);
}
@override
Widget build(BuildContext context) {
if (_controller.isDismissed) {
_error = null;
if (widget.helperText != null) {
return _helper = _buildHelper();
} else {
_helper = null;
return empty;
}
}
if (_controller.isCompleted) {
_helper = null;
if (widget.errorText != null) {
return _error = _buildError();
} else {
_error = null;
return empty;
}
}
if (_helper == null && widget.errorText != null)
return _buildError();
if (_error == null && widget.helperText != null)
return _buildHelper();
if (widget.errorText != null) {
return Stack(
children: <Widget>[
Opacity(
opacity: 1.0 - _controller.value,
child: _helper,
),
_buildError(),
],
);
}
if (widget.helperText != null) {
return Stack(
children: <Widget>[
_buildHelper(),
Opacity(
opacity: _controller.value,
child: _error,
),
],
);
}
return empty;
}
}
/// Defines the behaviour of the floating label
enum FloatingLabelBehavior {
/// The label will always be positioned within the content, or hidden.
never,
/// The label will float when the input is focused, or has content.
auto,
/// The label will always float above the content.
always,
}
// Identifies the children of a _RenderDecorationElement.
enum _DecorationSlot {
icon,
input,
label,
hint,
prefix,
suffix,
prefixIcon,
suffixIcon,
helperError,
counter,
container,
}
// An analog of InputDecoration for the _Decorator widget.
class _Decoration {
const _Decoration({
@required this.contentPadding,
@required this.isCollapsed,
@required this.floatingLabelHeight,
@required this.floatingLabelProgress,
this.border,
this.borderGap,
this.alignLabelWithHint,
this.isDense,
this.visualDensity,
this.icon,
this.input,
this.label,
this.hint,
this.prefix,
this.suffix,
this.prefixIcon,
this.suffixIcon,
this.helperError,
this.counter,
this.container,
}) : assert(contentPadding != null),
assert(isCollapsed != null),
assert(floatingLabelHeight != null),
assert(floatingLabelProgress != null);
final EdgeInsetsGeometry contentPadding;
final bool isCollapsed;
final double floatingLabelHeight;
final double floatingLabelProgress;
final InputBorder border;
final _InputBorderGap borderGap;
final bool alignLabelWithHint;
final bool isDense;
final VisualDensity visualDensity;
final Widget icon;
final Widget input;
final Widget label;
final Widget hint;
final Widget prefix;
final Widget suffix;
final Widget prefixIcon;
final Widget suffixIcon;
final Widget helperError;
final Widget counter;
final Widget container;
@override
bool operator ==(Object other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
return other is _Decoration
&& other.contentPadding == contentPadding
&& other.isCollapsed == isCollapsed
&& other.floatingLabelHeight == floatingLabelHeight
&& other.floatingLabelProgress == floatingLabelProgress
&& other.border == border
&& other.borderGap == borderGap
&& other.alignLabelWithHint == alignLabelWithHint
&& other.isDense == isDense
&& other.visualDensity == visualDensity
&& other.icon == icon
&& other.input == input
&& other.label == label
&& other.hint == hint
&& other.prefix == prefix
&& other.suffix == suffix
&& other.prefixIcon == prefixIcon
&& other.suffixIcon == suffixIcon
&& other.helperError == helperError
&& other.counter == counter
&& other.container == container;
}
@override
int get hashCode {
return hashValues(
contentPadding,
floatingLabelHeight,
floatingLabelProgress,
border,
borderGap,
alignLabelWithHint,
isDense,
visualDensity,
icon,
input,
label,
hint,
prefix,
suffix,
prefixIcon,
suffixIcon,
helperError,
counter,
container,
);
}
}
// A container for the layout values computed by _RenderDecoration._layout.
// These values are used by _RenderDecoration.performLayout to position
// all of the renderer children of a _RenderDecoration.
class _RenderDecorationLayout {
const _RenderDecorationLayout({
this.boxToBaseline,
this.inputBaseline, // for InputBorderType.underline
this.outlineBaseline, // for InputBorderType.outline
this.subtextBaseline,
this.containerHeight,
this.subtextHeight,
});
final Map<RenderBox, double> boxToBaseline;
final double inputBaseline;
final double outlineBaseline;
final double subtextBaseline; // helper/error counter
final double containerHeight;
final double subtextHeight;
}
// The workhorse: layout and paint a _Decorator widget's _Decoration.
class _RenderDecoration extends RenderBox {
_RenderDecoration({
@required _Decoration decoration,
@required TextDirection textDirection,
@required TextBaseline textBaseline,
@required bool isFocused,
@required bool expands,
TextAlignVertical textAlignVertical,
}) : assert(decoration != null),
assert(textDirection != null),
assert(textBaseline != null),
assert(expands != null),
_decoration = decoration,
_textDirection = textDirection,
_textBaseline = textBaseline,
_textAlignVertical = textAlignVertical,
_isFocused = isFocused,
_expands = expands;
static const double subtextGap = 8.0;
final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{};
final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{};
RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _DecorationSlot slot) {
if (oldChild != null) {
dropChild(oldChild);
childToSlot.remove(oldChild);
slotToChild.remove(slot);
}
if (newChild != null) {
childToSlot[newChild] = slot;
slotToChild[slot] = newChild;
adoptChild(newChild);
}
return newChild;
}
RenderBox _icon;
RenderBox get icon => _icon;
set icon(RenderBox value) {
_icon = _updateChild(_icon, value, _DecorationSlot.icon);
}
RenderBox _input;
RenderBox get input => _input;
set input(RenderBox value) {
_input = _updateChild(_input, value, _DecorationSlot.input);
}
RenderBox _label;
RenderBox get label => _label;
set label(RenderBox value) {
_label = _updateChild(_label, value, _DecorationSlot.label);
}
RenderBox _hint;
RenderBox get hint => _hint;
set hint(RenderBox value) {
_hint = _updateChild(_hint, value, _DecorationSlot.hint);
}
RenderBox _prefix;
RenderBox get prefix => _prefix;
set prefix(RenderBox value) {
_prefix = _updateChild(_prefix, value, _DecorationSlot.prefix);
}
RenderBox _suffix;
RenderBox get suffix => _suffix;
set suffix(RenderBox value) {
_suffix = _updateChild(_suffix, value, _DecorationSlot.suffix);
}
RenderBox _prefixIcon;
RenderBox get prefixIcon => _prefixIcon;
set prefixIcon(RenderBox value) {
_prefixIcon = _updateChild(_prefixIcon, value, _DecorationSlot.prefixIcon);
}
RenderBox _suffixIcon;
RenderBox get suffixIcon => _suffixIcon;
set suffixIcon(RenderBox value) {
_suffixIcon = _updateChild(_suffixIcon, value, _DecorationSlot.suffixIcon);
}
RenderBox _helperError;
RenderBox get helperError => _helperError;
set helperError(RenderBox value) {
_helperError = _updateChild(_helperError, value, _DecorationSlot.helperError);
}
RenderBox _counter;
RenderBox get counter => _counter;
set counter(RenderBox value) {
_counter = _updateChild(_counter, value, _DecorationSlot.counter);
}
RenderBox _container;
RenderBox get container => _container;
set container(RenderBox value) {
_container = _updateChild(_container, value, _DecorationSlot.container);
}
// The returned list is ordered for hit testing.
Iterable<RenderBox> get _children sync* {
if (icon != null)
yield icon;
if (input != null)
yield input;
if (prefixIcon != null)
yield prefixIcon;
if (suffixIcon != null)
yield suffixIcon;
if (prefix != null)
yield prefix;
if (suffix != null)
yield suffix;
if (label != null)
yield label;
if (hint != null)
yield hint;
if (helperError != null)
yield helperError;
if (counter != null)
yield counter;
if (container != null)
yield container;
}
_Decoration get decoration => _decoration;
_Decoration _decoration;
set decoration(_Decoration value) {
assert(value != null);
if (_decoration == value)
return;
_decoration = value;
markNeedsLayout();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (_textDirection == value)
return;
_textDirection = value;
markNeedsLayout();
}
TextBaseline get textBaseline => _textBaseline;
TextBaseline _textBaseline;
set textBaseline(TextBaseline value) {
assert(value != null);
if (_textBaseline == value)
return;
_textBaseline = value;
markNeedsLayout();
}
TextAlignVertical get textAlignVertical {
if (_textAlignVertical == null) {
return _isOutlineAligned ? TextAlignVertical.center : TextAlignVertical.top;
}
return _textAlignVertical;
}
TextAlignVertical _textAlignVertical;
set textAlignVertical(TextAlignVertical value) {
assert(value != null);
if (_textAlignVertical == value) {
return;
}
// No need to relayout if the effective value is still the same.
if (textAlignVertical.y == value.y) {
_textAlignVertical = value;
return;
}
_textAlignVertical = value;
markNeedsLayout();
}
bool get isFocused => _isFocused;
bool _isFocused;
set isFocused(bool value) {
assert(value != null);
if (_isFocused == value)
return;
_isFocused = value;
markNeedsSemanticsUpdate();
}
bool get expands => _expands;
bool _expands = false;
set expands(bool value) {
assert(value != null);
if (_expands == value)
return;
_expands = value;
markNeedsLayout();
}
// Indicates that the decoration should be aligned to accommodate an outline
// border.
bool get _isOutlineAligned {
return !decoration.isCollapsed && decoration.border.isOutline;
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
for (final RenderBox child in _children)
child.attach(owner);
}
@override
void detach() {
super.detach();
for (final RenderBox child in _children)
child.detach();
}
@override
void redepthChildren() {
_children.forEach(redepthChild);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
_children.forEach(visitor);
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (icon != null)
visitor(icon);
if (prefix != null)
visitor(prefix);
if (prefixIcon != null)
visitor(prefixIcon);
if (label != null) {
visitor(label);
}
if (hint != null) {
if (isFocused) {
visitor(hint);
} else if (label == null) {
visitor(hint);
}
}
if (input != null)
visitor(input);
if (suffixIcon != null)
visitor(suffixIcon);
if (suffix != null)
visitor(suffix);
if (container != null)
visitor(container);
if (helperError != null)
visitor(helperError);
if (counter != null)
visitor(counter);
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
void add(RenderBox child, String name) {
if (child != null)
value.add(child.toDiagnosticsNode(name: name));
}
add(icon, 'icon');
add(input, 'input');
add(label, 'label');
add(hint, 'hint');
add(prefix, 'prefix');
add(suffix, 'suffix');
add(prefixIcon, 'prefixIcon');
add(suffixIcon, 'suffixIcon');
add(helperError, 'helperError');
add(counter, 'counter');
add(container, 'container');
return value;
}
@override
bool get sizedByParent => false;
static double _minWidth(RenderBox box, double height) {
return box == null ? 0.0 : box.getMinIntrinsicWidth(height);
}
static double _maxWidth(RenderBox box, double height) {
return box == null ? 0.0 : box.getMaxIntrinsicWidth(height);
}
static double _minHeight(RenderBox box, double width) {
return box == null ? 0.0 : box.getMinIntrinsicHeight(width);
}
static Size _boxSize(RenderBox box) => box == null ? Size.zero : box.size;
static BoxParentData _boxParentData(RenderBox box) => box.parentData as BoxParentData;
EdgeInsets get contentPadding => decoration.contentPadding as EdgeInsets;
// Lay out the given box if needed, and return its baseline.
double _layoutLineBox(RenderBox box, BoxConstraints constraints) {
if (box == null) {
return 0.0;
}
box.layout(constraints, parentUsesSize: true);
// Since internally, all layout is performed against the alphabetic baseline,
// (eg, ascents/descents are all relative to alphabetic, even if the font is
// an ideographic or hanging font), we should always obtain the reference
// baseline from the alphabetic baseline. The ideographic baseline is for
// use post-layout and is derived from the alphabetic baseline combined with
// the font metrics.
final double baseline = box.getDistanceToBaseline(TextBaseline.alphabetic);
assert(baseline != null && baseline >= 0.0);
return baseline;
}
// Returns a value used by performLayout to position all of the renderers.
// This method applies layout to all of the renderers except the container.
// For convenience, the container is laid out in performLayout().
_RenderDecorationLayout _layout(BoxConstraints layoutConstraints) {
assert(
layoutConstraints.maxWidth < double.infinity,
'An InputDecorator, which is typically created by a TextField, cannot '
'have an unbounded width.\n'
'This happens when the parent widget does not provide a finite width '
'constraint. For example, if the InputDecorator is contained by a Row, '
'then its width must be constrained. An Expanded widget or a SizedBox '
'can be used to constrain the width of the InputDecorator or the '
'TextField that contains it.',
);
// Margin on each side of subtext (counter and helperError)
final Map<RenderBox, double> boxToBaseline = <RenderBox, double>{};
final BoxConstraints boxConstraints = layoutConstraints.loosen();
// Layout all the widgets used by InputDecorator
boxToBaseline[prefix] = _layoutLineBox(prefix, boxConstraints);
boxToBaseline[suffix] = _layoutLineBox(suffix, boxConstraints);
boxToBaseline[icon] = _layoutLineBox(icon, boxConstraints);
boxToBaseline[prefixIcon] = _layoutLineBox(prefixIcon, boxConstraints);
boxToBaseline[suffixIcon] = _layoutLineBox(suffixIcon, boxConstraints);
final double inputWidth = math.max(0.0, constraints.maxWidth - (
_boxSize(icon).width
+ contentPadding.left
+ _boxSize(prefixIcon).width
+ _boxSize(prefix).width
+ _boxSize(suffix).width
+ _boxSize(suffixIcon).width
+ contentPadding.right));
boxToBaseline[label] = _layoutLineBox(
label,
boxConstraints.copyWith(maxWidth: inputWidth),
);
boxToBaseline[hint] = _layoutLineBox(
hint,
boxConstraints.copyWith(minWidth: inputWidth, maxWidth: inputWidth),
);
boxToBaseline[counter] = _layoutLineBox(counter, boxConstraints);
// The helper or error text can occupy the full width less the space
// occupied by the icon and counter.
boxToBaseline[helperError] = _layoutLineBox(
helperError,
boxConstraints.copyWith(
maxWidth: math.max(0.0, boxConstraints.maxWidth
- _boxSize(icon).width
- _boxSize(counter).width
- contentPadding.horizontal,
),
),
);
// The height of the input needs to accommodate label above and counter and
// helperError below, when they exist.
final double labelHeight = label == null
? 0
: decoration.floatingLabelHeight;
final double topHeight = decoration.border.isOutline
? math.max(labelHeight - boxToBaseline[label], 0)
: labelHeight;
final double counterHeight = counter == null
? 0
: boxToBaseline[counter] + subtextGap;
final bool helperErrorExists = helperError?.size != null
&& helperError.size.height > 0;
final double helperErrorHeight = !helperErrorExists
? 0
: helperError.size.height + subtextGap;
final double bottomHeight = math.max(
counterHeight,
helperErrorHeight,
);
boxToBaseline[input] = _layoutLineBox(
input,
boxConstraints.deflate(EdgeInsets.only(
top: contentPadding.top + topHeight,
bottom: contentPadding.bottom + bottomHeight,
)).copyWith(
minWidth: inputWidth,
maxWidth: inputWidth,
),
);
// The field can be occupied by a hint or by the input itself
final double hintHeight = hint == null ? 0 : hint.size.height;
final double inputDirectHeight = input == null ? 0 : input.size.height;
final double inputHeight = math.max(hintHeight, inputDirectHeight);
final double inputInternalBaseline = math.max(
boxToBaseline[input],
boxToBaseline[hint],
);
// Calculate the amount that prefix/suffix affects height above and below
// the input.
final double prefixHeight = prefix == null ? 0 : prefix.size.height;
final double suffixHeight = suffix == null ? 0 : suffix.size.height;
final double fixHeight = math.max(
boxToBaseline[prefix],
boxToBaseline[suffix],
);
final double fixAboveInput = math.max(0, fixHeight - inputInternalBaseline);
final double fixBelowBaseline = math.max(
prefixHeight - boxToBaseline[prefix],
suffixHeight - boxToBaseline[suffix],
);
final double fixBelowInput = math.max(
0,
fixBelowBaseline - (inputHeight - inputInternalBaseline),
);
// Calculate the height of the input text container.
final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment;
final double prefixIconHeight = prefixIcon == null ? 0 : prefixIcon.size.height;
final double suffixIconHeight = suffixIcon == null ? 0 : suffixIcon.size.height;
final double fixIconHeight = math.max(prefixIconHeight, suffixIconHeight);
final double contentHeight = math.max(
fixIconHeight,
topHeight
+ contentPadding.top
+ fixAboveInput
+ inputHeight
+ fixBelowInput
+ contentPadding.bottom
+ densityOffset.dy,
);
final double minContainerHeight = decoration.isDense || expands
? 0.0
: kMinInteractiveDimension + densityOffset.dy;
final double maxContainerHeight = boxConstraints.maxHeight - bottomHeight + densityOffset.dy;
final double containerHeight = expands
? maxContainerHeight
: math.min(math.max(contentHeight, minContainerHeight), maxContainerHeight);
// Ensure the text is vertically centered in cases where the content is
// shorter than kMinInteractiveDimension.
final double interactiveAdjustment = minContainerHeight > contentHeight
? (minContainerHeight - contentHeight) / 2.0
: 0.0;
// Try to consider the prefix/suffix as part of the text when aligning it.
// If the prefix/suffix overflows however, allow it to extend outside of the
// input and align the remaining part of the text and prefix/suffix.
final double overflow = math.max(0, contentHeight - maxContainerHeight);
// Map textAlignVertical from -1:1 to 0:1 so that it can be used to scale
// the baseline from its minimum to maximum values.
final double textAlignVerticalFactor = (textAlignVertical.y + 1.0) / 2.0;
// Adjust to try to fit top overflow inside the input on an inverse scale of
// textAlignVertical, so that top aligned text adjusts the most and bottom
// aligned text doesn't adjust at all.
final double baselineAdjustment = fixAboveInput - overflow * (1 - textAlignVerticalFactor);
// The baselines that will be used to draw the actual input text content.
final double topInputBaseline = contentPadding.top
+ topHeight
+ inputInternalBaseline
+ baselineAdjustment
+ interactiveAdjustment;
final double maxContentHeight = containerHeight
- contentPadding.top
- topHeight
- contentPadding.bottom;
final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput;
final double maxVerticalOffset = maxContentHeight - alignableHeight;
final double textAlignVerticalOffset = maxVerticalOffset * textAlignVerticalFactor;
final double inputBaseline = topInputBaseline + textAlignVerticalOffset + densityOffset.dy / 2.0;
// The three main alignments for the baseline when an outline is present are
//
// * top (-1.0): topmost point considering padding.
// * center (0.0): the absolute center of the input ignoring padding but
// accommodating the border and floating label.
// * bottom (1.0): bottommost point considering padding.
//
// That means that if the padding is uneven, center is not the exact
// midpoint of top and bottom. To account for this, the above center and
// below center alignments are interpolated independently.
final double outlineCenterBaseline = inputInternalBaseline
+ baselineAdjustment / 2.0
+ (containerHeight - (2.0 + inputHeight)) / 2.0;
final double outlineTopBaseline = topInputBaseline;
final double outlineBottomBaseline = topInputBaseline + maxVerticalOffset;
final double outlineBaseline = _interpolateThree(
outlineTopBaseline,
outlineCenterBaseline,
outlineBottomBaseline,
textAlignVertical,
);
// Find the positions of the text below the input when it exists.
double subtextCounterBaseline = 0;
double subtextHelperBaseline = 0;
double subtextCounterHeight = 0;
double subtextHelperHeight = 0;
if (counter != null) {
subtextCounterBaseline =
containerHeight + subtextGap + boxToBaseline[counter];
subtextCounterHeight = counter.size.height + subtextGap;
}
if (helperErrorExists) {
subtextHelperBaseline =
containerHeight + subtextGap + boxToBaseline[helperError];
subtextHelperHeight = helperErrorHeight;
}
final double subtextBaseline = math.max(
subtextCounterBaseline,
subtextHelperBaseline,
);
final double subtextHeight = math.max(
subtextCounterHeight,
subtextHelperHeight,
);
return _RenderDecorationLayout(
boxToBaseline: boxToBaseline,
containerHeight: containerHeight,
inputBaseline: inputBaseline,
outlineBaseline: outlineBaseline,
subtextBaseline: subtextBaseline,
subtextHeight: subtextHeight,
);
}
// Interpolate between three stops using textAlignVertical. This is used to
// calculate the outline baseline, which ignores padding when the alignment is
// middle. When the alignment is less than zero, it interpolates between the
// centered text box's top and the top of the content padding. When the
// alignment is greater than zero, it interpolates between the centered box's
// top and the position that would align the bottom of the box with the bottom
// padding.
double _interpolateThree(double begin, double middle, double end, TextAlignVertical textAlignVertical) {
if (textAlignVertical.y <= 0) {
// It's possible for begin, middle, and end to not be in order because of
// excessive padding. Those cases are handled by using middle.
if (begin >= middle) {
return middle;
}
// Do a standard linear interpolation on the first half, between begin and
// middle.
final double t = textAlignVertical.y + 1;
return begin + (middle - begin) * t;
}
if (middle >= end) {
return middle;
}
// Do a standard linear interpolation on the second half, between middle and
// end.
final double t = textAlignVertical.y;
return middle + (end - middle) * t;
}
@override
double computeMinIntrinsicWidth(double height) {
return _minWidth(icon, height)
+ contentPadding.left
+ _minWidth(prefixIcon, height)
+ _minWidth(prefix, height)
+ math.max(_minWidth(input, height), _minWidth(hint, height))
+ _minWidth(suffix, height)
+ _minWidth(suffixIcon, height)
+ contentPadding.right;
}
@override
double computeMaxIntrinsicWidth(double height) {
return _maxWidth(icon, height)
+ contentPadding.left
+ _maxWidth(prefixIcon, height)
+ _maxWidth(prefix, height)
+ math.max(_maxWidth(input, height), _maxWidth(hint, height))
+ _maxWidth(suffix, height)
+ _maxWidth(suffixIcon, height)
+ contentPadding.right;
}
double _lineHeight(double width, List<RenderBox> boxes) {
double height = 0.0;
for (final RenderBox box in boxes) {
if (box == null)
continue;
height = math.max(_minHeight(box, width), height);
}
return height;
// TODO(hansmuller): this should compute the overall line height for the
// boxes when they've been baseline-aligned.
// See https://github.com/flutter/flutter/issues/13715
}
@override
double computeMinIntrinsicHeight(double width) {
double subtextHeight = _lineHeight(width, <RenderBox>[helperError, counter]);
if (subtextHeight > 0.0)
subtextHeight += subtextGap;
return contentPadding.top
+ (label == null ? 0.0 : decoration.floatingLabelHeight)
+ _lineHeight(width, <RenderBox>[prefix, input, suffix])
+ subtextHeight
+ contentPadding.bottom;
}
@override
double computeMaxIntrinsicHeight(double width) {
return computeMinIntrinsicHeight(width);
}
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
return _boxParentData(input).offset.dy + input.computeDistanceToActualBaseline(baseline);
}
// Records where the label was painted.
Matrix4 _labelTransform;
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
_labelTransform = null;
final _RenderDecorationLayout layout = _layout(constraints);
final double overallWidth = constraints.maxWidth;
final double overallHeight = layout.containerHeight + layout.subtextHeight;
if (container != null) {
final BoxConstraints containerConstraints = BoxConstraints.tightFor(
height: layout.containerHeight,
width: overallWidth - _boxSize(icon).width,
);
container.layout(containerConstraints, parentUsesSize: true);
double x;
switch (textDirection) {
case TextDirection.rtl:
x = 0.0;
break;
case TextDirection.ltr:
x = _boxSize(icon).width;
break;
}
_boxParentData(container).offset = Offset(x, 0.0);
}
double height;
double centerLayout(RenderBox box, double x) {
_boxParentData(box).offset = Offset(x, (height - box.size.height) / 2.0);
return box.size.width;
}
double baseline;
double baselineLayout(RenderBox box, double x) {
_boxParentData(box).offset = Offset(x, baseline - layout.boxToBaseline[box]);
return box.size.width;
}
final double left = contentPadding.left;
final double right = overallWidth - contentPadding.right;
height = layout.containerHeight;
baseline = _isOutlineAligned ? layout.outlineBaseline : layout.inputBaseline;
if (icon != null) {
double x;
switch (textDirection) {
case TextDirection.rtl:
x = overallWidth - icon.size.width;
break;
case TextDirection.ltr:
x = 0.0;
break;
}
centerLayout(icon, x);
}
switch (textDirection) {
case TextDirection.rtl: {
double start = right - _boxSize(icon).width;
double end = left;
if (prefixIcon != null) {
start += contentPadding.left;
start -= centerLayout(prefixIcon, start - prefixIcon.size.width);
}
if (label != null) {
if (decoration.alignLabelWithHint) {
baselineLayout(label, start - label.size.width);
} else {
centerLayout(label, start - label.size.width);
}
}
if (prefix != null)
start -= baselineLayout(prefix, start - prefix.size.width);
if (input != null)
baselineLayout(input, start - input.size.width);
if (hint != null)
baselineLayout(hint, start - hint.size.width);
if (suffixIcon != null) {
end -= contentPadding.left;
end += centerLayout(suffixIcon, end);
}
if (suffix != null)
end += baselineLayout(suffix, end);
break;
}
case TextDirection.ltr: {
double start = left + _boxSize(icon).width;
double end = right;
if (prefixIcon != null) {
start -= contentPadding.left;
start += centerLayout(prefixIcon, start);
}
if (label != null) {
if (decoration.alignLabelWithHint) {
baselineLayout(label, start);
} else {
centerLayout(label, start);
}
}
if (prefix != null)
start += baselineLayout(prefix, start);
if (input != null)
baselineLayout(input, start);
if (hint != null)
baselineLayout(hint, start);
if (suffixIcon != null) {
end += contentPadding.right;
end -= centerLayout(suffixIcon, end - suffixIcon.size.width);
}
if (suffix != null)
end -= baselineLayout(suffix, end - suffix.size.width);
break;
}
}
if (helperError != null || counter != null) {
height = layout.subtextHeight;
baseline = layout.subtextBaseline;
switch (textDirection) {
case TextDirection.rtl:
if (helperError != null)
baselineLayout(helperError, right - helperError.size.width - _boxSize(icon).width);
if (counter != null)
baselineLayout(counter, left);
break;
case TextDirection.ltr:
if (helperError != null)
baselineLayout(helperError, left + _boxSize(icon).width);
if (counter != null)
baselineLayout(counter, right - counter.size.width);
break;
}
}
if (label != null) {
final double labelX = _boxParentData(label).offset.dx;
switch (textDirection) {
case TextDirection.rtl:
decoration.borderGap.start = labelX + label.size.width;
break;
case TextDirection.ltr:
// The value of _InputBorderGap.start is relative to the origin of the
// _BorderContainer which is inset by the icon's width.
decoration.borderGap.start = labelX - _boxSize(icon).width;
break;
}
decoration.borderGap.extent = label.size.width * 0.75;
} else {
decoration.borderGap.start = null;
decoration.borderGap.extent = 0.0;
}
size = constraints.constrain(Size(overallWidth, overallHeight));
assert(size.width == constraints.constrainWidth(overallWidth));
assert(size.height == constraints.constrainHeight(overallHeight));
}
void _paintLabel(PaintingContext context, Offset offset) {
context.paintChild(label, offset);
}
@override
void paint(PaintingContext context, Offset offset) {
void doPaint(RenderBox child) {
if (child != null)
context.paintChild(child, _boxParentData(child).offset + offset);
}
doPaint(container);
if (label != null) {
final Offset labelOffset = _boxParentData(label).offset;
final double labelHeight = label.size.height;
final double t = decoration.floatingLabelProgress;
// The center of the outline border label ends up a little below the
// center of the top border line.
final bool isOutlineBorder = decoration.border != null && decoration.border.isOutline;
final double floatingY = isOutlineBorder ? -labelHeight * 0.25 : contentPadding.top;
final double scale = lerpDouble(1.0, 0.75, t);
double dx;
switch (textDirection) {
case TextDirection.rtl:
dx = labelOffset.dx + label.size.width * (1.0 - scale); // origin is on the right
break;
case TextDirection.ltr:
dx = labelOffset.dx; // origin on the left
break;
}
final double dy = lerpDouble(0.0, floatingY - labelOffset.dy, t);
_labelTransform = Matrix4.identity()
..translate(dx, labelOffset.dy + dy)
..scale(scale);
context.pushTransform(needsCompositing, offset, _labelTransform, _paintLabel);
}
doPaint(icon);
doPaint(prefix);
doPaint(suffix);
doPaint(prefixIcon);
doPaint(suffixIcon);
doPaint(hint);
doPaint(input);
doPaint(helperError);
doPaint(counter);
}
@override
bool hitTestSelf(Offset position) => true;
@override
bool hitTestChildren(BoxHitTestResult result, { @required Offset position }) {
assert(position != null);
for (final RenderBox child in _children) {
// TODO(hansmuller): label must be handled specially since we've transformed it
final Offset offset = _boxParentData(child).offset;
final bool isHit = result.addWithPaintOffset(
offset: offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - offset);
return child.hitTest(result, position: transformed);
},
);
if (isHit)
return true;
}
return false;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
if (child == label && _labelTransform != null) {
final Offset labelOffset = _boxParentData(label).offset;
transform
..multiply(_labelTransform)
..translate(-labelOffset.dx, -labelOffset.dy);
}
super.applyPaintTransform(child, transform);
}
}
class _RenderDecorationElement extends RenderObjectElement {
_RenderDecorationElement(_Decorator widget) : super(widget);
final Map<_DecorationSlot, Element> slotToChild = <_DecorationSlot, Element>{};
final Map<Element, _DecorationSlot> childToSlot = <Element, _DecorationSlot>{};
@override
_Decorator get widget => super.widget as _Decorator;
@override
_RenderDecoration get renderObject => super.renderObject as _RenderDecoration;
@override
void visitChildren(ElementVisitor visitor) {
slotToChild.values.forEach(visitor);
}
@override
void forgetChild(Element child) {
assert(slotToChild.values.contains(child));
assert(childToSlot.keys.contains(child));
final _DecorationSlot slot = childToSlot[child];
childToSlot.remove(child);
slotToChild.remove(slot);
super.forgetChild(child);
}
void _mountChild(Widget widget, _DecorationSlot slot) {
final Element oldChild = slotToChild[slot];
final Element newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) {
slotToChild.remove(slot);
childToSlot.remove(oldChild);
}
if (newChild != null) {
slotToChild[slot] = newChild;
childToSlot[newChild] = slot;
}
}
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_mountChild(widget.decoration.icon, _DecorationSlot.icon);
_mountChild(widget.decoration.input, _DecorationSlot.input);
_mountChild(widget.decoration.label, _DecorationSlot.label);
_mountChild(widget.decoration.hint, _DecorationSlot.hint);
_mountChild(widget.decoration.prefix, _DecorationSlot.prefix);
_mountChild(widget.decoration.suffix, _DecorationSlot.suffix);
_mountChild(widget.decoration.prefixIcon, _DecorationSlot.prefixIcon);
_mountChild(widget.decoration.suffixIcon, _DecorationSlot.suffixIcon);
_mountChild(widget.decoration.helperError, _DecorationSlot.helperError);
_mountChild(widget.decoration.counter, _DecorationSlot.counter);
_mountChild(widget.decoration.container, _DecorationSlot.container);
}
void _updateChild(Widget widget, _DecorationSlot slot) {
final Element oldChild = slotToChild[slot];
final Element newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) {
childToSlot.remove(oldChild);
slotToChild.remove(slot);
}
if (newChild != null) {
slotToChild[slot] = newChild;
childToSlot[newChild] = slot;
}
}
@override
void update(_Decorator newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_updateChild(widget.decoration.icon, _DecorationSlot.icon);
_updateChild(widget.decoration.input, _DecorationSlot.input);
_updateChild(widget.decoration.label, _DecorationSlot.label);
_updateChild(widget.decoration.hint, _DecorationSlot.hint);
_updateChild(widget.decoration.prefix, _DecorationSlot.prefix);
_updateChild(widget.decoration.suffix, _DecorationSlot.suffix);
_updateChild(widget.decoration.prefixIcon, _DecorationSlot.prefixIcon);
_updateChild(widget.decoration.suffixIcon, _DecorationSlot.suffixIcon);
_updateChild(widget.decoration.helperError, _DecorationSlot.helperError);
_updateChild(widget.decoration.counter, _DecorationSlot.counter);
_updateChild(widget.decoration.container, _DecorationSlot.container);
}
void _updateRenderObject(RenderBox child, _DecorationSlot slot) {
switch (slot) {
case _DecorationSlot.icon:
renderObject.icon = child;
break;
case _DecorationSlot.input:
renderObject.input = child;
break;
case _DecorationSlot.label:
renderObject.label = child;
break;
case _DecorationSlot.hint:
renderObject.hint = child;
break;
case _DecorationSlot.prefix:
renderObject.prefix = child;
break;
case _DecorationSlot.suffix:
renderObject.suffix = child;
break;
case _DecorationSlot.prefixIcon:
renderObject.prefixIcon = child;
break;
case _DecorationSlot.suffixIcon:
renderObject.suffixIcon = child;
break;
case _DecorationSlot.helperError:
renderObject.helperError = child;
break;
case _DecorationSlot.counter:
renderObject.counter = child;
break;
case _DecorationSlot.container:
renderObject.container = child;
break;
}
}
@override
void insertChildRenderObject(RenderObject child, dynamic slotValue) {
assert(child is RenderBox);
assert(slotValue is _DecorationSlot);
final _DecorationSlot slot = slotValue as _DecorationSlot;
_updateRenderObject(child as RenderBox, slot);
assert(renderObject.childToSlot.keys.contains(child));
assert(renderObject.slotToChild.keys.contains(slot));
}
@override
void removeChildRenderObject(RenderObject child) {
assert(child is RenderBox);
assert(renderObject.childToSlot.keys.contains(child));
_updateRenderObject(null, renderObject.childToSlot[child]);
assert(!renderObject.childToSlot.keys.contains(child));
assert(!renderObject.slotToChild.keys.contains(slot));
}
@override
void moveChildRenderObject(RenderObject child, dynamic slotValue) {
assert(false, 'not reachable');
}
}
class _Decorator extends RenderObjectWidget {
const _Decorator({
Key key,
@required this.textAlignVertical,
@required this.decoration,
@required this.textDirection,
@required this.textBaseline,
@required this.isFocused,
@required this.expands,
}) : assert(decoration != null),
assert(textDirection != null),
assert(textBaseline != null),
assert(expands != null),
super(key: key);
final _Decoration decoration;
final TextDirection textDirection;
final TextBaseline textBaseline;
final TextAlignVertical textAlignVertical;
final bool isFocused;
final bool expands;
@override
_RenderDecorationElement createElement() => _RenderDecorationElement(this);
@override
_RenderDecoration createRenderObject(BuildContext context) {
return _RenderDecoration(
decoration: decoration,
textDirection: textDirection,
textBaseline: textBaseline,
textAlignVertical: textAlignVertical,
isFocused: isFocused,
expands: expands,
);
}
@override
void updateRenderObject(BuildContext context, _RenderDecoration renderObject) {
renderObject
..decoration = decoration
..textDirection = textDirection
..textBaseline = textBaseline
..expands = expands
..isFocused = isFocused;
}
}
class _AffixText extends StatelessWidget {
const _AffixText({
this.labelIsFloating,
this.text,
this.style,
this.child,
});
final bool labelIsFloating;
final String text;
final TextStyle style;
final Widget child;
@override
Widget build(BuildContext context) {
return DefaultTextStyle.merge(
style: style,
child: AnimatedOpacity(
duration: _kTransitionDuration,
curve: _kTransitionCurve,
opacity: labelIsFloating ? 1.0 : 0.0,
child: child ?? Text(text, style: style,),
),
);
}
}
/// Defines the appearance of a Material Design text field.
///
/// [InputDecorator] displays the visual elements of a Material Design text
/// field around its input [child]. The visual elements themselves are defined
/// by an [InputDecoration] object and their layout and appearance depend
/// on the `baseStyle`, `textAlign`, `isFocused`, and `isEmpty` parameters.
///
/// [TextField] uses this widget to decorate its [EditableText] child.
///
/// [InputDecorator] can be used to create widgets that look and behave like a
/// [TextField] but support other kinds of input.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [TextField], which uses an [InputDecorator] to display a border,
/// labels, and icons, around its [EditableText] child.
/// * [Decoration] and [DecoratedBox], for drawing arbitrary decorations
/// around other widgets.
class InputDecorator extends StatefulWidget {
/// Creates a widget that displays a border, labels, and icons,
/// for a [TextField].
///
/// The [isFocused], [isHovering], [expands], and [isEmpty] arguments must not
/// be null.
const InputDecorator({
Key key,
this.decoration,
this.baseStyle,
this.textAlign,
this.textAlignVertical,
this.isFocused = false,
this.isHovering = false,
this.expands = false,
this.isEmpty = false,
this.child,
}) : assert(isFocused != null),
assert(isHovering != null),
assert(expands != null),
assert(isEmpty != null),
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, `baseStyle` defaults to the `subtitle1` style from the
/// current [Theme], see [ThemeData.textTheme].
///
/// The [TextStyle.textBaseline] of the [baseStyle] is used to determine
/// the baseline used for text alignment.
final TextStyle baseStyle;
/// How the text in the decoration should be aligned horizontally.
final TextAlign textAlign;
/// {@template flutter.widgets.inputDecorator.textAlignVertical}
/// How the text should be aligned vertically.
///
/// Determines the alignment of the baseline within the available space of
/// the input (typically a TextField). For example, TextAlignVertical.top will
/// place the baseline such that the text, and any attached decoration like
/// prefix and suffix, is as close to the top of the input as possible without
/// overflowing. The heights of the prefix and suffix are similarly included
/// for other alignment values. If the height is greater than the height
/// available, then the prefix and suffix will be allowed to overflow first
/// before the text scrolls.
/// {@endtemplate}
final TextAlignVertical textAlignVertical;
/// Whether the input field has focus.
///
/// Determines the position of the label text and the color and weight of the
/// border, as well as the container fill color, which is a blend of
/// [InputDecoration.focusColor] with [InputDecoration.fillColor] when
/// focused, and [InputDecoration.fillColor] when not.
///
/// Defaults to false.
///
/// See also:
///
/// * [InputDecoration.hoverColor], which is also blended into the focus
/// color and fill color when the [isHovering] is true to produce the final
/// color.
final bool isFocused;
/// Whether the input field is being hovered over by a mouse pointer.
///
/// Determines the container fill color, which is a blend of
/// [InputDecoration.hoverColor] with [InputDecoration.fillColor] when
/// true, and [InputDecoration.fillColor] when not.
///
/// Defaults to false.
///
/// See also:
///
/// * [InputDecoration.focusColor], which is also blended into the hover
/// color and fill color when [isFocused] is true to produce the final
/// color.
final bool isHovering;
/// If true, the height of the input field will be as large as possible.
///
/// If wrapped in a widget that constrains its child's height, like Expanded
/// or SizedBox, the input field will only be affected if [expands] is set to
/// true.
///
/// See [TextField.minLines] and [TextField.maxLines] for related ways to
/// affect the height of an input. When [expands] is true, both must be null
/// in order to avoid ambiguity in determining the height.
///
/// Defaults to false.
final bool expands;
/// Whether the input field is empty.
///
/// Determines the position of the label text and whether to display the hint
/// text.
///
/// Defaults to false.
final bool isEmpty;
/// The widget below this widget in the tree.
///
/// Typically an [EditableText], [DropdownButton], or [InkWell].
final Widget child;
/// Whether the label needs to get out of the way of the input, either by
/// floating or disappearing.
bool get _labelShouldWithdraw => !isEmpty || isFocused;
@override
_InputDecoratorState createState() => _InputDecoratorState();
/// The RenderBox that defines this decorator's "container". That's the
/// area which is filled if [InputDecoration.filled] is true. It's the area
/// adjacent to [InputDecoration.icon] and above the widgets that contain
/// [InputDecoration.helperText], [InputDecoration.errorText], and
/// [InputDecoration.counterText].
///
/// [TextField] renders ink splashes within the container.
static RenderBox containerOf(BuildContext context) {
final _RenderDecoration result = context.findAncestorRenderObjectOfType<_RenderDecoration>();
return result?.container;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration));
properties.add(DiagnosticsProperty<TextStyle>('baseStyle', baseStyle, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('isFocused', isFocused));
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('isEmpty', isEmpty));
}
}
class _InputDecoratorState extends State<InputDecorator> with TickerProviderStateMixin {
AnimationController _floatingLabelController;
AnimationController _shakingLabelController;
final _InputBorderGap _borderGap = _InputBorderGap();
@override
void initState() {
super.initState();
final bool labelIsInitiallyFloating = widget.decoration.floatingLabelBehavior == FloatingLabelBehavior.always
// ignore: deprecated_member_use_from_same_package
|| (widget.decoration.hasFloatingPlaceholder && widget._labelShouldWithdraw);
_floatingLabelController = AnimationController(
duration: _kTransitionDuration,
vsync: this,
value: labelIsInitiallyFloating ? 1.0 : 0.0
);
_floatingLabelController.addListener(_handleChange);
_shakingLabelController = AnimationController(
duration: _kTransitionDuration,
vsync: this,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_effectiveDecoration = null;
}
@override
void dispose() {
_floatingLabelController.dispose();
_shakingLabelController.dispose();
super.dispose();
}
void _handleChange() {
setState(() {
// The _floatingLabelController's value has changed.
});
}
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 && decoration.enabled;
bool get isHovering => widget.isHovering && decoration.enabled;
bool get isEmpty => widget.isEmpty;
bool get _floatingLabelEnabled {
// ignore: deprecated_member_use_from_same_package
return decoration.hasFloatingPlaceholder && decoration.floatingLabelBehavior != FloatingLabelBehavior.never;
}
@override
void didUpdateWidget(InputDecorator old) {
super.didUpdateWidget(old);
if (widget.decoration != old.decoration)
_effectiveDecoration = null;
final bool floatBehaviourChanged = widget.decoration.floatingLabelBehavior != old.decoration.floatingLabelBehavior
// ignore: deprecated_member_use_from_same_package
|| widget.decoration.hasFloatingPlaceholder != old.decoration.hasFloatingPlaceholder;
if (widget._labelShouldWithdraw != old._labelShouldWithdraw || floatBehaviourChanged) {
if (_floatingLabelEnabled
&& (widget._labelShouldWithdraw || widget.decoration.floatingLabelBehavior == FloatingLabelBehavior.always))
_floatingLabelController.forward();
else
_floatingLabelController.reverse();
}
final String errorText = decoration.errorText;
final String oldErrorText = old.decoration.errorText;
if (_floatingLabelController.isCompleted && errorText != null && errorText != oldErrorText) {
_shakingLabelController
..value = 0.0
..forward();
}
}
Color _getActiveColor(ThemeData themeData) {
if (isFocused) {
switch (themeData.brightness) {
case Brightness.dark:
return themeData.accentColor;
case Brightness.light:
return themeData.primaryColor;
}
}
return themeData.hintColor;
}
Color _getDefaultBorderColor(ThemeData themeData) {
if (isFocused) {
switch (themeData.brightness) {
case Brightness.dark:
return themeData.accentColor;
case Brightness.light:
return themeData.primaryColor;
}
}
if (decoration.filled) {
return themeData.hintColor;
}
final Color enabledColor = themeData.colorScheme.onSurface.withOpacity(0.38);
if (isHovering) {
final Color hoverColor = decoration.hoverColor ?? themeData.inputDecorationTheme?.hoverColor ?? themeData.hoverColor;
return Color.alphaBlend(hoverColor.withOpacity(0.12), enabledColor);
}
return enabledColor;
}
Color _getFillColor(ThemeData themeData) {
if (decoration.filled != true) // filled == null same as filled == false
return Colors.transparent;
if (decoration.fillColor != null)
return decoration.fillColor;
// dark theme: 10% white (enabled), 5% white (disabled)
// light theme: 4% black (enabled), 2% black (disabled)
const Color darkEnabled = Color(0x1AFFFFFF);
const Color darkDisabled = Color(0x0DFFFFFF);
const Color lightEnabled = Color(0x0A000000);
const Color lightDisabled = Color(0x05000000);
switch (themeData.brightness) {
case Brightness.dark:
return decoration.enabled ? darkEnabled : darkDisabled;
case Brightness.light:
return decoration.enabled ? lightEnabled : lightDisabled;
}
return lightEnabled;
}
Color _getHoverColor(ThemeData themeData) {
if (decoration.filled == null || !decoration.filled || isFocused || !decoration.enabled)
return Colors.transparent;
return decoration.hoverColor ?? themeData.inputDecorationTheme?.hoverColor ?? themeData.hoverColor;
}
Color _getDefaultIconColor(ThemeData themeData) {
if (!decoration.enabled)
return themeData.disabledColor;
switch (themeData.brightness) {
case Brightness.dark:
return Colors.white70;
case Brightness.light:
return Colors.black45;
default:
return themeData.iconTheme.color;
}
}
// True if the label will be shown and the hint will not.
// If we're not focused, there's no value, and labelText was provided,
// then the label appears where the hint would.
bool get _hasInlineLabel => !widget._labelShouldWithdraw && decoration.labelText != null;
// If the label is a floating placeholder, it's always shown.
bool get _shouldShowLabel => _hasInlineLabel || _floatingLabelEnabled;
// 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.subtitle1.merge(widget.baseStyle)
.copyWith(color: decoration.enabled ? themeData.hintColor : themeData.disabledColor);
}
TextStyle _getFloatingLabelStyle(ThemeData themeData) {
final Color color = decoration.errorText != null
? decoration.errorStyle?.color ?? themeData.errorColor
: _getActiveColor(themeData);
final TextStyle style = themeData.textTheme.subtitle1.merge(widget.baseStyle);
return style
.copyWith(color: decoration.enabled ? color : themeData.disabledColor)
.merge(decoration.labelStyle);
}
TextStyle _getHelperStyle(ThemeData themeData) {
final Color color = decoration.enabled ? themeData.hintColor : Colors.transparent;
return themeData.textTheme.caption.copyWith(color: color).merge(decoration.helperStyle);
}
TextStyle _getErrorStyle(ThemeData themeData) {
final Color color = decoration.enabled ? themeData.errorColor : Colors.transparent;
return themeData.textTheme.caption.copyWith(color: color).merge(decoration.errorStyle);
}
InputBorder _getDefaultBorder(ThemeData themeData) {
if (decoration.border?.borderSide == BorderSide.none) {
return decoration.border;
}
Color borderColor;
if (decoration.enabled) {
borderColor = decoration.errorText == null
? _getDefaultBorderColor(themeData)
: themeData.errorColor;
} else {
borderColor = (decoration.filled == true && decoration.border?.isOutline != true)
? Colors.transparent
: themeData.disabledColor;
}
double borderWeight;
if (decoration.isCollapsed || decoration?.border == InputBorder.none || !decoration.enabled)
borderWeight = 0.0;
else
borderWeight = isFocused ? 2.0 : 1.0;
final InputBorder border = decoration.border ?? const UnderlineInputBorder();
return border.copyWith(borderSide: BorderSide(color: borderColor, width: borderWeight));
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TextStyle inlineStyle = _getInlineStyle(themeData);
final TextBaseline textBaseline = inlineStyle.textBaseline;
final TextStyle hintStyle = inlineStyle.merge(decoration.hintStyle);
final Widget hint = decoration.hintText == null ? null : AnimatedOpacity(
opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
alwaysIncludeSemantics: true,
child: Text(
decoration.hintText,
style: hintStyle,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
maxLines: decoration.hintMaxLines,
),
);
final bool isError = decoration.errorText != null;
InputBorder border;
if (!decoration.enabled)
border = isError ? decoration.errorBorder : decoration.disabledBorder;
else if (isFocused)
border = isError ? decoration.focusedErrorBorder : decoration.focusedBorder;
else
border = isError ? decoration.errorBorder : decoration.enabledBorder;
border ??= _getDefaultBorder(themeData);
final Widget container = _BorderContainer(
border: border,
gap: _borderGap,
gapAnimation: _floatingLabelController.view,
fillColor: _getFillColor(themeData),
hoverColor: _getHoverColor(themeData),
isHovering: isHovering,
);
final TextStyle inlineLabelStyle = inlineStyle.merge(decoration.labelStyle);
final Widget label = decoration.labelText == null ? null : _Shaker(
animation: _shakingLabelController.view,
child: AnimatedOpacity(
duration: _kTransitionDuration,
curve: _kTransitionCurve,
opacity: _shouldShowLabel ? 1.0 : 0.0,
child: AnimatedDefaultTextStyle(
duration:_kTransitionDuration,
curve: _kTransitionCurve,
style: widget._labelShouldWithdraw
? _getFloatingLabelStyle(themeData)
: inlineLabelStyle,
child: Text(
decoration.labelText,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
),
),
),
);
final Widget prefix = decoration.prefix == null && decoration.prefixText == null ? null :
_AffixText(
labelIsFloating: widget._labelShouldWithdraw,
text: decoration.prefixText,
style: decoration.prefixStyle ?? hintStyle,
child: decoration.prefix,
);
final Widget suffix = decoration.suffix == null && decoration.suffixText == null ? null :
_AffixText(
labelIsFloating: widget._labelShouldWithdraw,
text: decoration.suffixText,
style: decoration.suffixStyle ?? hintStyle,
child: decoration.suffix,
);
final Color activeColor = _getActiveColor(themeData);
final bool decorationIsDense = decoration.isDense == true; // isDense == null, same as false
final double iconSize = decorationIsDense ? 18.0 : 24.0;
final Color iconColor = isFocused ? activeColor : _getDefaultIconColor(themeData);
final Widget icon = decoration.icon == null ? null :
Padding(
padding: const EdgeInsetsDirectional.only(end: 16.0),
child: IconTheme.merge(
data: IconThemeData(
color: iconColor,
size: iconSize,
),
child: decoration.icon,
),
);
final Widget prefixIcon = decoration.prefixIcon == null ? null :
Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: ConstrainedBox(
constraints: decoration.prefixIconConstraints ?? themeData.visualDensity.effectiveConstraints(
const BoxConstraints(
minWidth: kMinInteractiveDimension,
minHeight: kMinInteractiveDimension,
),
),
child: IconTheme.merge(
data: IconThemeData(
color: iconColor,
size: iconSize,
),
child: decoration.prefixIcon,
),
),
);
final Widget suffixIcon = decoration.suffixIcon == null ? null :
Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: ConstrainedBox(
constraints: decoration.suffixIconConstraints ?? themeData.visualDensity.effectiveConstraints(
const BoxConstraints(
minWidth: kMinInteractiveDimension,
minHeight: kMinInteractiveDimension,
),
),
child: IconTheme.merge(
data: IconThemeData(
color: iconColor,
size: iconSize,
),
child: decoration.suffixIcon,
),
),
);
final Widget helperError = _HelperError(
textAlign: textAlign,
helperText: decoration.helperText,
helperStyle: _getHelperStyle(themeData),
helperMaxLines: decoration.helperMaxLines,
errorText: decoration.errorText,
errorStyle: _getErrorStyle(themeData),
errorMaxLines: decoration.errorMaxLines,
);
Widget counter;
if (decoration.counter != null) {
counter = decoration.counter;
} else if (decoration.counterText != null && decoration.counterText != '') {
counter = Semantics(
container: true,
liveRegion: isFocused,
child: Text(
decoration.counterText,
style: _getHelperStyle(themeData).merge(decoration.counterStyle),
overflow: TextOverflow.ellipsis,
semanticsLabel: decoration.semanticCounterText,
),
);
}
// The _Decoration widget and _RenderDecoration assume that contentPadding
// has been resolved to EdgeInsets.
final TextDirection textDirection = Directionality.of(context);
final EdgeInsets decorationContentPadding = decoration.contentPadding?.resolve(textDirection);
EdgeInsets contentPadding;
double floatingLabelHeight;
if (decoration.isCollapsed) {
floatingLabelHeight = 0.0;
contentPadding = decorationContentPadding ?? EdgeInsets.zero;
} else if (!border.isOutline) {
// 4.0: the vertical gap between the inline elements and the floating label.
floatingLabelHeight = (4.0 + 0.75 * inlineLabelStyle.fontSize) * MediaQuery.textScaleFactorOf(context);
if (decoration.filled == true) { // filled == null same as filled == false
contentPadding = decorationContentPadding ?? (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 = decorationContentPadding ?? (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 = decorationContentPadding ?? (decorationIsDense
? const EdgeInsets.fromLTRB(12.0, 20.0, 12.0, 12.0)
: const EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 16.0));
}
return _Decorator(
decoration: _Decoration(
contentPadding: contentPadding,
isCollapsed: decoration.isCollapsed,
floatingLabelHeight: floatingLabelHeight,
floatingLabelProgress: _floatingLabelController.value,
border: border,
borderGap: _borderGap,
alignLabelWithHint: decoration.alignLabelWithHint,
isDense: decoration.isDense,
visualDensity: themeData.visualDensity,
icon: icon,
input: widget.child,
label: label,
hint: hint,
prefix: prefix,
suffix: suffix,
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
helperError: helperError,
counter: counter,
container: container,
),
textDirection: textDirection,
textBaseline: textBaseline,
textAlignVertical: widget.textAlignVertical,
isFocused: isFocused,
expands: widget.expands,
);
}
}
/// The border, labels, icons, and styles used to decorate a Material
/// Design text field.
///
/// The [TextField] and [InputDecorator] classes use [InputDecoration] objects
/// to describe their decoration. (In fact, this class is merely the
/// configuration of an [InputDecorator], which does all the heavy lifting.)
///
/// {@tool dartpad --template=stateless_widget_scaffold}
///
/// This sample shows how to style a `TextField` using an `InputDecorator`. The
/// TextField displays a "send message" icon to the left of the input area,
/// which is surrounded by a border an all sides. It displays the `hintText`
/// inside the input area to help the user understand what input is required. It
/// displays the `helperText` and `counterText` below the input area.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/input_decoration.png)
///
/// ```dart
/// Widget build(BuildContext context) {
/// return TextField(
/// decoration: InputDecoration(
/// icon: Icon(Icons.send),
/// hintText: 'Hint Text',
/// helperText: 'Helper Text',
/// counterText: '0 characters',
/// border: const OutlineInputBorder(),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@tool dartpad --template=stateless_widget_scaffold}
///
/// This sample shows how to style a "collapsed" `TextField` using an
/// `InputDecorator`. The collapsed `TextField` surrounds the hint text and
/// input area with a border, but does not add padding around them.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/input_decoration_collapsed.png)
///
/// ```dart
/// Widget build(BuildContext context) {
/// return TextField(
/// decoration: InputDecoration.collapsed(
/// hintText: 'Hint Text',
/// border: OutlineInputBorder(),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@tool dartpad --template=stateless_widget_scaffold}
///
/// This sample shows how to create a `TextField` with hint text, a red border
/// on all sides, and an error message. To display a red border and error
/// message, provide `errorText` to the `InputDecoration` constructor.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/input_decoration_error.png)
///
/// ```dart
/// Widget build(BuildContext context) {
/// return TextField(
/// decoration: InputDecoration(
/// hintText: 'Hint Text',
/// errorText: 'Error Text',
/// border: OutlineInputBorder(),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@tool dartpad --template=stateless_widget_scaffold}
///
/// This sample shows how to style a `TextField` with a round border and
/// additional text before and after the input area. It displays "Prefix" before
/// the input area, and "Suffix" after the input area.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/input_decoration_prefix_suffix.png)
///
/// ```dart
/// Widget build(BuildContext context) {
/// return TextFormField(
/// initialValue: 'abc',
/// decoration: const InputDecoration(
/// prefix: Text('Prefix'),
/// suffix: Text('Suffix'),
/// border: OutlineInputBorder(),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [TextField], which is a text input widget that uses an
/// [InputDecoration].
/// * [InputDecorator], which is a widget that draws an [InputDecoration]
/// around an input child widget.
/// * [Decoration] and [DecoratedBox], for drawing borders and backgrounds
/// around a child widget.
@immutable
class InputDecoration {
/// Creates a bundle of the border, labels, icons, and styles used to
/// decorate a Material Design text field.
///
/// Unless specified by [ThemeData.inputDecorationTheme],
/// [InputDecorator] defaults [isDense] to false, and [filled] to false,
/// and [maxLines] to 1. 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.
///
/// Only one of [prefix] and [prefixText] can be specified.
///
/// Similarly, only one of [suffix] and [suffixText] can be specified.
const InputDecoration({
this.icon,
this.labelText,
this.labelStyle,
this.helperText,
this.helperStyle,
this.helperMaxLines,
this.hintText,
this.hintStyle,
this.hintMaxLines,
this.errorText,
this.errorStyle,
this.errorMaxLines,
@Deprecated(
'Use floatingLabelBehaviour instead. '
'This feature was deprecated after v1.13.2.'
)
this.hasFloatingPlaceholder = true, // ignore: deprecated_member_use_from_same_package
this.floatingLabelBehavior = FloatingLabelBehavior.auto,
this.isDense,
this.contentPadding,
this.prefixIcon,
this.prefixIconConstraints,
this.prefix,
this.prefixText,
this.prefixStyle,
this.suffixIcon,
this.suffix,
this.suffixText,
this.suffixStyle,
this.suffixIconConstraints,
this.counter,
this.counterText,
this.counterStyle,
this.filled,
this.fillColor,
this.focusColor,
this.hoverColor,
this.errorBorder,
this.focusedBorder,
this.focusedErrorBorder,
this.disabledBorder,
this.enabledBorder,
this.border,
this.enabled = true,
this.semanticCounterText,
this.alignLabelWithHint,
}) : assert(enabled != null),
assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not supported.'),
assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not supported.'),
isCollapsed = false;
/// Defines an [InputDecorator] that is the same size as the input field.
///
/// This type of input decoration does not include a border by default.
///
/// Sets the [isCollapsed] property to true.
const InputDecoration.collapsed({
@required this.hintText,
@Deprecated(
'Use floatingLabelBehaviour instead. '
'This feature was deprecated after v1.13.2.'
)
// ignore: deprecated_member_use_from_same_package
this.hasFloatingPlaceholder = true,
this.floatingLabelBehavior = FloatingLabelBehavior.auto,
this.hintStyle,
this.filled = false,
this.fillColor,
this.focusColor,
this.hoverColor,
this.border = InputBorder.none,
this.enabled = true,
}) : assert(enabled != null),
// ignore: deprecated_member_use_from_same_package
assert(!(!hasFloatingPlaceholder && identical(floatingLabelBehavior, FloatingLabelBehavior.always)),
'hasFloatingPlaceholder=false conflicts with FloatingLabelBehavior.always'),
icon = null,
labelText = null,
labelStyle = null,
helperText = null,
helperStyle = null,
helperMaxLines = null,
hintMaxLines = null,
errorText = null,
errorStyle = null,
errorMaxLines = null,
isDense = false,
contentPadding = EdgeInsets.zero,
isCollapsed = true,
prefixIcon = null,
prefix = null,
prefixText = null,
prefixStyle = null,
prefixIconConstraints = null,
suffix = null,
suffixIcon = null,
suffixText = null,
suffixStyle = null,
suffixIconConstraints = null,
counter = null,
counterText = null,
counterStyle = null,
errorBorder = null,
focusedBorder = null,
focusedErrorBorder = null,
disabledBorder = null,
enabledBorder = null,
semanticCounterText = null,
alignLabelWithHint = false;
/// An icon to show before the input field and outside of the decoration's
/// container.
///
/// The size and color of the icon is configured automatically using an
/// [IconTheme] and therefore does not need to be explicitly given in the
/// icon widget.
///
/// The trailing edge of the icon is padded by 16dps.
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// See [Icon], [ImageIcon].
final Widget icon;
/// Text that describes the input field.
///
/// When the input field is empty and unfocused, the label is displayed on
/// top of the input field (i.e., at the same location on the screen where
/// text may be entered in the input field). When the input field receives
/// focus (or if the field is non-empty), the label moves above (i.e.,
/// vertically adjacent to) the input field.
final String labelText;
/// The style to use for the [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;
/// Text that provides context about the input [child]'s value, such as how
/// the value will be used.
///
/// If non-null, the text is displayed below the input [child], in the same
/// location as [errorText]. If a non-null [errorText] value is specified then
/// the helper text is not shown.
final String helperText;
/// The style to use for the [helperText].
final TextStyle helperStyle;
/// The maximum number of lines the [helperText] can occupy.
///
/// Defaults to null, which means that the [helperText] will be limited
/// to a single line with [TextOverflow.ellipsis].
///
/// This value is passed along to the [Text.maxLines] attribute
/// of the [Text] widget used to display the helper.
///
/// See also:
///
/// * [errorMaxLines], the equivalent but for the [errorText].
final int helperMaxLines;
/// Text that suggests what sort of input the field accepts.
///
/// Displayed on top of the input [child] (i.e., at the same location on the
/// screen where text may be entered in the input [child]) when the input
/// [isEmpty] and either (a) [labelText] is null or (b) the input has the focus.
final String hintText;
/// The style to use for the [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 may be entered in the input [child]).
///
/// If null, defaults to a value derived from the base [TextStyle] for the
/// input field and the current [Theme].
final TextStyle hintStyle;
/// The maximum number of lines the [hintText] can occupy.
///
/// Defaults to the value of [TextField.maxLines] attribute.
///
/// This value is passed along to the [Text.maxLines] attribute
/// of the [Text] widget used to display the hint text. [TextOverflow.ellipsis] is
/// used to handle the overflow when it is limited to single line.
final int hintMaxLines;
/// Text that appears below the input [child] and the border.
///
/// If non-null, the border's color animates to red and the [helperText] is
/// not shown.
///
/// In a [TextFormField], this is overridden by the value returned from
/// [TextFormField.validator], if that is not null.
final String errorText;
/// The style to use for the [errorText].
///
/// If null, defaults of a value derived from the base [TextStyle] for the
/// input field and the current [Theme].
final TextStyle errorStyle;
/// The maximum number of lines the [errorText] can occupy.
///
/// Defaults to null, which means that the [errorText] will be limited
/// to a single line with [TextOverflow.ellipsis].
///
/// This value is passed along to the [Text.maxLines] attribute
/// of the [Text] widget used to display the error.
///
/// See also:
///
/// * [helperMaxLines], the equivalent but for the [helperText].
final int errorMaxLines;
/// Whether the label floats on focus.
///
/// If this is false, the placeholder disappears when the input has focus or
/// text has been entered.
/// If this is true, the placeholder will rise to the top of the input when
/// the input has focus or text has been entered.
///
/// Defaults to true.
///
@Deprecated(
'Use floatingLabelBehaviour instead. '
'This feature was deprecated after v1.13.2.'
)
final bool hasFloatingPlaceholder;
/// {@template flutter.material.inputDecoration.floatingLabelBehavior}
/// Defines how the floating label should be displayed.
///
/// When [FloatingLabelBehavior.auto] the label will float to the top only when
/// the field is focused or has some text content, otherwise it will appear
/// in the field in place of the content.
///
/// When [FloatingLabelBehavior.always] the label will always float at the top
/// of the field above the content.
///
/// When [FloatingLabelBehavior.never] the label will always appear in an empty
/// field in place of the content.
///
/// Defaults to [FloatingLabelBehavior.auto].
/// {@endtemplate}
final FloatingLabelBehavior floatingLabelBehavior;
/// Whether the input [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 [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// By default the `contentPadding` reflects [isDense] and the type of the
/// [border].
///
/// If [isCollapsed] is true then `contentPadding` is [EdgeInsets.zero].
///
/// If `isOutline` property of [border] is false and if [filled] is true then
/// `contentPadding` is `EdgeInsets.fromLTRB(12, 8, 12, 8)` when [isDense]
/// is true and `EdgeInsets.fromLTRB(12, 12, 12, 12)` when [isDense] is false`.
/// If `isOutline` property of [border] is false and if [filled] is false then
/// `contentPadding` is `EdgeInsets.fromLTRB(0, 8, 0, 8)` when [isDense] is
/// true and `EdgeInsets.fromLTRB(0, 12, 0, 12)` when [isDense] is false`.
///
/// If `isOutline` property of [border] is true then `contentPaddding` is
/// `EdgeInsets.fromLTRB(12, 20, 12, 12)` when [isDense] is true
/// and `EdgeInsets.fromLTRB(12, 24, 12, 16)` when [isDense] is false.
final EdgeInsetsGeometry contentPadding;
/// Whether the decoration is the same size as the input field.
///
/// A collapsed decoration cannot have [labelText], [errorText], an [icon].
///
/// To create a collapsed input decoration, use [InputDecoration..collapsed].
final bool isCollapsed;
/// An icon that appears before the [prefix] or [prefixText] and before
/// the editable part of the text field, within the decoration's container.
///
/// The size and color of the prefix icon is configured automatically using an
/// [IconTheme] and therefore does not need to be explicitly given in the
/// icon widget.
///
/// The prefix icon is constrained with a minimum size of 48px by 48px, but
/// can be expanded beyond that. Anything larger than 24px will require
/// additional padding to ensure it matches the material spec of 12px padding
/// between the left edge of the input and leading edge of the prefix icon.
/// The following snippet shows how to pad the leading edge of the prefix
/// icon:
///
/// ```dart
/// prefixIcon: Padding(
/// padding: const EdgeInsetsDirectional.only(start: 12.0),
/// child: myIcon, // myIcon is a 48px-wide widget.
/// )
/// ```
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// See also:
///
/// * [Icon] and [ImageIcon], which are typically used to show icons.
/// * [prefix] and [prefixText], which are other ways to show content
/// before the text field (but after the icon).
/// * [suffixIcon], which is the same but on the trailing edge.
final Widget prefixIcon;
/// The constraints for the prefix icon.
///
/// This can be used to modify the [BoxConstraints] surrounding [prefixIcon].
///
/// This property is particularly useful for getting the decoration's height
/// less than 48px. This can be achieved by setting [isDense] to true and
/// setting the constraints' minimum height and width to a value lower than
/// 48px.
///
/// {@tool dartpad --template=stateless_widget_scaffold}
/// This example shows the differences between two `TextField` widgets when
/// [prefixIconConstraints] is set to the default value and when one is not.
///
/// Note that [isDense] must be set to true to be able to
/// set the constraints smaller than 48px.
///
/// If null, [BoxConstraints] with a minimum width and height of 48px is
/// used.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Padding(
/// padding: const EdgeInsets.symmetric(horizontal: 8.0),
/// child: Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: <Widget>[
/// TextField(
/// decoration: InputDecoration(
/// hintText: 'Normal Icon Constraints',
/// prefixIcon: Icon(Icons.search),
/// ),
/// ),
/// SizedBox(height: 10),
/// TextField(
/// decoration: InputDecoration(
/// isDense: true,
/// hintText:'Smaller Icon Constraints',
/// prefixIcon: Icon(Icons.search),
/// prefixIconConstraints: BoxConstraints(
/// minHeight: 32,
/// minWidth: 32,
/// ),
/// ),
/// ),
/// ],
/// ),
/// );
/// }
/// ```
/// {@end-tool}
final BoxConstraints prefixIconConstraints;
/// Optional widget to place on the line before the input.
///
/// This can be used, for example, to add some padding to text that would
/// otherwise be specified using [prefixText], or to add a custom widget in
/// front of the input. The widget's baseline is lined up with the input
/// baseline.
///
/// Only one of [prefix] and [prefixText] can be specified.
///
/// The [prefix] appears after the [prefixIcon], if both are specified.
///
/// See also:
///
/// * [suffix], the equivalent but on the trailing edge.
final Widget prefix;
/// Optional text prefix to place on the line before the input.
///
/// Uses the [prefixStyle]. Uses [hintStyle] if [prefixStyle] isn't specified.
/// The prefix text is not returned as part of the user's input.
///
/// If a more elaborate prefix is required, consider using [prefix] instead.
/// Only one of [prefix] and [prefixText] can be specified.
///
/// The [prefixText] appears after the [prefixIcon], if both are specified.
///
/// See also:
///
/// * [suffixText], the equivalent but on the trailing edge.
final String prefixText;
/// The style to use for the [prefixText].
///
/// If null, defaults to the [hintStyle].
///
/// See also:
///
/// * [suffixStyle], the equivalent but on the trailing edge.
final TextStyle prefixStyle;
/// An icon that appears after the editable part of the text field and
/// after the [suffix] or [suffixText], within the decoration's container.
///
/// The size and color of the suffix icon is configured automatically using an
/// [IconTheme] and therefore does not need to be explicitly given in the
/// icon widget.
///
/// The suffix icon is constrained with a minimum size of 48px by 48px, but
/// can be expanded beyond that. Anything larger than 24px will require
/// additional padding to ensure it matches the material spec of 12px padding
/// between the right edge of the input and trailing edge of the prefix icon.
/// The following snippet shows how to pad the trailing edge of the suffix
/// icon:
///
/// ```dart
/// suffixIcon: Padding(
/// padding: const EdgeInsetsDirectional.only(end: 12.0),
/// child: myIcon, // myIcon is a 48px-wide widget.
/// )
/// ```
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// See also:
///
/// * [Icon] and [ImageIcon], which are typically used to show icons.
/// * [suffix] and [suffixText], which are other ways to show content
/// after the text field (but before the icon).
/// * [prefixIcon], which is the same but on the leading edge.
final Widget suffixIcon;
/// Optional widget to place on the line after the input.
///
/// This can be used, for example, to add some padding to the text that would
/// otherwise be specified using [suffixText], or to add a custom widget after
/// the input. The widget's baseline is lined up with the input baseline.
///
/// Only one of [suffix] and [suffixText] can be specified.
///
/// The [suffix] appears before the [suffixIcon], if both are specified.
///
/// See also:
///
/// * [prefix], the equivalent but on the leading edge.
final Widget suffix;
/// Optional text suffix to place on the line after the input.
///
/// Uses the [suffixStyle]. Uses [hintStyle] if [suffixStyle] isn't specified.
/// The suffix text is not returned as part of the user's input.
///
/// If a more elaborate suffix is required, consider using [suffix] instead.
/// Only one of [suffix] and [suffixText] can be specified.
///
/// The [suffixText] appears before the [suffixIcon], if both are specified.
///
/// See also:
///
/// * [prefixText], the equivalent but on the leading edge.
final String suffixText;
/// The style to use for the [suffixText].
///
/// If null, defaults to the [hintStyle].
///
/// See also:
///
/// * [prefixStyle], the equivalent but on the leading edge.
final TextStyle suffixStyle;
/// The constraints for the suffix icon.
///
/// This can be used to modify the [BoxConstraints] surrounding [suffixIcon].
///
/// This property is particularly useful for getting the decoration's height
/// less than 48px. This can be achieved by setting [isDense] to true and
/// setting the constraints' minimum height and width to a value lower than
/// 48px.
///
/// If null, a [BoxConstraints] with a minimum width and height of 48px is
/// used.
///
/// {@tool dartpad --template=stateless_widget_scaffold}
/// This example shows the differences between two `TextField` widgets when
/// [suffixIconConstraints] is set to the default value and when one is not.
///
/// Note that [isDense] must be set to true to be able to
/// set the constraints smaller than 48px.
///
/// If null, [BoxConstraints] with a minimum width and height of 48px is
/// used.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Padding(
/// padding: const EdgeInsets.symmetric(horizontal: 8.0),
/// child: Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: <Widget>[
/// TextField(
/// decoration: InputDecoration(
/// hintText: 'Normal Icon Constraints',
/// suffixIcon: Icon(Icons.search),
/// ),
/// ),
/// SizedBox(height: 10),
/// TextField(
/// decoration: InputDecoration(
/// isDense: true,
/// hintText:'Smaller Icon Constraints',
/// suffixIcon: Icon(Icons.search),
/// suffixIconConstraints: BoxConstraints(
/// minHeight: 32,
/// minWidth: 32,
/// ),
/// ),
/// ),
/// ],
/// ),
/// );
/// }
/// ```
/// {@end-tool}
final BoxConstraints suffixIconConstraints;
/// Optional text to place below the line as a character count.
///
/// Rendered using [counterStyle]. Uses [helperStyle] if [counterStyle] is
/// null.
///
/// The semantic label can be replaced by providing a [semanticCounterText].
///
/// If null or an empty string and [counter] isn't specified, then nothing
/// will appear in the counter's location.
final String counterText;
/// Optional custom counter widget to go in the place otherwise occupied by
/// [counterText]. If this property is non null, then [counterText] is
/// ignored.
final Widget counter;
/// The style to use for the [counterText].
///
/// If null, defaults to the [helperStyle].
final TextStyle counterStyle;
/// If true the decoration's container is filled with [fillColor].
///
/// When [isFocused] is true, the [focusColor] is also blended into the final
/// fill color. When [isHovering] is true, the [hoverColor] is also blended
/// into the final fill color.
///
/// Typically this field set to true if [border] is an
/// [UnderlineInputBorder].
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// This property is false by default.
final bool filled;
/// The base fill color of the decoration's container color.
///
/// When [isFocused] is true, the [focusColor] is also blended into the final
/// fill color. When [isHovering] is true, the [hoverColor] is also blended
/// into the final fill color.
///
/// By default the fillColor is based on the current [Theme].
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// This color is blended with [focusColor] if the decoration is focused.
final Color fillColor;
/// The color to blend with [fillColor] and fill the decoration's container
/// with, if [filled] is true and the container has input focus.
///
/// When [isHovering] is true, the [hoverColor] is also blended into the final
/// fill color.
///
/// By default the [focusColor] is based on the current [Theme].
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
final Color focusColor;
/// The color of the focus highlight for the decoration shown if the container
/// is being hovered over by a mouse.
///
/// If [filled] is true, the color is blended with [fillColor] and fills the
/// decoration's container. When [isFocused] is true, the [focusColor] is also
/// blended into the final fill color.
///
/// If [filled] is false, and [isFocused] is false, the color is blended over
/// the [enabledBorder]'s color.
///
/// By default the [hoverColor] is based on the current [Theme].
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
final Color hoverColor;
/// The border to display when the [InputDecorator] does not have the focus and
/// is showing an error.
///
/// See also:
///
/// * [InputDecorator.isFocused], which is true if the [InputDecorator]'s child
/// has the focus.
/// * [InputDecoration.errorText], the error shown by the [InputDecorator], if non-null.
/// * [border], for a description of where the [InputDecorator] border appears.
/// * [UnderlineInputBorder], an [InputDecorator] border 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.
/// * [InputBorder.none], which doesn't draw a border.
/// * [focusedBorder], displayed when [InputDecorator.isFocused] is true
/// and [InputDecoration.errorText] is null.
/// * [focusedErrorBorder], displayed when [InputDecorator.isFocused] is true
/// and [InputDecoration.errorText] is non-null.
/// * [disabledBorder], displayed when [InputDecoration.enabled] is false
/// and [InputDecoration.errorText] is null.
/// * [enabledBorder], displayed when [InputDecoration.enabled] is true
/// and [InputDecoration.errorText] is null.
final InputBorder errorBorder;
/// The border to display when the [InputDecorator] has the focus and is not
/// showing an error.
///