Unverified Commit d188a8ff authored by Hans Muller's avatar Hans Muller Committed by GitHub

Update InputDecorator et al (#13734)

parent c3366a65
...@@ -55,6 +55,7 @@ export 'src/material/icons.dart'; ...@@ -55,6 +55,7 @@ export 'src/material/icons.dart';
export 'src/material/ink_highlight.dart'; export 'src/material/ink_highlight.dart';
export 'src/material/ink_splash.dart'; export 'src/material/ink_splash.dart';
export 'src/material/ink_well.dart'; export 'src/material/ink_well.dart';
export 'src/material/input_border.dart';
export 'src/material/input_decorator.dart'; export 'src/material/input_decorator.dart';
export 'src/material/list_tile.dart'; export 'src/material/list_tile.dart';
export 'src/material/material.dart'; export 'src/material/material.dart';
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/widgets.dart';
/// Defines the appearance of an [InputDecorator]'s border.
///
/// An input decorator's border is specified by [InputDecoration.border].
///
/// The border is drawn relative to the input decorator's "container" which
/// is the optionally filled area above the decorator's helper, error,
/// and counter.
///
/// Input border's are decorated with a line whose weight and color are defined
/// by [borderSide]. The input decorator's renderer animates the input border's
/// appearance in response to state changes, like gaining or losing the focus,
/// by creating new copies of its input border with [copyWith].
///
/// See also:
///
/// * [UnderlineInputBorder], the default [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.
/// * [InputDecoration], which is used to configure an [InputDecorator].
abstract class InputBorder extends ShapeBorder {
/// Creates a border for an [InputDecorator].
///
/// The [borderSide] parameter must not be null. Applications typically do
/// not specify a [borderSide] parameter because the input decorator
/// substitutes its own, using [copyWith], based on the current theme and
/// [InputDecorator.isFocused].
const InputBorder({
this.borderSide: BorderSide.none,
}) : assert(borderSide != null);
/// Defines the border line's color and weight.
///
/// The [InputDecorator] creates copies of its input border, using [copyWith],
/// based on the current theme and [InputDecorator.isFocused].
final BorderSide borderSide;
/// Creates a copy of this input border with the specified `borderSide`.
InputBorder copyWith({ BorderSide borderSide });
/// True if this border will enclose the [InputDecorator]'s container.
///
/// This property affects the alignment of container's contents. For example
/// when an input decorator is configured with an [OutlineInputBorder] its
/// label is centered with its container.
bool get isOutline;
/// Paint this input border on [canvas].
///
/// The [rect] parameter bounds the [InputDecorator]'s container.
///
/// The additional `gap` parameters reflect the state of the [InputDecorator]'s
/// floating label. When an input decorator gains the focus, its label
/// animates upwards, to make room for the input child. The [gapStart] and
/// [gapExtent] parameters define a floating label width interval, and
/// [gapPercentage] defines the animation's progress (0.0 to 1.0).
@override
void paint(Canvas canvas, Rect rect, {
double gapStart,
double gapExtent: 0.0,
double gapPercentage: 0.0,
TextDirection textDirection,
});
}
/// Draws a horizontal line at the bottom of an [InputDecorator]'s container.
///
/// The input decorator's "container" is the optionally filled area above the
/// decorator's helper, error, and counter.
///
/// See also:
///
/// * [OutlineInputBorder], an [InputDecorator] border which draws a
/// rounded rectangle around the input decorator's container.
/// * [InputDecoration], which is used to configure an [InputDecorator].
class UnderlineInputBorder extends InputBorder {
/// Creates an underline border for an [InputDecorator].
///
/// The [borderSide] parameter defaults to [BorderSide.none] (it must not be
/// null). Applications typically do not specify a [borderSide] parameter
/// because the input decorator substitutes its own, using [copyWith], based
/// on the current theme and [InputDecorator.isFocused].
const UnderlineInputBorder({
BorderSide borderSide: BorderSide.none,
}) : super(borderSide: borderSide);
@override
bool get isOutline => false;
@override
UnderlineInputBorder copyWith({ BorderSide borderSide }) {
return new UnderlineInputBorder(borderSide: borderSide ?? this.borderSide);
}
@override
EdgeInsetsGeometry get dimensions {
return new EdgeInsets.only(bottom: borderSide.width);
}
@override
UnderlineInputBorder scale(double t) {
return new UnderlineInputBorder(borderSide: borderSide.scale(t));
}
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRect(new Rect.fromLTWH(rect.left, rect.top, rect.width, math.max(0.0, rect.height - borderSide.width)));
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()..addRect(rect);
}
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a is UnderlineInputBorder) {
return new UnderlineInputBorder(
borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
if (b is UnderlineInputBorder) {
return new UnderlineInputBorder(
borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
);
}
return super.lerpTo(b, t);
}
/// Draw a horizontal line at the bottom of [rect].
///
/// The [borderSide] defines the line's color and weight. The `textDirection`
/// `gap` and `textDirection` parameters are ignored.
@override
void paint(Canvas canvas, Rect rect, {
double gapStart,
double gapExtent: 0.0,
double gapPercentage: 0.0,
TextDirection textDirection,
}) {
canvas.drawLine(rect.bottomLeft, rect.bottomRight, borderSide.toPaint());
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final InputBorder typedOther = other;
return typedOther.borderSide == borderSide;
}
@override
int get hashCode => borderSide.hashCode;
}
/// Draws a rounded rectangle around an [InputDecorator]'s container.
///
/// When the input decorator's label is floating, for example because its
/// input child has the focus, the label appears in a gap in the border outline.
///
/// The input decorator's "container" is the optionally filled area above the
/// decorator's helper, error, and counter.
///
/// See also:
///
/// * [UnderlineInputBorder], the default [InputDecorator] border which
/// draws a horizontal line at the bottom of the input decorator's container.
/// * [InputDecoration], which is used to configure an [InputDecorator].
class OutlineInputBorder extends InputBorder {
/// Creates a rounded rectangle outline border for an [InputDecorator].
///
/// The [borderSide] parameter defaults to [BorderSide.none] (it must not be
/// null). Applications typically do not specify a [borderSide] parameter
/// because the input decorator substitutes its own, using [copyWith], based
/// on the current theme and [InputDecorator.isFocused].
///
/// If [borderRadius] is null (the default) then the border's corners
/// are drawn with a radius of 4 logical pixels. The corner radii must be
/// circular, i.e. their [Radius.x] and [Radius.y] values must be the same.
const OutlineInputBorder({
BorderSide borderSide: BorderSide.none,
this.borderRadius: const BorderRadius.all(const Radius.circular(4.0)),
this.gapPadding: 4.0,
}) : assert(borderRadius != null),
assert(gapPadding != null && gapPadding >= 0.0),
super(borderSide: borderSide);
// The label text's gap can extend into the corners (even both the top left
// and the top right corner). To avoid the more complicated problem of finding
// how far the gap penetrates into an elliptical corner, just require them
// to be circular.
//
// This can't be checked by the constructor because const constructor.
static bool _cornersAreCircular(BorderRadius borderRadius) {
return borderRadius.topLeft.x == borderRadius.topLeft.y
&& borderRadius.bottomLeft.x == borderRadius.bottomLeft.y
&& borderRadius.topRight.x == borderRadius.topRight.y
&& borderRadius.bottomRight.x == borderRadius.bottomRight.y;
}
/// Horizontal padding on either side of the border's
/// [InputDecoration.labelText] width gap.
///
/// This value is used by the [paint] method to compute the actual gap width.
final double gapPadding;
/// The radii of the border's rounded rectangle corners.
///
/// The corner radii must be circular, i.e. their [Radius.x] and [Radius.y]
/// values must be the same.
final BorderRadius borderRadius;
@override
bool get isOutline => true;
@override
OutlineInputBorder copyWith({
BorderSide borderSide,
BorderRadius borderRadius,
double gapPadding,
}) {
return new OutlineInputBorder(
borderSide: borderSide ?? this.borderSide,
borderRadius: borderRadius ?? this.borderRadius,
gapPadding: gapPadding ?? this.gapPadding,
);
}
@override
EdgeInsetsGeometry get dimensions {
return new EdgeInsets.all(borderSide.width);
}
@override
OutlineInputBorder scale(double t) {
return new OutlineInputBorder(
borderSide: borderSide.scale(t),
borderRadius: borderRadius * t,
gapPadding: gapPadding * t,
);
}
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a is OutlineInputBorder) {
final OutlineInputBorder outline = a;
return new OutlineInputBorder(
borderRadius: BorderRadius.lerp(outline.borderRadius, borderRadius, t),
borderSide: BorderSide.lerp(outline.borderSide, borderSide, t),
gapPadding: outline.gapPadding,
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
if (b is OutlineInputBorder) {
final OutlineInputBorder outline = b;
return new OutlineInputBorder(
borderRadius: BorderRadius.lerp(borderRadius, outline.borderRadius, t),
borderSide: BorderSide.lerp(borderSide, outline.borderSide, t),
gapPadding: outline.gapPadding,
);
}
return super.lerpTo(b, t);
}
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRRect(borderRadius.resolve(textDirection).toRRect(rect).deflate(borderSide.width));
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
}
Path _gapBorderPath(Canvas canvas, RRect center, double start, double extent) {
final Rect tlCorner = new Rect.fromLTWH(
center.left,
center.top,
center.tlRadiusX * 2.0,
center.tlRadiusY * 2.0,
);
final Rect trCorner = new Rect.fromLTWH(
center.right - center.trRadiusX * 2.0,
center.top,
center.trRadiusX * 2.0,
center.trRadiusY * 2.0,
);
final Rect brCorner = new Rect.fromLTWH(
center.right - center.brRadiusX * 2.0,
center.bottom - center.brRadiusY * 2.0,
center.brRadiusX * 2.0,
center.brRadiusY * 2.0,
);
final Rect blCorner = new Rect.fromLTWH(
center.left,
center.bottom - center.brRadiusY * 2.0,
center.blRadiusX * 2.0,
center.blRadiusY * 2.0,
);
final double cornerArcSweep = math.PI / 2.0;
final double tlCornerArcSweep = start < center.tlRadiusX
? math.asin(start / center.tlRadiusX)
: math.PI / 2.0;
final Path path = new Path()
..addArc(tlCorner, math.PI, tlCornerArcSweep)
..moveTo(center.left + center.tlRadiusX, center.top);
if (start > center.tlRadiusX)
path.lineTo(center.left + start, center.top);
final double trCornerArcStart = (3 * math.PI) / 2.0;
final double trCornerArcSweep = cornerArcSweep;
if (start + extent < center.width - center.trRadiusX) {
path
..relativeMoveTo(extent, 0.0)
..lineTo(center.right - center.trRadiusX, center.top)
..addArc(trCorner, trCornerArcStart, trCornerArcSweep);
} else if (start + extent < center.width) {
final double dx = center.width - (start + extent);
final double sweep = math.acos(dx / center.trRadiusX);
path.addArc(trCorner, trCornerArcStart + sweep, trCornerArcSweep - sweep);
}
return path
..moveTo(center.right, center.top + center.trRadiusY)
..lineTo(center.right, center.bottom - center.brRadiusY)
..addArc(brCorner, 0.0, cornerArcSweep)
..lineTo(center.left + center.blRadiusX, center.bottom)
..addArc(blCorner, math.PI / 2.0, cornerArcSweep)
..lineTo(center.left, center.top + center.trRadiusY);
}
/// Draw a rounded rectangle around [rect] using [borderRadius].
///
/// The [borderSide] defines the line's color and weight.
///
/// The top side of the rounded rectangle may be interrupted by a single gap
/// if [gapExtent] is non-null. In that case the gap begins at
/// `gapStart - gapPadding` (assuming that the [textDirection] is [TextDirection.ltr]).
/// The gap's width is `(gapPadding + gapExtent + gapPadding) * gapPercentage`.
@override
void paint(Canvas canvas, Rect rect, {
double gapStart,
double gapExtent: 0.0,
double gapPercentage: 0.0,
TextDirection textDirection,
}) {
assert(gapExtent != null);
assert(gapPercentage >= 0.0 && gapPercentage <= 1.0);
assert(_cornersAreCircular(borderRadius));
final Paint paint = borderSide.toPaint();
final RRect outer = borderRadius.toRRect(rect);
final RRect center = outer.deflate(borderSide.width / 2.0);
if (gapStart == null || gapExtent <= 0.0 || gapPercentage == 0.0) {
canvas.drawRRect(center, paint);
} else {
final double extent = lerpDouble(0.0, gapExtent + gapPadding * 2.0, gapPercentage);
switch (textDirection) {
case TextDirection.rtl: {
final Path path = _gapBorderPath(canvas, center, gapStart + gapPadding - extent, extent);
canvas.drawPath(path, paint);
break;
}
case TextDirection.ltr: {
final Path path = _gapBorderPath(canvas, center, gapStart - gapPadding, extent);
canvas.drawPath(path, paint);
break;
}
}
}
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final OutlineInputBorder typedOther = other;
return typedOther.borderSide == borderSide
&& typedOther.borderRadius == borderRadius
&& typedOther.gapPadding == gapPadding;
}
@override
int get hashCode => hashValues(borderSide, borderRadius, gapPadding);
}
...@@ -2,22 +2,1633 @@ ...@@ -2,22 +2,1633 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'debug.dart'; import 'input_border.dart';
import 'theme.dart'; import 'theme.dart';
const Duration _kTransitionDuration = const Duration(milliseconds: 200); const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn; const Curve _kTransitionCurve = Curves.fastOutSlowIn;
// See the InputDecorator.build method, where this is used. // Defines the gap in the InputDecorator's outline border where the
class _InputDecoratorChildGlobalKey extends GlobalObjectKey { // floating label will appear.
const _InputDecoratorChildGlobalKey(BuildContext value) : super(value); 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 ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final _InputBorderGap typedOther = other;
return typedOther.start == start && typedOther.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);
}
// Passes the _InputBorderGap parameters along to an InputBorder's paint method.
class _InputBorderPainter extends CustomPainter {
_InputBorderPainter({
Listenable repaint,
this.borderAnimation,
this.border,
this.gapAnimation,
this.gap,
this.textDirection,
}) : super(repaint: repaint);
final Animation<double> borderAnimation;
final _InputBorderTween border;
final Animation<double> gapAnimation;
final _InputBorderGap gap;
final TextDirection textDirection;
@override
void paint(Canvas canvas, Size size) {
border.evaluate(borderAnimation).paint(
canvas,
Offset.zero & size,
gapStart: gap.start,
gapExtent: gap.extent,
gapPercentage: gapAnimation.value,
textDirection: textDirection,
);
}
@override
bool shouldRepaint(_InputBorderPainter oldPainter) {
return borderAnimation != oldPainter.borderAnimation
|| 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,
this.child
}) : assert(border != null),
assert(gap != null),
super(key: key);
final InputBorder border;
final _InputBorderGap gap;
final Animation<double> gapAnimation;
final Widget child;
@override
_BorderContainerState createState() => new _BorderContainerState();
}
class _BorderContainerState extends State<_BorderContainer> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _borderAnimation;
_InputBorderTween _border;
@override
void initState() {
super.initState();
_controller = new AnimationController(
duration: _kTransitionDuration,
vsync: this,
);
_borderAnimation = new CurvedAnimation(
parent: _controller,
curve: _kTransitionCurve,
);
_border = new _InputBorderTween(
begin: widget.border,
end: widget.border,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(_BorderContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.border != oldWidget.border) {
_border = new _InputBorderTween(
begin: oldWidget.border,
end: widget.border,
);
_controller
..value = 0.0
..forward();
}
}
@override
Widget build(BuildContext context) {
return new CustomPaint(
foregroundPainter: new _InputBorderPainter(
repaint: new Listenable.merge(<Listenable>[_borderAnimation, widget.gap]),
borderAnimation: _borderAnimation,
border: _border,
gapAnimation: widget.gapAnimation,
gap: widget.gap,
textDirection: Directionality.of(context),
),
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;
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 new Transform(
transform: new 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.errorText,
this.errorStyle,
}) : super(key: key);
final TextAlign textAlign;
final String helperText;
final TextStyle helperStyle;
final String errorText;
final TextStyle errorStyle;
@override
_HelperErrorState createState() => new _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 = const SizedBox();
AnimationController _controller;
Widget _helper;
Widget _error;
@override
void initState() {
super.initState();
_controller = new 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 errorText = widget.errorText;
final String helperText = widget.helperText;
final String oldErrorText = old.errorText;
final String oldHelperText = old.helperText;
if ((errorText ?? helperText) != (oldErrorText ?? oldHelperText)) {
if (errorText != null) {
_error = _buildError();
_controller.forward();
} else if (helperText != null) {
_helper = _buildHelper();
_controller.reverse();
} else {
_controller.reverse();
}
}
}
Widget _buildHelper() {
assert(widget.helperText != null);
return new Opacity(
opacity: 1.0 - _controller.value,
child: new Text(
widget.helperText,
style: widget.helperStyle,
textAlign: widget.textAlign,
overflow: TextOverflow.ellipsis,
),
);
}
Widget _buildError() {
assert(widget.errorText != null);
return new Opacity(
opacity: _controller.value,
child: new FractionalTranslation(
translation: new Tween<Offset>(
begin: const Offset(0.0, -0.25),
end: const Offset(0.0, 0.0),
).evaluate(_controller.view),
child: new Text(
widget.errorText,
style: widget.errorStyle,
textAlign: widget.textAlign,
overflow: TextOverflow.ellipsis,
),
),
);
}
@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 new Stack(
children: <Widget>[
new Opacity(
opacity: 1.0 - _controller.value,
child: _helper,
),
_buildError(),
],
);
}
if (widget.helperText != null) {
return new Stack(
children: <Widget>[
_buildHelper(),
new Opacity(
opacity: _controller.value,
child: _error,
),
],
);
}
return empty;
}
}
// 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.floatingLabelHeight,
@required this.floatingLabelProgress,
this.border,
this.borderGap,
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(floatingLabelHeight != null),
assert(floatingLabelProgress != null);
final EdgeInsets contentPadding;
final double floatingLabelHeight;
final double floatingLabelProgress;
final InputBorder border;
final _InputBorderGap borderGap;
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 ==(dynamic other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
final _Decoration typedOther = other;
return typedOther.contentPadding == contentPadding
&& typedOther.floatingLabelHeight == floatingLabelHeight
&& typedOther.floatingLabelProgress == floatingLabelProgress
&& typedOther.border == border
&& typedOther.borderGap == borderGap
&& typedOther.icon == icon
&& typedOther.input == input
&& typedOther.label == label
&& typedOther.hint == hint
&& typedOther.prefix == prefix
&& typedOther.suffix == suffix
&& typedOther.prefixIcon == prefixIcon
&& typedOther.suffixIcon == suffixIcon
&& typedOther.helperError == helperError
&& typedOther.counter == counter
&& typedOther.container == container;
}
@override
int get hashCode {
return hashValues(
contentPadding,
floatingLabelHeight,
floatingLabelProgress,
border,
borderGap,
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({
_Decoration decoration,
TextDirection textDirection,
}) : _decoration = decoration,
_textDirection = textDirection;
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) {
if (_decoration == value)
return;
_decoration = value;
markNeedsLayout();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value)
return;
_textDirection = value;
markNeedsLayout();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
for (RenderBox child in _children)
child.attach(owner);
}
@override
void detach() {
super.detach();
for (RenderBox child in _children)
child.detach();
}
@override
void redepthChildren() {
_children.forEach(redepthChild);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
_children.forEach(visitor);
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
void add(RenderBox child, String name) {
if (child != null)
value.add(input.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.getMinIntrinsicWidth(width);
}
static Size _boxSize(RenderBox box) => box == null ? Size.zero : box.size;
static BoxParentData _boxParentData(RenderBox box) => box.parentData;
EdgeInsets get contentPadding => decoration.contentPadding;
// 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) {
final Map<RenderBox, double> boxToBaseline = <RenderBox, double>{};
BoxConstraints boxConstraints = layoutConstraints.loosen();
double aboveBaseline = 0.0;
double belowBaseline = 0.0;
void layoutLineBox(RenderBox box) {
if (box == null)
return;
box.layout(boxConstraints, parentUsesSize: true);
final double baseline = box.getDistanceToBaseline(TextBaseline.alphabetic);
assert(baseline != null && baseline >= 0.0);
boxToBaseline[box] = baseline;
aboveBaseline = math.max(baseline, aboveBaseline);
belowBaseline = math.max(box.size.height - baseline, belowBaseline);
}
layoutLineBox(prefix);
layoutLineBox(suffix);
if (icon != null)
icon.layout(boxConstraints, parentUsesSize: true);
if (prefixIcon != null)
prefixIcon.layout(boxConstraints, parentUsesSize: true);
if (suffixIcon != null)
suffixIcon.layout(boxConstraints, parentUsesSize: true);
final double inputWidth = constraints.maxWidth - (
_boxSize(icon).width
+ contentPadding.left
+ _boxSize(prefixIcon).width
+ _boxSize(prefix).width
+ _boxSize(suffix).width
+ _boxSize(suffixIcon).width
+ contentPadding.right);
boxConstraints = boxConstraints.copyWith(maxWidth: inputWidth);
layoutLineBox(hint);
if (label != null) // The label is not baseline aligned.
label.layout(boxConstraints, parentUsesSize: true);
boxConstraints = boxConstraints.copyWith(minWidth: inputWidth);
layoutLineBox(input);
double inputBaseline = contentPadding.top + aboveBaseline;
double containerHeight = contentPadding.top
+ aboveBaseline
+ belowBaseline
+ contentPadding.bottom;
if (label != null) {
// floatingLabelHeight includes the vertical gap between the inline
// elements and the floating label.
containerHeight += decoration.floatingLabelHeight;
inputBaseline += decoration.floatingLabelHeight;
}
// Inline text within an outline border is centered within the container
// less 2.0 dps at the top to account for the vertical space occupied
// by the floating label.
final double outlineBaseline = aboveBaseline +
(containerHeight - (2.0 + aboveBaseline + belowBaseline)) / 2.0;
double subtextBaseline = 0.0;
double subtextHeight = 0.0;
if (helperError != null || counter != null) {
aboveBaseline = 0.0;
belowBaseline = 0.0;
layoutLineBox(helperError);
layoutLineBox(counter);
if (aboveBaseline + belowBaseline > 0.0) {
const double subtextGap = 8.0;
subtextBaseline = containerHeight + subtextGap + aboveBaseline;
subtextHeight = subtextGap + aboveBaseline + belowBaseline;
}
}
return new _RenderDecorationLayout(
boxToBaseline: boxToBaseline,
containerHeight: containerHeight,
inputBaseline: inputBaseline,
outlineBaseline: outlineBaseline,
subtextBaseline: subtextBaseline,
subtextHeight: subtextHeight,
);
}
@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 (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 += 8.0;
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) {
assert(false, 'not implemented');
return 0.0;
}
// Records where the label was painted.
Matrix4 _labelTransform;
@override
void performLayout() {
_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 = new BoxConstraints.tightFor(
height: layout.containerHeight,
width: overallWidth - _boxSize(icon).width,
);
container.layout(containerConstraints, parentUsesSize: true);
final double x = textDirection == TextDirection.rtl ? 0.0 : _boxSize(icon).width;
_boxParentData(container).offset = new Offset(x, 0.0);
}
double height;
double centerLayout(RenderBox box, double x) {
_boxParentData(box).offset = new Offset(x, (height - box.size.height) / 2.0);
return box.size.width;
}
double baseline;
double baselineLayout(RenderBox box, double x) {
_boxParentData(box).offset = new 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 = decoration.border == null || decoration.border.isOutline
? layout.outlineBaseline
: layout.inputBaseline;
if (icon != null) {
final double x = textDirection == TextDirection.rtl ? overallWidth - icon.size.width : 0.0;
centerLayout(icon, x);
}
switch (textDirection) {
case TextDirection.rtl: {
double start = right - _boxSize(icon).width;
double end = left;
if (prefixIcon != null)
start -= centerLayout(prefixIcon, start - prefixIcon.size.width);
if (label != null)
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 += 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 += centerLayout(prefixIcon, start);
if (label != null)
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 -= 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) {
decoration.borderGap.start = textDirection == TextDirection.rtl
? _boxParentData(label).offset.dx + label.size.width
: _boxParentData(label).offset.dx;
decoration.borderGap.extent = label.size.width * 0.75;
} else {
decoration.borderGap.start = null;
decoration.borderGap.extent = 0.0;
}
size = constraints.constrain(new 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 double floatingY = decoration.border.isOutline ? -labelHeight * 0.25 : contentPadding.top;
final double scale = lerpDouble(1.0, 0.75, t);
final double dx = textDirection == TextDirection.rtl
? labelOffset.dx + label.size.width * (1.0 - scale) // origin is on the right
: labelOffset.dx; // origin on the left
final double dy = lerpDouble(0.0, floatingY - labelOffset.dy, t);
_labelTransform = new 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(HitTestResult result, { @required Offset position }) {
assert(position != null);
for (RenderBox child in _children) {
// TODO(hansmuller): label must be handled specially since we've transformed it
if (child.hitTest(result, position: position - _boxParentData(child).offset))
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;
@override
_RenderDecoration get renderObject => super.renderObject;
@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);
}
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(RenderObject 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;
_updateRenderObject(child, 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,
this.decoration,
}) : super(key: key);
final _Decoration decoration;
@override
_RenderDecorationElement createElement() => new _RenderDecorationElement(this);
@override
_RenderDecoration createRenderObject(BuildContext context) {
return new _RenderDecoration(
decoration: decoration,
textDirection: Directionality.of(context),
);
}
@override
void updateRenderObject(BuildContext context, _RenderDecoration renderObject) {
renderObject
..decoration = decoration
..textDirection = Directionality.of(context);
}
}
/// 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] and [isEmpty] arguments must not be null.
const InputDecorator({
Key key,
@required this.decoration,
this.baseStyle,
this.textAlign,
this.isFocused: false,
this.isEmpty: false,
this.child,
}) : assert(isFocused != null),
assert(isEmpty != null),
super(key: key);
/// The text and styles to use when decorating the child.
final InputDecoration decoration;
/// The style on which to base the label, hint, counter, and error styles
/// if the [decoration] does not provide explicit styles.
///
/// If null, defaults to a text style from the current [Theme].
final TextStyle baseStyle;
/// How the text in the decoration should be aligned horizontally.
final TextAlign textAlign;
/// Whether the input field has focus.
///
/// Determines the position of the label text and the color and weight
/// of the border.
///
/// Defaults to false.
final bool isFocused;
/// 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;
bool get _labelIsFloating => !isEmpty || isFocused;
@override
_InputDecoratorState createState() => new _InputDecoratorState();
/// The RenderBox that defines this decorator's "container". That's the
/// area which is filled if [InputDecoration.isFilled] 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.ancestorRenderObjectOfType(const TypeMatcher<_RenderDecoration>());
return result?.container;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<InputDecoration>('decoration', decoration));
description.add(new DiagnosticsProperty<TextStyle>('baseStyle', baseStyle, defaultValue: null));
description.add(new DiagnosticsProperty<bool>('isFocused', isFocused));
description.add(new DiagnosticsProperty<bool>('isEmpty', isEmpty));
}
}
class _InputDecoratorState extends State<InputDecorator> with TickerProviderStateMixin {
AnimationController _floatingLabelController;
AnimationController _shakingLabelController;
final _InputBorderGap _borderGap = new _InputBorderGap();
@override
void initState() {
super.initState();
_floatingLabelController = new AnimationController(
duration: _kTransitionDuration,
vsync: this,
value: widget._labelIsFloating ? 1.0 : 0.0,
);
_floatingLabelController.addListener(_handleChange);
_shakingLabelController = new AnimationController(
duration: _kTransitionDuration,
vsync: this,
);
}
@override
void dispose() {
_floatingLabelController.dispose();
_shakingLabelController.dispose();
super.dispose();
}
void _handleChange() {
setState(() {
// The _floatingLabelController's value has changed.
});
}
InputDecoration get decoration => widget.decoration;
TextAlign get textAlign => widget.textAlign;
bool get isFocused => widget.isFocused;
bool get isEmpty => widget.isEmpty;
@override
void didUpdateWidget(InputDecorator old) {
super.didUpdateWidget(old);
if (widget._labelIsFloating != old._labelIsFloating) {
if (widget._labelIsFloating)
_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 _getFillColor(ThemeData themeData) {
if (!decoration.filled)
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 = const Color(0x1AFFFFFF);
const Color darkDisabled = const Color(0x0DFFFFFF);
const Color lightEnabled = const Color(0x0A000000);
const Color lightDisabled = const Color(0x05000000);
switch (themeData.brightness) {
case Brightness.dark:
return decoration.enabled ? darkEnabled : darkDisabled;
case Brightness.light:
return decoration.enabled ? lightEnabled : lightDisabled;
}
return lightEnabled;
}
// 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 => !isFocused && isEmpty && decoration.labelText != null;
// The style for the inline label or hint when they're displayed "inline", i.e.
// when they appear in place of the empty text field.
TextStyle _getInlineLabelStyle(ThemeData themeData) {
return themeData.textTheme.subhead.merge(widget.baseStyle)
.copyWith(color: themeData.hintColor)
.merge(decoration.hintStyle);
}
TextStyle _getFloatingLabelStyle(ThemeData themeData) {
final Color color = decoration.errorText != null
? decoration.errorStyle?.color ?? themeData.errorColor
: _getActiveColor(themeData);
final TextStyle style = themeData.textTheme.subhead.merge(widget.baseStyle);
return style
.copyWith(color: color)
.merge(decoration.labelStyle);
}
TextStyle _getHelperStyle(ThemeData themeData) {
return themeData.textTheme.caption.copyWith(color: themeData.hintColor).merge(decoration.helperStyle);
}
TextStyle _getErrorStyle(ThemeData themeData) {
return themeData.textTheme.caption.copyWith(color: themeData.errorColor).merge(decoration.errorStyle);
}
double get _borderWeight {
if (decoration.isCollapsed || decoration.border == null || !decoration.enabled)
return 0.0;
return isFocused ? 2.0 : 1.0;
}
Color _getBorderColor(ThemeData themeData) {
return decoration.errorText == null
? _getActiveColor(themeData)
: themeData.errorColor;
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TextStyle inlineStyle = _getInlineLabelStyle(themeData);
final Widget hint = decoration.hintText == null ? null : new AnimatedOpacity(
opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new Text(
decoration.hintText,
style: inlineStyle,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
),
);
final InputBorder border = decoration.border == null ? null : decoration.border.copyWith(
borderSide: new BorderSide(
color: _getBorderColor(themeData),
width: _borderWeight,
),
);
final Widget containerFill = new DecoratedBox(
decoration: new BoxDecoration(color: _getFillColor(themeData)),
);
final Widget container = border == null ? containerFill : new _BorderContainer(
border: border,
gap: _borderGap,
gapAnimation: _floatingLabelController.view,
child: containerFill,
);
final Widget label = decoration.labelText == null ? null : new _Shaker(
animation: _shakingLabelController.view,
child: new AnimatedDefaultTextStyle(
duration: _kTransitionDuration,
curve: _kTransitionCurve,
style: widget._labelIsFloating
? _getFloatingLabelStyle(themeData)
: _getInlineLabelStyle(themeData),
child: new Text(
decoration.labelText,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
),
),
);
final Widget prefix = decoration.prefixText == null ? null :
new AnimatedOpacity(
duration: _kTransitionDuration,
curve: _kTransitionCurve,
opacity: widget._labelIsFloating ? 1.0 : 0.0,
child: new Text(
decoration.prefixText,
style: decoration.prefixStyle ?? inlineStyle
),
);
final Widget suffix = decoration.suffixText == null ? null :
new AnimatedOpacity(
duration: _kTransitionDuration,
curve: _kTransitionCurve,
opacity: widget._labelIsFloating ? 1.0 : 0.0,
child: new Text(
decoration.suffixText,
style: decoration.suffixStyle ?? inlineStyle
),
);
final Color activeColor = _getActiveColor(themeData);
final double iconSize = decoration.isDense ? 18.0 : 24.0;
final Color iconColor = isFocused ? activeColor : Colors.black45;
final Widget icon = decoration.icon == null ? null :
new Padding(
padding: const EdgeInsetsDirectional.only(end: 16.0),
child: IconTheme.merge(
data: new IconThemeData(
color: iconColor,
size: iconSize,
),
child: decoration.icon,
),
);
final Widget prefixIcon = decoration.prefixIcon == null ? null :
IconTheme.merge(
data: new IconThemeData(
color: iconColor,
size: iconSize,
),
child: decoration.prefixIcon,
);
final Widget suffixIcon = decoration.suffixIcon == null ? null :
IconTheme.merge(
data: new IconThemeData(
color: iconColor,
size: iconSize,
),
child: decoration.suffixIcon,
);
final Widget helperError = new _HelperError(
textAlign: textAlign,
helperText: decoration.helperText,
helperStyle: _getHelperStyle(themeData),
errorText: decoration.errorText,
errorStyle: _getErrorStyle(themeData),
);
final Widget counter = decoration.counterText == null ? null :
new Text(
decoration.counterText,
style: _getHelperStyle(themeData).merge(decoration.counterStyle),
textAlign: textAlign == TextAlign.end ? TextAlign.start : TextAlign.end,
overflow: TextOverflow.ellipsis,
);
EdgeInsets contentPadding;
double floatingLabelHeight;
if (decoration.isCollapsed) {
floatingLabelHeight = 0.0;
contentPadding = decoration.contentPadding ?? EdgeInsets.zero;
} else if (decoration.border == null || !decoration.border.isOutline) {
// 4.0: the vertical gap between the inline elements and the floating label.
floatingLabelHeight = 4.0 + 0.75 * inlineStyle.fontSize;
if (decoration.filled) {
contentPadding = decoration.contentPadding ?? (decoration.isDense
? const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0)
: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 12.0));
} else {
// Not left or right padding for underline borders that aren't filled
// is a small concession to backwards compatibility. This eliminates
// the most noticeable layout change introduced by #13734.
contentPadding = decoration.contentPadding ?? (decoration.isDense
? const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0)
: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 12.0));
}
} else {
floatingLabelHeight = 0.0;
contentPadding = decoration.contentPadding ?? (decoration.isDense
? const EdgeInsets.fromLTRB(12.0, 20.0, 12.0, 12.0)
: const EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 16.0));
}
return new _Decorator(
decoration: new _Decoration(
contentPadding: contentPadding,
floatingLabelHeight: floatingLabelHeight,
floatingLabelProgress: _floatingLabelController.value,
border: decoration.border,
borderGap: _borderGap,
icon: icon,
input: widget.child,
label: label,
hint: hint,
prefix: prefix,
suffix: suffix,
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
helperError: helperError,
counter: counter,
container: container,
),
);
}
} }
/// Text and styles used to label an input field. /// The border, labels, icons, and styles used to decorate a Material
/// Design text field.
/// ///
/// The [TextField] and [InputDecorator] classes use [InputDecoration] objects /// The [TextField] and [InputDecorator] classes use [InputDecoration] objects
/// to describe their decoration. (In fact, this class is merely the /// to describe their decoration. (In fact, this class is merely the
...@@ -28,16 +1639,16 @@ class _InputDecoratorChildGlobalKey extends GlobalObjectKey { ...@@ -28,16 +1639,16 @@ class _InputDecoratorChildGlobalKey extends GlobalObjectKey {
/// * [TextField], which is a text input widget that uses an /// * [TextField], which is a text input widget that uses an
/// [InputDecoration]. /// [InputDecoration].
/// * [InputDecorator], which is a widget that draws an [InputDecoration] /// * [InputDecorator], which is a widget that draws an [InputDecoration]
/// around an arbitrary child widget. /// around an input child widget.
/// * [Decoration] and [DecoratedBox], for drawing arbitrary decorations /// * [Decoration] and [DecoratedBox], for drawing borders and backgrounds
/// around other widgets. /// around a child widget.
@immutable @immutable
class InputDecoration { class InputDecoration {
/// Creates a bundle of text and styles used to label an input field. /// Creates a bundle of the border, labels, icons, and styles used to
/// decorate a Material Design text field.
/// ///
/// Sets the [isCollapsed] property to false. To create a decoration that does /// The [isDense], [filled], and [enabled] arguments must not
/// not reserve space for [labelText] or [errorText], use /// be null.
/// [InputDecoration.collapsed].
const InputDecoration({ const InputDecoration({
this.icon, this.icon,
this.labelText, this.labelText,
...@@ -49,25 +1660,39 @@ class InputDecoration { ...@@ -49,25 +1660,39 @@ class InputDecoration {
this.errorText, this.errorText,
this.errorStyle, this.errorStyle,
this.isDense: false, this.isDense: false,
this.hideDivider: false, this.contentPadding,
this.prefixIcon,
this.prefixText, this.prefixText,
this.prefixStyle, this.prefixStyle,
this.suffixText, this.suffixText,
this.suffixIcon,
this.suffixStyle, this.suffixStyle,
this.counterText, this.counterText,
this.counterStyle, this.counterStyle,
}) : isCollapsed = false; this.filled: false,
this.fillColor,
/// Creates a decoration that is the same size as the input field. this.border: const UnderlineInputBorder(),
this.enabled: true,
}) : assert(isDense != null),
assert(filled != null),
assert(enabled != null),
isCollapsed = false;
/// Defines an [InputDecorator] that is the same size as the input field.
/// ///
/// This type of input decoration does not include a divider or an icon and /// This type of input decoration only includes the border.
/// does not reserve space for [labelText] or [errorText].
/// ///
/// Sets the [isCollapsed] property to true. /// Sets the [isCollapsed] property to true.
const InputDecoration.collapsed({ const InputDecoration.collapsed({
@required this.hintText, @required this.hintText,
this.hintStyle, this.hintStyle,
}) : icon = null, this.filled: false,
this.fillColor,
this.border: const UnderlineInputBorder(),
this.enabled: true,
}) : assert(filled != null),
assert(enabled != null),
icon = null,
labelText = null, labelText = null,
labelStyle = null, labelStyle = null,
helperText = null, helperText = null,
...@@ -75,21 +1700,31 @@ class InputDecoration { ...@@ -75,21 +1700,31 @@ class InputDecoration {
errorText = null, errorText = null,
errorStyle = null, errorStyle = null,
isDense = false, isDense = false,
contentPadding = EdgeInsets.zero,
isCollapsed = true, isCollapsed = true,
hideDivider = true, prefixIcon = null,
prefixText = null, prefixText = null,
prefixStyle = null, prefixStyle = null,
suffixIcon = null,
suffixText = null, suffixText = null,
suffixStyle = null, suffixStyle = null,
counterText = null, counterText = null,
counterStyle = null; counterStyle = null;
/// An icon to show before the input field. /// 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 /// The size and color of the icon is configured automatically using an
/// [IconTheme] and therefore does not need to be explicitly given in the /// [IconTheme] and therefore does not need to be explicitly given in the
/// icon widget. /// icon widget.
/// ///
/// The trailing edge of the icon is padded by 16dps.
///
/// The decoration's container is the area which is filled if [isFilled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// See [Icon], [ImageIcon]. /// See [Icon], [ImageIcon].
final Widget icon; final Widget icon;
...@@ -108,14 +1743,14 @@ class InputDecoration { ...@@ -108,14 +1743,14 @@ class InputDecoration {
/// When the [labelText] is on top of the input field, the text uses the /// When the [labelText] is on top of the input field, the text uses the
/// [hintStyle] instead. /// [hintStyle] instead.
/// ///
/// If null, defaults of a value derived from the base [TextStyle] for the /// If null, defaults to a value derived from the base [TextStyle] for the
/// input field and the current [Theme]. /// input field and the current [Theme].
final TextStyle labelStyle; final TextStyle labelStyle;
/// Text that provides context about the field’s value, such as how the value /// Text that provides context about the input [child]'s value, such as how
/// will be used. /// the value will be used.
/// ///
/// If non-null, the text is displayed below the input field, in the same /// 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 /// location as [errorText]. If a non-null [errorText] value is specified then
/// the helper text is not shown. /// the helper text is not shown.
final String helperText; final String helperText;
...@@ -125,25 +1760,25 @@ class InputDecoration { ...@@ -125,25 +1760,25 @@ class InputDecoration {
/// Text that suggests what sort of input the field accepts. /// Text that suggests what sort of input the field accepts.
/// ///
/// Displayed on top of the input field (i.e., at the same location on the /// Displayed on top of the input [child] (i.e., at the same location on the
/// screen where text my be entered in the input field) when the input field /// screen where text my be entered in the input [child]) when the input
/// is empty and either (a) [labelText] is null or (b) the input field has /// [isEmpty] and either (a) [labelText] is null or (b) the input has the focus.
/// focus.
final String hintText; final String hintText;
/// The style to use for the [hintText]. /// The style to use for the [hintText].
/// ///
/// Also used for the [labelText] when the [labelText] is displayed on /// 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 /// top of the input field (i.e., at the same location on the screen where
/// text my be entered in the input field). /// text my be entered in the input [child]).
/// ///
/// If null, defaults of a value derived from the base [TextStyle] for the /// If null, defaults to a value derived from the base [TextStyle] for the
/// input field and the current [Theme]. /// input field and the current [Theme].
final TextStyle hintStyle; final TextStyle hintStyle;
/// Text that appears below the input field. /// Text that appears below the input [child] and the border.
/// ///
/// If non-null, the divider that appears below the input field is red. /// If non-null, the border's color animates to red and the [helperText] is
/// not shown.
final String errorText; final String errorText;
/// The style to use for the [errorText]. /// The style to use for the [errorText].
...@@ -152,24 +1787,53 @@ class InputDecoration { ...@@ -152,24 +1787,53 @@ class InputDecoration {
/// input field and the current [Theme]. /// input field and the current [Theme].
final TextStyle errorStyle; final TextStyle errorStyle;
/// Whether the input field is part of a dense form (i.e., uses less vertical /// Whether the input [child] is part of a dense form (i.e., uses less vertical
/// space). /// space).
/// ///
/// Defaults to false. /// Defaults to false.
final bool isDense; final bool isDense;
/// The padding for the input decoration's container.
///
/// The decoration's container is the area which is filled if [isFilled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [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].
final EdgeInsets contentPadding;
/// Whether the decoration is the same size as the input field. /// Whether the decoration is the same size as the input field.
/// ///
/// A collapsed decoration cannot have [labelText], [errorText], an [icon], or /// A collapsed decoration cannot have [labelText], [errorText], an [icon].
/// a divider because those elements require extra space.
/// ///
/// To create a collapsed input decoration, use [InputDecoration..collapsed]. /// To create a collapsed input decoration, use [InputDecoration..collapsed].
final bool isCollapsed; final bool isCollapsed;
/// Whether to hide the divider below the input field and above the error text. /// An icon that that appears before the [prefixText] and the input and within
/// the decoration's container.
/// ///
/// Defaults to false. /// The size and color of the prefix icon is configured automatically using an
final bool hideDivider; /// [IconTheme] and therefore does not need to be explicitly given in the
/// icon widget.
///
/// The prefix icon is not padded. To pad the trailing edge of the prefix icon:
/// ```dart
/// prefixIcon: new Padding(
/// padding: const EdgeInsetsDirectional.only(end: 16.0),
/// child: myIcon,
/// )
/// ```
///
/// The decoration's container is the area which is filled if [isFilled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// See [Icon], [ImageIcon].
final Widget prefixIcon;
/// Optional text prefix to place on the line before the input. /// Optional text prefix to place on the line before the input.
/// ///
...@@ -182,6 +1846,29 @@ class InputDecoration { ...@@ -182,6 +1846,29 @@ class InputDecoration {
/// If null, defaults to the [hintStyle]. /// If null, defaults to the [hintStyle].
final TextStyle prefixStyle; final TextStyle prefixStyle;
/// An icon that that appears after the input and [suffixText] and 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 not padded. To pad the leading edge of the prefix icon:
/// ```dart
/// prefixIcon: new Padding(
/// padding: const EdgeInsetsDirectional.only(start: 16.0),
/// child: myIcon,
/// )
/// ```
///
/// The decoration's container is the area which is filled if [isFilled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// See [Icon], [ImageIcon].
final Widget suffixIcon;
/// Optional text suffix to place on the line after the input. /// Optional text suffix to place on the line after the input.
/// ///
/// Uses the [suffixStyle]. Uses [hintStyle] if [suffixStyle] isn't /// Uses the [suffixStyle]. Uses [hintStyle] if [suffixStyle] isn't
...@@ -204,8 +1891,53 @@ class InputDecoration { ...@@ -204,8 +1891,53 @@ class InputDecoration {
/// If null, defaults to the [helperStyle]. /// If null, defaults to the [helperStyle].
final TextStyle counterStyle; final TextStyle counterStyle;
/// Creates a copy of this input decoration but with the given fields replaced /// If true the decoration's container is filled with [fillColor].
/// with the new values. ///
/// Typically this field set to true if [border] is
/// [const UnderlineInputBorder()].
///
/// The decoration's container is the area which is filled if [isFilled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// This property is false by default.
final bool filled;
/// The color to fill the decoration's container with, if [filled] is true.
///
/// By default the fillColor is based on the current [Theme].
///
/// The decoration's container is the area which is filled if [isFilled] 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 fillColor;
/// The border to draw around the decoration's container.
///
/// The decoration's container is the area which is filled if [isFilled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// The default value of this property is `const UnderlineInputBorder()`.
///
/// See also:
/// * [UnderlineInputBorder], which draws a horizontal line at the
/// bottom of the input decorator's container.
/// * [OutlineInputBorder], an [InputDecorator] border which draws a
/// rounded rectangle around the input decorator's container.
final InputBorder border;
/// If false [helperText],[errorText], and [counterText] are not displayed,
/// and the opacity of the remaining visual elements is reduced.
///
/// This property is true by default.
final bool enabled;
/// Creates a copy of this input decoration with the given fields replaced
/// by the new values.
/// ///
/// Always sets [isCollapsed] to false. /// Always sets [isCollapsed] to false.
InputDecoration copyWith({ InputDecoration copyWith({
...@@ -219,13 +1951,19 @@ class InputDecoration { ...@@ -219,13 +1951,19 @@ class InputDecoration {
String errorText, String errorText,
TextStyle errorStyle, TextStyle errorStyle,
bool isDense, bool isDense,
bool hideDivider, EdgeInsets contentPadding,
Widget prefixIcon,
String prefixText, String prefixText,
TextStyle prefixStyle, TextStyle prefixStyle,
Widget suffixIcon,
String suffixText, String suffixText,
TextStyle suffixStyle, TextStyle suffixStyle,
String counterText, String counterText,
TextStyle counterStyle, TextStyle counterStyle,
bool filled,
Color fillColor,
InputBorder border,
bool enabled,
}) { }) {
return new InputDecoration( return new InputDecoration(
icon: icon ?? this.icon, icon: icon ?? this.icon,
...@@ -238,13 +1976,19 @@ class InputDecoration { ...@@ -238,13 +1976,19 @@ class InputDecoration {
errorText: errorText ?? this.errorText, errorText: errorText ?? this.errorText,
errorStyle: errorStyle ?? this.errorStyle, errorStyle: errorStyle ?? this.errorStyle,
isDense: isDense ?? this.isDense, isDense: isDense ?? this.isDense,
hideDivider: hideDivider ?? this.hideDivider, contentPadding: contentPadding ?? this.contentPadding,
prefixIcon: prefixIcon ?? this.prefixIcon,
prefixText: prefixText ?? this.prefixText, prefixText: prefixText ?? this.prefixText,
prefixStyle: prefixStyle ?? this.prefixStyle, prefixStyle: prefixStyle ?? this.prefixStyle,
suffixIcon: suffixIcon ?? this.suffixIcon,
suffixText: suffixText ?? this.suffixText, suffixText: suffixText ?? this.suffixText,
suffixStyle: suffixStyle ?? this.suffixStyle, suffixStyle: suffixStyle ?? this.suffixStyle,
counterText: counterText ?? this.counterText, counterText: counterText ?? this.counterText,
counterStyle: counterStyle ?? this.counterStyle, counterStyle: counterStyle ?? this.counterStyle,
filled: filled ?? this.filled,
fillColor: fillColor ?? this.fillColor,
border: border ?? this.border,
enabled: enabled ?? this.enabled,
); );
} }
...@@ -265,14 +2009,20 @@ class InputDecoration { ...@@ -265,14 +2009,20 @@ class InputDecoration {
&& typedOther.errorText == errorText && typedOther.errorText == errorText
&& typedOther.errorStyle == errorStyle && typedOther.errorStyle == errorStyle
&& typedOther.isDense == isDense && typedOther.isDense == isDense
&& typedOther.contentPadding == contentPadding
&& typedOther.isCollapsed == isCollapsed && typedOther.isCollapsed == isCollapsed
&& typedOther.hideDivider == hideDivider && typedOther.prefixIcon == prefixIcon
&& typedOther.prefixText == prefixText && typedOther.prefixText == prefixText
&& typedOther.prefixStyle == prefixStyle && typedOther.prefixStyle == prefixStyle
&& typedOther.suffixIcon == suffixIcon
&& typedOther.suffixText == suffixText && typedOther.suffixText == suffixText
&& typedOther.suffixStyle == suffixStyle && typedOther.suffixStyle == suffixStyle
&& typedOther.counterText == counterText && typedOther.counterText == counterText
&& typedOther.counterStyle == counterStyle; && typedOther.counterStyle == counterStyle
&& typedOther.filled == filled
&& typedOther.fillColor == fillColor
&& typedOther.border == border
&& typedOther.enabled == enabled;
} }
@override @override
...@@ -283,19 +2033,27 @@ class InputDecoration { ...@@ -283,19 +2033,27 @@ class InputDecoration {
labelStyle, labelStyle,
helperText, helperText,
helperStyle, helperStyle,
hashValues( // Over 20 fields...
hintText, hintText,
hintStyle, hintStyle,
errorText, errorText,
errorStyle, errorStyle,
isDense, isDense,
contentPadding,
isCollapsed, isCollapsed,
hideDivider, prefixIcon,
prefixText, prefixText,
prefixStyle, prefixStyle,
suffixIcon,
suffixText, suffixText,
suffixStyle, suffixStyle,
counterText, counterText,
counterStyle, counterStyle,
filled,
fillColor,
border,
enabled,
),
); );
} }
...@@ -314,14 +2072,18 @@ class InputDecoration { ...@@ -314,14 +2072,18 @@ class InputDecoration {
description.add('errorText: "$errorText"'); description.add('errorText: "$errorText"');
if (isDense) if (isDense)
description.add('isDense: $isDense'); description.add('isDense: $isDense');
if (contentPadding != null)
description.add('contentPadding: $contentPadding');
if (isCollapsed) if (isCollapsed)
description.add('isCollapsed: $isCollapsed'); description.add('isCollapsed: $isCollapsed');
if (hideDivider) if (prefixIcon != null)
description.add('hideDivider: $hideDivider'); description.add('prefixIcon: $prefixIcon');
if (prefixText != null) if (prefixText != null)
description.add('prefixText: $prefixText'); description.add('prefixText: $prefixText');
if (prefixStyle != null) if (prefixStyle != null)
description.add('prefixStyle: $prefixStyle'); description.add('prefixStyle: $prefixStyle');
if (suffixIcon != null)
description.add('suffixIcon: $suffixIcon');
if (suffixText != null) if (suffixText != null)
description.add('suffixText: $suffixText'); description.add('suffixText: $suffixText');
if (suffixStyle != null) if (suffixStyle != null)
...@@ -330,408 +2092,14 @@ class InputDecoration { ...@@ -330,408 +2092,14 @@ class InputDecoration {
description.add('counterText: $counterText'); description.add('counterText: $counterText');
if (counterStyle != null) if (counterStyle != null)
description.add('counterStyle: $counterStyle'); description.add('counterStyle: $counterStyle');
if (filled)
description.add('filled: true');
if (fillColor != null)
description.add('fillColor: $fillColor');
if (border != null)
description.add('border: $border');
if (!enabled)
description.add('enabled: false');
return 'InputDecoration(${description.join(', ')})'; return 'InputDecoration(${description.join(', ')})';
} }
} }
/// Displays the visual elements of a Material Design text field around an
/// arbitrary widget.
///
/// Use [InputDecorator] to create widgets that look and behave like a
/// [TextField] but can be used to input information other than text.
///
/// The configuration of this widget is primarily provided in the form of an
/// [InputDecoration] object.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [TextField], which uses an [InputDecorator] to draw labels and other
/// visual elements around a text entry widget.
/// * [Decoration] and [DecoratedBox], for drawing arbitrary decorations
/// around other widgets.
class InputDecorator extends StatelessWidget {
/// Creates a widget that displays labels and other visual elements similar
/// to a [TextField].
///
/// The [isFocused] and [isEmpty] arguments must not be null.
const InputDecorator({
Key key,
@required this.decoration,
this.baseStyle,
this.textAlign,
this.isFocused: false,
this.isEmpty: false,
this.child,
}) : assert(isFocused != null),
assert(isEmpty != null),
super(key: key);
/// The text and styles to use when decorating the child.
final InputDecoration decoration;
/// The style on which to base the label, hint, and error styles if the
/// [decoration] does not provide explicit styles.
///
/// If null, defaults to a text style from the current [Theme].
final TextStyle baseStyle;
/// How the text in the decoration should be aligned horizontally.
final TextAlign textAlign;
/// Whether the input field has focus.
///
/// Determines the position of the label text and the color of the divider.
///
/// Defaults to false.
final bool isFocused;
/// 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;
static const double _kBottomBorderHeight = 1.0;
static const double _kDensePadding = 4.0;
static const double _kNormalPadding = 8.0;
static const double _kDenseTopPadding = 8.0;
static const double _kNormalTopPadding = 16.0;
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<InputDecoration>('decoration', decoration));
description.add(new DiagnosticsProperty<TextStyle>('baseStyle', baseStyle, defaultValue: null));
description.add(new DiagnosticsProperty<bool>('isFocused', isFocused));
description.add(new DiagnosticsProperty<bool>('isEmpty', isEmpty));
}
Color _getActiveColor(ThemeData themeData) {
if (isFocused) {
switch (themeData.brightness) {
case Brightness.dark:
return themeData.accentColor;
case Brightness.light:
return themeData.primaryColor;
}
}
return themeData.hintColor;
}
Widget _buildContent(Color borderColor, double topPadding, bool isDense, Widget inputChild) {
if (decoration.hideDivider) {
return new Container(
padding: new EdgeInsets.only(top: topPadding, bottom: _kNormalPadding),
child: inputChild,
);
}
return new AnimatedContainer(
padding: new EdgeInsets.only(top: topPadding, bottom: _kNormalPadding - _kBottomBorderHeight),
duration: _kTransitionDuration,
curve: _kTransitionCurve,
decoration: new BoxDecoration(
border: new Border(
bottom: new BorderSide(
color: borderColor,
width: _kBottomBorderHeight,
),
),
),
child: inputChild,
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(context);
final double textScaleFactor = MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0;
final bool isDense = decoration.isDense;
final bool isCollapsed = decoration.isCollapsed;
assert(!isDense || !isCollapsed);
final String labelText = decoration.labelText;
final String helperText = decoration.helperText;
final String counterText = decoration.counterText;
final String hintText = decoration.hintText;
final String errorText = decoration.errorText;
// If we're not focused, there's no value, and labelText was provided,
// then the label appears where the hint would. And we will not show
// the hintText.
final bool hasInlineLabel = !isFocused && labelText != null && isEmpty;
final Color activeColor = _getActiveColor(themeData);
final TextStyle baseStyle = themeData.textTheme.subhead.merge(this.baseStyle);
final TextStyle hintStyle = baseStyle.copyWith(color: themeData.hintColor).merge(decoration.hintStyle);
final TextStyle helperStyle = themeData.textTheme.caption.copyWith(color: themeData.hintColor).merge(decoration.helperStyle);
final TextStyle counterStyle = helperStyle.merge(decoration.counterStyle);
final TextStyle subtextStyle = errorText != null
? themeData.textTheme.caption.copyWith(color: themeData.errorColor).merge(decoration.errorStyle)
: helperStyle;
double topPadding = isCollapsed ? 0.0 : (isDense ? _kDenseTopPadding : _kNormalTopPadding);
final List<Widget> stackChildren = <Widget>[];
if (labelText != null) {
assert(!isCollapsed);
final TextStyle floatingLabelStyle = themeData.textTheme.caption.copyWith(color: activeColor).merge(decoration.labelStyle);
final TextStyle labelStyle = hasInlineLabel ? hintStyle : floatingLabelStyle;
final double labelTextHeight = floatingLabelStyle.fontSize * textScaleFactor;
final double topPaddingIncrement = labelTextHeight + (isDense ? _kDensePadding : _kNormalPadding);
stackChildren.add(
new AnimatedPositionedDirectional(
start: 0.0,
top: topPadding + (hasInlineLabel ? topPaddingIncrement : 0.0),
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new _AnimatedLabel(
text: labelText,
style: labelStyle,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
),
),
);
topPadding += topPaddingIncrement;
}
if (hintText != null) {
stackChildren.add(
new AnimatedPositionedDirectional(
start: 0.0,
end: 0.0,
top: topPadding,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new AnimatedOpacity(
opacity: (isEmpty && !hasInlineLabel) ? 1.0 : 0.0,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new Text(
hintText,
style: hintStyle,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
),
),
),
);
}
Widget inputChild = new KeyedSubtree(
// It's important that we maintain the state of our child subtree, as it
// may be stateful (e.g. containing text selections). Since our build
// function risks changing the depth of the tree, we preserve the subtree
// using global keys.
// GlobalObjectKey(context) will always be the same whenever we are built.
// Additionally, we use a subclass of GlobalObjectKey to avoid clashes
// with anyone else using our BuildContext as their global object key
// value.
key: new _InputDecoratorChildGlobalKey(context),
child: child,
);
if (!hasInlineLabel && (!isEmpty || hintText == null) &&
(decoration?.prefixText != null || decoration?.suffixText != null)) {
final List<Widget> rowContents = <Widget>[];
if (decoration.prefixText != null) {
rowContents.add(
new Text(decoration.prefixText,
style: decoration.prefixStyle ?? hintStyle)
);
}
rowContents.add(new Expanded(child: inputChild));
if (decoration.suffixText != null) {
rowContents.add(
new Text(decoration.suffixText,
style: decoration.suffixStyle ?? hintStyle)
);
}
inputChild = new Row(children: rowContents);
}
// The inputChild and the helper/error text need to be in a column so that if the inputChild is
// a multiline input or a non-text widget, it lays out with the helper/error text below the
// inputChild.
final List<Widget> columnChildren = <Widget>[];
if (isCollapsed) {
columnChildren.add(inputChild);
} else {
final Color borderColor = errorText == null ? activeColor : themeData.errorColor;
columnChildren.add(_buildContent(borderColor, topPadding, isDense, inputChild));
}
if (errorText != null || helperText != null || counterText != null) {
assert(!isCollapsed, "Collapsed fields can't have errorText, helperText, or counterText set.");
final EdgeInsets topPadding = new EdgeInsets.only(
top: _kBottomBorderHeight + (isDense ? _kDensePadding : _kNormalPadding)
);
Widget buildSubText() {
return new AnimatedContainer(
padding: topPadding,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new Text(
errorText ?? helperText,
style: subtextStyle,
textAlign: textAlign,
overflow: TextOverflow.ellipsis,
),
);
}
Widget buildCounter() {
return new AnimatedContainer(
padding: topPadding,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new Text(
counterText,
style: counterStyle,
textAlign: textAlign == TextAlign.end ? TextAlign.start : TextAlign.end,
overflow: TextOverflow.ellipsis,
),
);
}
final bool needSubTextField = errorText != null || helperText != null;
final bool needCounterField = counterText != null;
if (needCounterField && needSubTextField) {
columnChildren.add(
new Row(
children: <Widget>[
new Expanded(child: buildSubText()),
buildCounter(),
],
),
);
} else if (needSubTextField) {
columnChildren.add(buildSubText());
} else if (needCounterField) {
columnChildren.add(buildCounter());
}
}
stackChildren.add(
new Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: columnChildren,
),
);
final Widget stack = new Stack(
fit: StackFit.passthrough,
children: stackChildren
);
if (decoration.icon != null) {
assert(!isCollapsed);
final double iconSize = isDense ? 18.0 : 24.0;
final double entryTextHeight = baseStyle.fontSize * textScaleFactor;
final double iconTop = topPadding + (entryTextHeight - iconSize) / 2.0;
return new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new AnimatedContainer(
margin: new EdgeInsets.only(top: iconTop),
duration: _kTransitionDuration,
curve: _kTransitionCurve,
width: isDense ? 40.0 : 48.0,
child: IconTheme.merge(
data: new IconThemeData(
color: isFocused ? activeColor : Colors.black45,
size: iconSize,
),
child: decoration.icon,
),
),
new Expanded(child: stack),
],
);
} else {
return new ConstrainedBox(
constraints: const BoxConstraints(minWidth: double.INFINITY),
child: stack,
);
}
}
}
// Smoothly animate the label of an InputDecorator as the label
// transitions between inline and caption.
class _AnimatedLabel extends ImplicitlyAnimatedWidget {
const _AnimatedLabel({
Key key,
this.text,
@required this.style,
Curve curve: Curves.linear,
@required Duration duration,
this.textAlign,
this.overflow,
}) : assert(style != null),
super(key: key, curve: curve, duration: duration);
final String text;
final TextStyle style;
final TextAlign textAlign;
final TextOverflow overflow;
@override
_AnimatedLabelState createState() => new _AnimatedLabelState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
style?.debugFillProperties(description);
}
}
class _AnimatedLabelState extends AnimatedWidgetBaseState<_AnimatedLabel> {
TextStyleTween _style;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_style = visitor(_style, widget.style, (dynamic value) => new TextStyleTween(begin: value));
}
@override
Widget build(BuildContext context) {
TextStyle style = _style.evaluate(animation);
double scale = 1.0;
if (style.fontSize != widget.style.fontSize) {
// While the fontSize is transitioning, use a scaled Transform as a
// fraction of the original fontSize. That way we get a smooth scaling
// effect with no snapping between discrete font sizes.
scale = style.fontSize / widget.style.fontSize;
style = style.copyWith(fontSize: widget.style.fontSize);
}
return new Transform(
transform: new Matrix4.identity()..scale(scale),
child: new Text(
widget.text,
style: style,
textAlign: widget.textAlign,
overflow: widget.overflow,
),
);
}
}
...@@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart'; ...@@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart';
import 'feedback.dart'; import 'feedback.dart';
import 'input_decorator.dart'; import 'input_decorator.dart';
import 'material.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'theme.dart'; import 'theme.dart';
......
...@@ -285,6 +285,7 @@ class _ShapeDecorationPainter extends BoxPainter { ...@@ -285,6 +285,7 @@ class _ShapeDecorationPainter extends BoxPainter {
final ShapeDecoration _decoration; final ShapeDecoration _decoration;
Rect _lastRect; Rect _lastRect;
TextDirection _lastTextDirection;
Path _outerPath; Path _outerPath;
Path _innerPath; Path _innerPath;
Paint _interiorPaint; Paint _interiorPaint;
...@@ -292,10 +293,11 @@ class _ShapeDecorationPainter extends BoxPainter { ...@@ -292,10 +293,11 @@ class _ShapeDecorationPainter extends BoxPainter {
List<Path> _shadowPaths; List<Path> _shadowPaths;
List<Paint> _shadowPaints; List<Paint> _shadowPaints;
void _precache(Rect rect) { void _precache(Rect rect, TextDirection textDirection) {
assert(rect != null); assert(rect != null);
if (rect == _lastRect) if (rect == _lastRect && textDirection == _lastTextDirection)
return; return;
// We reach here in two cases: // We reach here in two cases:
// - the very first time we paint, in which case everything except _decoration is null // - the very first time we paint, in which case everything except _decoration is null
// - subsequent times, if the rect has changed, in which case we only need to update // - subsequent times, if the rect has changed, in which case we only need to update
...@@ -328,7 +330,9 @@ class _ShapeDecorationPainter extends BoxPainter { ...@@ -328,7 +330,9 @@ class _ShapeDecorationPainter extends BoxPainter {
_outerPath = _decoration.shape.getOuterPath(rect); _outerPath = _decoration.shape.getOuterPath(rect);
if (_decoration.image != null) if (_decoration.image != null)
_innerPath = _decoration.shape.getInnerPath(rect); _innerPath = _decoration.shape.getInnerPath(rect);
_lastRect = rect; _lastRect = rect;
_lastTextDirection = textDirection;
} }
void _paintShadows(Canvas canvas) { void _paintShadows(Canvas canvas) {
...@@ -362,10 +366,11 @@ class _ShapeDecorationPainter extends BoxPainter { ...@@ -362,10 +366,11 @@ class _ShapeDecorationPainter extends BoxPainter {
assert(configuration != null); assert(configuration != null);
assert(configuration.size != null); assert(configuration.size != null);
final Rect rect = offset & configuration.size; final Rect rect = offset & configuration.size;
_precache(rect); final TextDirection textDirection = configuration.textDirection;
_precache(rect, textDirection);
_paintShadows(canvas); _paintShadows(canvas);
_paintInterior(canvas); _paintInterior(canvas);
_paintImage(canvas, configuration); _paintImage(canvas, configuration);
_decoration.shape.paint(canvas, rect); _decoration.shape.paint(canvas, rect, textDirection: textDirection);
} }
} }
...@@ -6,509 +6,911 @@ import 'package:flutter/material.dart'; ...@@ -6,509 +6,911 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { Widget buildInputDecorator({
Widget buildInputDecorator({InputDecoration decoration = const InputDecoration(), Widget child = const Text('Test')}) { InputDecoration decoration: const InputDecoration(),
TextDirection textDirection: TextDirection.ltr,
bool isEmpty: false,
bool isFocused: false,
TextStyle baseStyle,
Widget child: const Text(
'text',
style: const TextStyle(fontFamily: 'Ahem', fontSize: 16.0),
),
}) {
return new MaterialApp( return new MaterialApp(
home: new Material( home: new Material(
child: new DefaultTextStyle( child: new Align(
style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0), alignment: Alignment.topLeft,
child: new Center( child: new Directionality(
textDirection: textDirection,
child: new InputDecorator( child: new InputDecorator(
decoration: decoration, decoration: decoration,
isEmpty: isEmpty,
isFocused: isFocused,
baseStyle: baseStyle,
child: child, child: child,
), ),
), ),
), ),
), ),
); );
} }
Finder findInputDecoratorChildContainer() {
return find.byWidgetPredicate(
(Widget w) {
return w is AnimatedContainer && (w as dynamic).decoration != null;
});
}
double getBoxDecorationThickness(WidgetTester tester) { Finder findBorderPainter() {
final AnimatedContainer container = tester.widget(findInputDecoratorChildContainer()); return find.descendant(
final BoxDecoration decoration = container.decoration; of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'),
final Border border = decoration.border; matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
return border.bottom.width; );
} }
double getDividerY(WidgetTester tester) { double getBorderBottom(WidgetTester tester) {
final Finder animatedContainerFinder = find.byWidgetPredicate( final RenderBox box = InputDecorator.containerOf(tester.element(findBorderPainter()));
(Widget w) { return box.size.height;
return w is AnimatedContainer && (w as dynamic).decoration != null; }
});
return tester.getRect(animatedContainerFinder).bottom;
}
double getDividerWidth(WidgetTester tester) { double getBorderWeight(WidgetTester tester) {
final Finder animatedContainerFinder = find.byWidgetPredicate( if (!tester.any(findBorderPainter()))
(Widget w) { return 0.0;
return w is AnimatedContainer && (w as dynamic).decoration != null; final CustomPaint customPaint = tester.widget(findBorderPainter());
}); final dynamic/* _InputBorderPainter */ inputBorderPainter = customPaint.foregroundPainter;
return tester.getRect(animatedContainerFinder).size.width; final dynamic/*_InputBorderTween */ inputBorderTween = inputBorderPainter.border;
} final Animation<double> animation = inputBorderPainter.borderAnimation;
final dynamic/*_InputBorder */ border = inputBorderTween.evaluate(animation);
return border.borderSide.width;
}
testWidgets('InputDecorator always expands horizontally', (WidgetTester tester) async { double getHintOpacity(WidgetTester tester) {
final Key key = new UniqueKey(); final Opacity opacityWidget = tester.widget<Opacity>(
find.ancestor(
of: find.text('hint'),
matching: find.byType(Opacity),
).last
);
return opacityWidget.opacity;
}
void main() {
testWidgets('InputDecorator input/label layout', (WidgetTester tester) async {
// The label appears above the input text
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
child: new Container(key: key, width: 50.0, height: 60.0, color: Colors.blue), // isEmpty: false (default)
// isFocused: false (default)
decoration: const InputDecoration(
labelText: 'label',
),
), ),
); );
expect(tester.element(find.byKey(key)).size, equals(const Size(800.0, 60.0))); // Overall height for this InputDecorator is 56dps:
// 12 - top padding
// 12 - floating label (ahem font size 16dps * 0.75 = 12)
// 4 - floating label / input text gap
// 16 - input text (ahem font size 16dps)
// 12 - bottom padding
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 1.0);
// isFocused: true increases the border's weight from 1.0 to 2.0
// but does not change the overall height.
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
// isEmpty: false (default)
isFocused: true,
decoration: const InputDecoration( decoration: const InputDecoration(
icon: const Icon(Icons.add_shopping_cart), labelText: 'label',
), ),
child: new Container(key: key, width: 50.0, height: 60.0, color: Colors.blue),
), ),
); );
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 2.0);
expect(tester.element(find.byKey(key)).size, equals(const Size(752.0, 60.0))); // isEmpty: true causes the label to be aligned with the input text
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: false,
decoration: const InputDecoration(
labelText: 'label',
),
),
);
// The label animates downwards from it's initial position
// above the input text. The animation's duration is 200ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double labelY50ms = tester.getTopLeft(find.text('label')).dy;
expect(labelY50ms, inExclusiveRange(12.0, 20.0));
await tester.pump(const Duration(milliseconds: 50));
final double labelY100ms = tester.getTopLeft(find.text('label')).dy;
expect(labelY100ms, inExclusiveRange(labelY50ms, 20.0));
}
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 1.0);
// isFocused: true causes the label to move back up above the input text.
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
decoration: const InputDecoration.collapsed( isEmpty: true,
hintText: 'Hint text', isFocused: true,
decoration: const InputDecoration(
labelText: 'label',
), ),
child: new Container(key: key, width: 50.0, height: 60.0, color: Colors.blue),
), ),
); );
expect(tester.element(find.byKey(key)).size, equals(const Size(800.0, 60.0))); // The label animates upwards from it's initial position
}); // above the input text. The animation's duration is 200ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double labelY50ms = tester.getTopLeft(find.text('label')).dy;
expect(labelY50ms, inExclusiveRange(12.0, 28.0));
await tester.pump(const Duration(milliseconds: 50));
final double labelY100ms = tester.getTopLeft(find.text('label')).dy;
expect(labelY100ms, inExclusiveRange(12.0, labelY50ms));
}
testWidgets('InputDecorator draws the divider correctly in the right place.', (WidgetTester tester) async { await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 2.0);
// enabled: false causes the border to disappear
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
isEmpty: true,
isFocused: false,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Hint', labelText: 'label',
labelText: 'Label', enabled: false,
helperText: 'Helper',
counterText: 'Counter',
), ),
), ),
); );
await tester.pumpAndSettle();
expect(getBoxDecorationThickness(tester), equals(1.0)); expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
expect(getDividerY(tester), equals(316.5)); expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(getDividerWidth(tester), equals(800.0)); expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
expect(getBorderWeight(tester), 0.0);
}); });
testWidgets('InputDecorator draws the divider correctly in the right place for dense layout.', (WidgetTester tester) async { // Overall height for this InputDecorator is 40.0dps
// 12 - top padding
// 16 - input text (ahem font size 16dps)
// 12 - bottom padding
testWidgets('InputDecorator input/hint layout', (WidgetTester tester) async {
// The hint aligns with the input text
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
isEmpty: true,
// isFocused: false (default)
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Hint', hintText: 'hint',
labelText: 'Label',
helperText: 'Helper',
counterText: 'Counter',
isDense: true,
), ),
), ),
); );
expect(getBoxDecorationThickness(tester), equals(1.0)); expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 40.0));
expect(getDividerY(tester), equals(312.5)); expect(tester.getTopLeft(find.text('text')).dy, 12.0);
expect(getDividerWidth(tester), equals(800.0)); expect(tester.getBottomLeft(find.text('text')).dy, 28.0);
expect(tester.getTopLeft(find.text('hint')).dy, 12.0);
expect(tester.getBottomLeft(find.text('hint')).dy, 28.0);
expect(getBorderBottom(tester), 40.0);
expect(getBorderWeight(tester), 1.0);
}); });
testWidgets('InputDecorator does not draw the underline when hideDivider is true.', (WidgetTester tester) async { testWidgets('InputDecorator input/label/hint layout', (WidgetTester tester) async {
// Label is visible, hint is not (opacity 0.0).
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
isEmpty: true,
// isFocused: false (default)
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Hint', labelText: 'label',
labelText: 'Label', hintText: 'hint',
helperText: 'Helper',
counterText: 'Counter',
hideDivider: true,
), ),
), ),
); );
expect(findInputDecoratorChildContainer(), findsNothing); // Overall height for this InputDecorator is 56dps. When the
}); // label is "floating" (empty input or no focus) the layout is:
//
// 12 - top padding
// 12 - floating label (ahem font size 16dps * 0.75 = 12)
// 4 - floating label / input text gap
// 16 - input text (ahem font size 16dps)
// 12 - bottom padding
//
// When the label is not floating, it's vertically centered.
//
// 20 - top padding
// 16 - label (ahem font size 16dps)
// 20 - bottom padding (empty input text still appears here)
testWidgets('InputDecorator uses proper padding for dense mode', (WidgetTester tester) async { // The label is not floating so it's vertically centered.
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
expect(getHintOpacity(tester), 0.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 1.0);
// Label moves upwards, hint is visible (opacity 1.0).
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
isEmpty: true,
isFocused: true,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Hint', labelText: 'label',
labelText: 'Label', hintText: 'hint',
helperText: 'Helper',
counterText: 'Counter',
isDense: true,
), ),
), ),
); );
// TODO(#12357): Update this test when the font metric bug is fixed to remove the anyOfs. // The hint's opacity animates from 0.0 to 1.0.
expect( // The animation's duration is 200ms.
tester.getRect(find.text('Label')).size, {
anyOf(<Size>[const Size(60.0, 12.0), const Size(61.0, 12.0)]), await tester.pump(const Duration(milliseconds: 50));
); final double hintOpacity50ms = getHintOpacity(tester);
expect(tester.getRect(find.text('Label')).left, equals(0.0)); expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0));
expect(tester.getRect(find.text('Label')).top, equals(278.5)); await tester.pump(const Duration(milliseconds: 50));
expect(tester.getRect(find.text('Hint')).size, equals(const Size(800.0, 16.0))); final double hintOpacity100ms = getHintOpacity(tester);
expect(tester.getRect(find.text('Hint')).left, equals(0.0)); expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0));
expect(tester.getRect(find.text('Hint')).top, equals(294.5)); }
expect(
tester.getRect(find.text('Helper')).size, await tester.pumpAndSettle();
anyOf(<Size>[const Size(716.0, 12.0), const Size(715.0, 12.0)]), expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
); expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getRect(find.text('Helper')).left, equals(0.0)); expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getRect(find.text('Helper')).top, equals(317.5)); expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect( expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
tester.getRect(find.text('Counter')).size, expect(tester.getTopLeft(find.text('hint')).dy, 28.0);
anyOf(<Size>[const Size(84.0, 12.0), const Size(85.0, 12.0)]), expect(tester.getBottomLeft(find.text('hint')).dy, 44.0);
); expect(getHintOpacity(tester), 1.0);
expect(tester.getRect(find.text('Counter')).left, anyOf(716.0, 715.0)); expect(getBorderBottom(tester), 56.0);
expect(tester.getRect(find.text('Counter')).top, equals(317.5)); expect(getBorderWeight(tester), 2.0);
});
testWidgets('InputDecorator uses proper padding', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
isEmpty: false,
isFocused: true,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Hint', labelText: 'label',
labelText: 'Label', hintText: 'hint',
helperText: 'Helper',
counterText: 'Counter',
), ),
), ),
); );
// TODO(#12357): Update this test when the font metric bug is fixed to remove the anyOfs. // The hint's opacity animates from 1.0 to 0.0.
expect( // The animation's duration is 200ms.
tester.getRect(find.text('Label')).size, {
anyOf(<Size>[const Size(60.0, 12.0), const Size(61.0, 12.0)]), await tester.pump(const Duration(milliseconds: 50));
); final double hintOpacity50ms = getHintOpacity(tester);
expect(tester.getRect(find.text('Label')).left, equals(0.0)); expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0));
expect(tester.getRect(find.text('Label')).top, equals(278.5)); await tester.pump(const Duration(milliseconds: 50));
expect(tester.getRect(find.text('Hint')).size, equals(const Size(800.0, 16.0))); final double hintOpacity100ms = getHintOpacity(tester);
expect(tester.getRect(find.text('Hint')).left, equals(0.0)); expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms));
expect(tester.getRect(find.text('Hint')).top, equals(298.5)); }
expect(
tester.getRect(find.text('Helper')).size, await tester.pumpAndSettle();
anyOf(<Size>[const Size(715.0, 12.0), const Size(716.0, 12.0)]), expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
); expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getRect(find.text('Helper')).left, equals(0.0)); expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getRect(find.text('Helper')).top, equals(325.5)); expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect( expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
tester.getRect(find.text('Counter')).size, expect(tester.getTopLeft(find.text('hint')).dy, 28.0);
anyOf(<Size>[const Size(84.0, 12.0), const Size(85.0, 12.0)]), expect(tester.getBottomLeft(find.text('hint')).dy, 44.0);
); expect(getHintOpacity(tester), 0.0);
expect(tester.getRect(find.text('Counter')).left, anyOf(715.0, 716.0)); expect(getBorderBottom(tester), 56.0);
expect(tester.getRect(find.text('Counter')).top, equals(325.5)); expect(getBorderWeight(tester), 2.0);
}); });
testWidgets('InputDecorator uses proper padding when error is set', (WidgetTester tester) async { testWidgets('InputDecorator input/label/hint dense layout', (WidgetTester tester) async {
// Label is visible, hint is not (opacity 0.0).
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
isEmpty: true,
// isFocused: false (default)
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Hint', labelText: 'label',
labelText: 'Label', hintText: 'hint',
helperText: 'Helper', isDense: true,
errorText: 'Error',
counterText: 'Counter',
), ),
), ),
); );
// TODO(#12357): Update this test when the font metric bug is fixed to remove the anyOfs. // Overall height for this InputDecorator is 48dps. When the
expect( // label is "floating" (empty input or no focus) the layout is:
tester.getRect(find.text('Label')).size, //
anyOf(<Size>[const Size(60.0, 12.0), const Size(61.0, 12.0)]), // 8 - top padding
); // 12 - floating label (ahem font size 16dps * 0.75 = 12)
expect(tester.getRect(find.text('Label')).left, equals(0.0)); // 4 - floating label / input text gap
expect(tester.getRect(find.text('Label')).top, equals(278.5)); // 16 - input text (ahem font size 16dps)
expect(tester.getRect(find.text('Hint')).size, equals(const Size(800.0, 16.0))); // 8 - bottom padding
expect(tester.getRect(find.text('Hint')).left, equals(0.0)); //
expect(tester.getRect(find.text('Hint')).top, equals(298.5)); // When the label is not floating, it's vertically centered.
expect( //
tester.getRect(find.text('Error')).size, // 16 - top padding
anyOf(<Size>[const Size(715.0, 12.0), const Size(716.0, 12.0)]), // 16 - label (ahem font size 16dps)
); // 16 - bottom padding (empty input text still appears here)
expect(tester.getRect(find.text('Error')).left, equals(0.0));
expect(tester.getRect(find.text('Error')).top, equals(325.5)); // The label is not floating so it's vertically centered.
expect( expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 48.0));
tester.getRect(find.text('Counter')).size, expect(tester.getTopLeft(find.text('text')).dy, 24.0);
anyOf(<Size>[const Size(84.0, 12.0), const Size(85.0, 12.0)]), expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
expect(tester.getTopLeft(find.text('label')).dy, 16.0);
expect(tester.getBottomLeft(find.text('label')).dy, 32.0);
expect(getHintOpacity(tester), 0.0);
expect(getBorderBottom(tester), 48.0);
expect(getBorderWeight(tester), 1.0);
// Label is visible, hint is not (opacity 0.0).
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: true,
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
isDense: true,
),
),
); );
expect(tester.getRect(find.text('Counter')).left, anyOf(715.0, 716.0)); await tester.pumpAndSettle();
expect(tester.getRect(find.text('Counter')).top, equals(325.5)); expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 48.0));
expect(tester.getTopLeft(find.text('text')).dy, 24.0);
expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
expect(tester.getTopLeft(find.text('label')).dy, 8.0);
expect(tester.getBottomLeft(find.text('label')).dy, 20.0);
expect(getHintOpacity(tester), 1.0);
expect(getBorderBottom(tester), 48.0);
expect(getBorderWeight(tester), 2.0);
}); });
testWidgets('InputDecorator animates properly', (WidgetTester tester) async { testWidgets('InputDecorator with null border', (WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp( // Label is visible, hint is not (opacity 0.0).
home: const Material( await tester.pumpWidget(
child: const DefaultTextStyle( buildInputDecorator(
style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0), isEmpty: true,
child: const Center( // isFocused: false (default)
child: const TextField(
decoration: const InputDecoration( decoration: const InputDecoration(
suffixText: 'S', border: null,
prefixText: 'P',
hintText: 'Hint',
labelText: 'Label',
helperText: 'Helper',
counterText: 'Counter',
),
), ),
), ),
);
expect(getBorderWeight(tester), 0.0);
});
testWidgets('InputDecorator error/helper/counter layout', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
// isFocused: false (default)
decoration: const InputDecoration(
labelText: 'label',
helperText: 'helper',
counterText: 'counter',
filled: true,
),
),
);
// Overall height for this InputDecorator is 76dps. When the label is
// floating the layout is:
//
// 12 - top padding
// 12 - floating label (ahem font size 16dps * 0.75 = 12)
// 4 - floating label / input text gap
// 16 - input text (ahem font size 16dps)
// 12 - bottom padding
// 8 - below the border padding
// 12 - help/error/counter text (ahem font size 12dps)
//
// When the label is not floating, it's vertically centered in the space
// above the subtext:
//
// 20 - top padding
// 16 - label (ahem font size 16dps)
// 20 - bottom padding (empty input text still appears here)
// 8 - below the border padding
// 12 - help/error/counter text (ahem font size 12dps)
// isEmpty: true, the label is not floating
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 76.0));
expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
expect(tester.getBottomLeft(find.text('label')).dy, 36.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 1.0);
expect(tester.getTopLeft(find.text('helper')), const Offset(12.0, 64.0));
expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 64.0));
// If errorText is specified then the helperText isn't shown
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
decoration: const InputDecoration(
labelText: 'label',
errorText: 'error',
helperText: 'helper',
counterText: 'counter',
filled: true,
),
),
);
await tester.pumpAndSettle();
// isEmpty: false, the label _is_ floating
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 76.0));
expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 1.0);
expect(tester.getTopLeft(find.text('error')), const Offset(12.0, 64.0));
expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 64.0));
expect(find.text('helper'), findsNothing);
// Overall height for this dense layout InputDecorator is 68dps. When the
// label is floating the layout is:
//
// 8 - top padding
// 12 - floating label (ahem font size 16dps * 0.75 = 12)
// 4 - floating label / input text gap
// 16 - input text (ahem font size 16dps)
// 8 - bottom padding
// 8 - below the border padding
// 12 - help/error/counter text (ahem font size 12dps)
//
// When the label is not floating, it's vertically centered in the space
// above the subtext:
//
// 16 - top padding
// 16 - label (ahem font size 16dps)
// 16 - bottom padding (empty input text still appears here)
// 8 - below the border padding
// 12 - help/error/counter text (ahem font size 12dps)
// The layout of the error/helper/counter subtext doesn't change for dense layout.
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
decoration: const InputDecoration(
isDense: true,
labelText: 'label',
errorText: 'error',
helperText: 'helper',
counterText: 'counter',
filled: true,
), ),
), ),
));
// TODO(#12357): Update this test when the font metric bug is fixed to remove the anyOfs.
expect(
tester.getRect(find.text('Label')).size,
anyOf(<Size>[const Size(80.0, 16.0), const Size(81.0, 16.0)]),
);
expect(tester.getRect(find.text('Label')).left, equals(0.0));
expect(tester.getRect(find.text('Label')).top, equals(295.5));
expect(tester.getRect(find.text('Hint')).size, equals(const Size(800.0, 16.0)));
expect(tester.getRect(find.text('Hint')).left, equals(0.0));
expect(tester.getRect(find.text('Hint')).top, equals(295.5));
expect(
tester.getRect(find.text('Helper')).size,
anyOf(<Size>[const Size(715.0, 12.0), const Size(716.0, 12.0)]),
); );
expect(tester.getRect(find.text('Helper')).left, equals(0.0)); await tester.pumpAndSettle();
expect(tester.getRect(find.text('Helper')).top, equals(328.5));
expect(
tester.getRect(find.text('Counter')).size,
anyOf(<Size>[const Size(84.0, 12.0), const Size(85.0, 12.0)]),
);
expect(tester.getRect(find.text('Counter')).left, anyOf(715.0, 716.0));
expect(tester.getRect(find.text('Counter')).top, equals(328.5));
expect(find.text('P'), findsNothing);
expect(find.text('S'), findsNothing);
await tester.tap(find.byType(TextField)); // isEmpty: false, the label _is_ floating
await tester.pump(const Duration(milliseconds: 100)); expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 68.0));
expect(tester.getTopLeft(find.text('text')).dy, 24.0);
expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
expect(tester.getTopLeft(find.text('label')).dy, 8.0);
expect(tester.getBottomLeft(find.text('label')).dy, 20.0);
expect(getBorderBottom(tester), 48.0);
expect(getBorderWeight(tester), 1.0);
expect(tester.getTopLeft(find.text('error')), const Offset(12.0, 56.0));
expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 56.0));
expect( await tester.pumpWidget(
tester.getRect(find.text('Label')).size, buildInputDecorator(
anyOf(<Size>[const Size(60.0, 12.0), const Size(61.0, 12.0)]), isEmpty: true,
); // isFocused: false (default)
expect(tester.getRect(find.text('Label')).left, equals(0.0)); decoration: const InputDecoration(
expect(tester.getRect(find.text('Label')).top, equals(295.5)); isDense: true,
expect(tester.getRect(find.text('Hint')).size, equals(const Size(800.0, 16.0))); labelText: 'label',
expect(tester.getRect(find.text('Hint')).left, equals(0.0)); errorText: 'error',
expect(tester.getRect(find.text('Hint')).top, equals(295.5)); helperText: 'helper',
expect( counterText: 'counter',
tester.getRect(find.text('Helper')).size, filled: true,
anyOf(<Size>[const Size(715.0, 12.0), const Size(716.0, 12.0)]), ),
); ),
expect(tester.getRect(find.text('Helper')).left, equals(0.0));
expect(tester.getRect(find.text('Helper')).top, equals(328.5));
expect(
tester.getRect(find.text('Counter')).size,
anyOf(<Size>[const Size(84.0, 12.0), const Size(85.0, 12.0)]),
); );
expect(tester.getRect(find.text('Counter')).left, anyOf(715.0, 716.0)); await tester.pumpAndSettle();
expect(tester.getRect(find.text('Counter')).top, equals(328.5));
expect(find.text('P'), findsNothing);
expect(find.text('S'), findsNothing);
await tester.pump(const Duration(seconds: 1)); // isEmpty: false, the label is not floating
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 68.0));
expect(tester.getTopLeft(find.text('text')).dy, 24.0);
expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
expect(tester.getTopLeft(find.text('label')).dy, 16.0);
expect(tester.getBottomLeft(find.text('label')).dy, 32.0);
expect(getBorderBottom(tester), 48.0);
expect(getBorderWeight(tester), 1.0);
expect(tester.getTopLeft(find.text('error')), const Offset(12.0, 56.0));
expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 56.0));
});
expect( testWidgets('InputDecorator prefix/suffix', (WidgetTester tester) async {
tester.getRect(find.text('Label')).size, await tester.pumpWidget(
anyOf(<Size>[const Size(60.0, 12.0), const Size(61.0, 12.0)]), buildInputDecorator(
); // isEmpty: false (default)
expect(tester.getRect(find.text('Label')).left, equals(0.0)); // isFocused: false (default)
expect(tester.getRect(find.text('Label')).top, equals(275.5)); decoration: const InputDecoration(
expect(tester.getRect(find.text('Hint')).size, equals(const Size(800.0, 16.0))); prefixText: 'p',
expect(tester.getRect(find.text('Hint')).left, equals(0.0)); suffixText: 's',
expect(tester.getRect(find.text('Hint')).top, equals(295.5)); filled: true,
expect( ),
tester.getRect(find.text('Helper')).size, ),
anyOf(<Size>[const Size(715.0, 12.0), const Size(716.0, 12.0)]), );
);
expect(tester.getRect(find.text('Helper')).left, equals(0.0)); // Overall height for this InputDecorator is 40dps:
expect(tester.getRect(find.text('Helper')).top, equals(328.5)); // 12 - top padding
expect( // 16 - input text (ahem font size 16dps)
tester.getRect(find.text('Counter')).size, // 12 - bottom padding
anyOf(<Size>[const Size(84.0, 12.0), const Size(85.0, 12.0)]), //
// The prefix and suffix wrap the input text and are left and right justified
// respectively. They should have the same height as the input text (16).
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 40.0));
expect(tester.getSize(find.text('text')).height, 16.0);
expect(tester.getSize(find.text('p')).height, 16.0);
expect(tester.getSize(find.text('s')).height, 16.0);
expect(tester.getTopLeft(find.text('text')).dy, 12.0);
expect(tester.getTopLeft(find.text('p')).dy, 12.0);
expect(tester.getTopLeft(find.text('p')).dx, 12.0);
expect(tester.getTopLeft(find.text('s')).dy, 12.0);
expect(tester.getTopRight(find.text('s')).dx, 788.0);
// layout is a row: [p text s]
expect(tester.getTopLeft(find.text('p')).dx, 12.0);
expect(tester.getTopRight(find.text('p')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('text')).dx));
expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('s')).dx));
});
testWidgets('InputDecorator icon/prefix/suffix', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
decoration: const InputDecoration(
prefixText: 'p',
suffixText: 's',
icon: const Icon(Icons.android),
filled: true,
),
),
); );
expect(tester.getRect(find.text('Counter')).left, anyOf(715.0, 716.0));
expect(tester.getRect(find.text('Counter')).top, equals(328.5));
expect(find.text('P'), findsNothing);
expect(find.text('S'), findsNothing);
await tester.enterText(find.byType(TextField), 'Test'); // Overall height for this InputDecorator is 40dps:
await tester.pump(const Duration(milliseconds: 100)); // 12 - top padding
// 16 - input text (ahem font size 16dps)
// 12 - bottom padding
expect( expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 40.0));
tester.getRect(find.text('Label')).size, expect(tester.getSize(find.text('text')).height, 16.0);
anyOf(<Size>[const Size(60.0, 12.0), const Size(61.0, 12.0)]), expect(tester.getSize(find.text('p')).height, 16.0);
); expect(tester.getSize(find.text('s')).height, 16.0);
expect(tester.getRect(find.text('Label')).left, equals(0.0)); expect(tester.getTopLeft(find.text('text')).dy, 12.0);
expect(tester.getRect(find.text('Label')).top, equals(275.5)); expect(tester.getTopLeft(find.text('p')).dy, 12.0);
expect(tester.getRect(find.text('Hint')).size, equals(const Size(800.0, 16.0))); expect(tester.getTopLeft(find.text('s')).dy, 12.0);
expect(tester.getRect(find.text('Hint')).left, equals(0.0)); expect(tester.getTopRight(find.text('s')).dx, 788.0);
expect(tester.getRect(find.text('Hint')).top, equals(295.5)); expect(tester.getSize(find.byType(Icon)).height, 24.0);
expect(
tester.getRect(find.text('Helper')).size,
anyOf(<Size>[const Size(715.0, 12.0), const Size(716.0, 12.0)]),
);
expect(tester.getRect(find.text('Helper')).left, equals(0.0));
expect(tester.getRect(find.text('Helper')).top, equals(328.5));
expect(
tester.getRect(find.text('Counter')).size,
anyOf(<Size>[const Size(84.0, 12.0), const Size(85.0, 12.0)]),
);
expect(tester.getRect(find.text('Counter')).left, anyOf(715.0, 716.0));
expect(tester.getRect(find.text('Counter')).top, equals(328.5));
expect(
tester.getRect(find.text('P')).size,
anyOf(<Size>[const Size(17.0, 16.0), const Size(16.0, 16.0)]),
);
expect(tester.getRect(find.text('P')).left, equals(0.0));
expect(tester.getRect(find.text('P')).top, equals(295.5));
expect(
tester.getRect(find.text('S')).size,
anyOf(<Size>[const Size(17.0, 16.0), const Size(16.0, 16.0)]),
);
expect(tester.getRect(find.text('S')).left, anyOf(783.0, 784.0));
expect(tester.getRect(find.text('S')).top, equals(295.5));
await tester.pump(const Duration(seconds: 1)); // The 24dps high icon is centered on the 16dps high input line
expect(tester.getTopLeft(find.byType(Icon)).dy, 8.0);
expect( // layout is a row: [icon, p text s]
tester.getRect(find.text('Label')).size, expect(tester.getTopLeft(find.byType(Icon)).dx, 0.0);
anyOf(<Size>[const Size(60.0, 12.0), const Size(61.0, 12.0)]), expect(tester.getTopRight(find.byType(Icon)).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('p')).dx));
); expect(tester.getTopRight(find.text('p')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('text')).dx));
expect(tester.getRect(find.text('Label')).left, equals(0.0)); expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('s')).dx));
expect(tester.getRect(find.text('Label')).top, equals(275.5));
expect(tester.getRect(find.text('Hint')).size, equals(const Size(800.0, 16.0)));
expect(tester.getRect(find.text('Hint')).left, equals(0.0));
expect(tester.getRect(find.text('Hint')).top, equals(295.5));
expect(
tester.getRect(find.text('Helper')).size,
anyOf(<Size>[const Size(715.0, 12.0), const Size(716.0, 12.0)]),
);
expect(tester.getRect(find.text('Helper')).left, equals(0.0));
expect(tester.getRect(find.text('Helper')).top, equals(328.5));
expect(
tester.getRect(find.text('Counter')).size,
anyOf(<Size>[const Size(84.0, 12.0), const Size(85.0, 12.0)]),
);
expect(tester.getRect(find.text('Counter')).left, anyOf(715.0, 716.0));
expect(tester.getRect(find.text('Counter')).top, equals(328.5));
expect(
tester.getRect(find.text('P')).size,
anyOf(<Size>[const Size(17.0, 16.0), const Size(16.0, 16.0)]),
);
expect(tester.getRect(find.text('P')).left, equals(0.0));
expect(tester.getRect(find.text('P')).top, equals(295.5));
expect(
tester.getRect(find.text('S')).size,
anyOf(<Size>[const Size(17.0, 16.0), const Size(16.0, 16.0)]),
);
expect(tester.getRect(find.text('S')).left, anyOf(783.0, 784.0));
expect(tester.getRect(find.text('S')).top, equals(295.5));
}); });
testWidgets('InputDecorator animates properly', (WidgetTester tester) async { testWidgets('InputDecorator error/helper/counter RTL layout', (WidgetTester tester) async {
final Widget child = const InputDecorator( await tester.pumpWidget(
key: const Key('key'), buildInputDecorator(
decoration: const InputDecoration(), // isEmpty: false (default)
baseStyle: const TextStyle(), // isFocused: false (default)
textAlign: TextAlign.center, textDirection: TextDirection.rtl,
isFocused: false, decoration: const InputDecoration(
isEmpty: false, labelText: 'label',
child: const Placeholder(), helperText: 'helper',
); counterText: 'counter',
expect( filled: true,
child.toString(), ),
'InputDecorator-[<\'key\'>](decoration: InputDecoration(), baseStyle: TextStyle(<all styles inherited>), isFocused: false, isEmpty: false)', ),
); );
});
testWidgets('InputDecorator works with partially specified styles', (WidgetTester tester) async { // Overall height for this InputDecorator is 76dps:
await tester.pumpWidget(new MaterialApp( // 12 - top padding
home: const Material( // 12 - floating label (ahem font size 16dps * 0.75 = 12)
child: const DefaultTextStyle( // 4 - floating label / input text gap
style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0), // 16 - input text (ahem font size 16dps)
child: const Center( // 12 - bottom padding
child: const TextField( // 8 - below the border padding
// 12 - [counter helper/error] (ahem font size 12dps)
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 76.0));
expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 1.0);
expect(tester.getTopLeft(find.text('counter')), const Offset(12.0, 64.0));
expect(tester.getTopRight(find.text('helper')), const Offset(788.0, 64.0));
// If both error and helper are specified, show the error
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
textDirection: TextDirection.rtl,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'label', labelText: 'label',
labelStyle: const TextStyle(),
helperText: 'helper', helperText: 'helper',
helperStyle: const TextStyle(),
hintText: 'hint',
hintStyle: const TextStyle(),
errorText: 'error', errorText: 'error',
errorStyle: const TextStyle(),
prefixText: 'prefix',
prefixStyle: const TextStyle(),
suffixText: 'suffix',
suffixStyle: const TextStyle(),
counterText: 'counter', counterText: 'counter',
counterStyle: const TextStyle(), filled: true,
),
), ),
), ),
);
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('counter')), const Offset(12.0, 64.0));
expect(tester.getTopRight(find.text('error')), const Offset(788.0, 64.0));
expect(find.text('helper'), findsNothing);
});
testWidgets('InputDecorator prefix/suffix RTL', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
textDirection: TextDirection.rtl,
decoration: const InputDecoration(
prefixText: 'p',
suffixText: 's',
filled: true,
), ),
), ),
)); );
expect(find.text('label'), findsOneWidget); // Overall height for this InputDecorator is 40dps:
// 12 - top padding
// 16 - input text (ahem font size 16dps)
// 12 - bottom padding
// Tap to make the hint show up. expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 40.0));
await tester.tap(find.byType(TextField)); expect(tester.getSize(find.text('text')).height, 16.0);
await tester.pump(const Duration(milliseconds: 100)); expect(tester.getSize(find.text('p')).height, 16.0);
expect(tester.getSize(find.text('s')).height, 16.0);
expect(tester.getTopLeft(find.text('text')).dy, 12.0);
expect(tester.getTopLeft(find.text('p')).dy, 12.0);
expect(tester.getTopLeft(find.text('s')).dy, 12.0);
expect(find.text('hint'), findsOneWidget); // layout is a row: [s text p]
expect(tester.getTopLeft(find.text('s')).dx, 12.0);
expect(tester.getTopRight(find.text('s')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('text')).dx));
expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('p')).dx));
});
// Enter text to make the text style get used. testWidgets('InputDecorator prefix/suffix dense layout', (WidgetTester tester) async {
await tester.enterText(find.byType(TextField), 'Test'); await tester.pumpWidget(
await tester.pump(const Duration(milliseconds: 100)); buildInputDecorator(
// isEmpty: false (default)
isFocused: true,
decoration: const InputDecoration(
isDense: true,
prefixText: 'p',
suffixText: 's',
filled: true,
),
),
);
// Overall height for this InputDecorator is 32dps:
// 8 - top padding
// 16 - input text (ahem font size 16dps)
// 8 - bottom padding
//
// The only difference from normal layout for this case is that the
// padding above and below the prefix, input text, suffix, is 8 instead of 12.
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 32.0));
expect(tester.getSize(find.text('text')).height, 16.0);
expect(tester.getSize(find.text('p')).height, 16.0);
expect(tester.getSize(find.text('s')).height, 16.0);
expect(tester.getTopLeft(find.text('text')).dy, 8.0);
expect(tester.getTopLeft(find.text('p')).dy, 8.0);
expect(tester.getTopLeft(find.text('p')).dx, 12.0);
expect(tester.getTopLeft(find.text('s')).dy, 8.0);
expect(tester.getTopRight(find.text('s')).dx, 788.0);
// layout is a row: [p text s]
expect(tester.getTopLeft(find.text('p')).dx, 12.0);
expect(tester.getTopRight(find.text('p')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('text')).dx));
expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('s')).dx));
expect(getBorderBottom(tester), 32.0);
expect(getBorderWeight(tester), 2.0);
});
expect(find.text('prefix'), findsOneWidget); testWidgets('InputDecorator with empty InputDecoration', (WidgetTester tester) async {
expect(find.text('suffix'), findsOneWidget); await tester.pumpWidget(buildInputDecorator());
// Test again without error, so helper style gets used. // Overall height for this InputDecorator is 40dps:
await tester.pumpWidget(new MaterialApp( // 12 - top padding
home: const Material( // 16 - input text (ahem font size 16dps)
child: const DefaultTextStyle( // 12 - bottom padding
style: const TextStyle(),
child: const Center( expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 40.0));
child: const TextField( expect(tester.getSize(find.text('text')).height, 16.0);
decoration: const InputDecoration( expect(tester.getTopLeft(find.text('text')).dy, 12.0);
labelText: 'label', expect(getBorderBottom(tester), 40.0);
labelStyle: const TextStyle(), expect(getBorderWeight(tester), 1.0);
helperText: 'helper', });
helperStyle: const TextStyle(),
testWidgets('InputDecorator.collapsed', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default),
// isFocused: false (default)
decoration: const InputDecoration.collapsed(
hintText: 'hint', hintText: 'hint',
hintStyle: const TextStyle(),
prefixText: 'prefix',
prefixStyle: const TextStyle(),
suffixText: 'suffix',
suffixStyle: const TextStyle(),
counterText: 'counter',
counterStyle: const TextStyle(),
), ),
), ),
);
// Overall height for this InputDecorator is 16dps:
// 16 - input text (ahem font size 16dps)
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 16.0));
expect(tester.getSize(find.text('text')).height, 16.0);
expect(tester.getTopLeft(find.text('text')).dy, 0.0);
expect(getHintOpacity(tester), 0.0);
expect(getBorderWeight(tester), 0.0);
// The hint should appear
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: true,
decoration: const InputDecoration.collapsed(
hintText: 'hint',
), ),
), ),
);
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 16.0));
expect(tester.getSize(find.text('text')).height, 16.0);
expect(tester.getTopLeft(find.text('text')).dy, 0.0);
expect(tester.getSize(find.text('hint')).height, 16.0);
expect(tester.getTopLeft(find.text('hint')).dy, 0.0);
expect(getBorderWeight(tester), 0.0);
});
testWidgets('InputDecorator with baseStyle', (WidgetTester tester) async {
// Setting the baseStyle of the InputDecoration and the style of the input
// text child to a smaller font reduces the InputDecoration's vertical size.
const TextStyle style = const TextStyle(fontFamily: 'Ahem', fontSize: 10.0);
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: false,
baseStyle: style,
decoration: const InputDecoration(
hintText: 'hint',
labelText: 'label',
), ),
)); child: const Text('text', style: style),
),
);
// Overall height for this InputDecorator is 45.5dps. When the label is
// floating the layout is:
//
// 12 - top padding
// 7.5 - floating label (ahem font size 10dps * 0.75 = 7.5)
// 4 - floating label / input text gap
// 10 - input text (ahem font size 10dps)
// 12 - bottom padding
//
// When the label is not floating, it's vertically centered.
//
// 17.75 - top padding
// 10 - label (ahem font size 10dps)
// 17.75 - bottom padding (empty input text still appears here)
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 45.5));
expect(tester.getSize(find.text('hint')).height, 10.0);
expect(tester.getSize(find.text('label')).height, 10.0);
expect(tester.getSize(find.text('text')).height, 10.0);
expect(tester.getTopLeft(find.text('hint')).dy, 23.5);
expect(tester.getTopLeft(find.text('label')).dy, 17.75);
expect(tester.getTopLeft(find.text('text')).dy, 23.5);
});
expect(find.text('label'), findsOneWidget); testWidgets('InputDecorator with empty style overrides', (WidgetTester tester) async {
expect(find.text('helper'), findsOneWidget); // Same as not specifying any style overrides
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
helperText: 'helper',
counterText: 'counter',
labelStyle: const TextStyle(),
hintStyle: const TextStyle(),
errorStyle: const TextStyle(),
helperStyle: const TextStyle(),
filled: true,
),
),
);
// Overall height for this InputDecorator is 76dps. When the label is
// floating the layout is:
// 12 - top padding
// 12 - floating label (ahem font size 16dps * 0.75 = 12)
// 4 - floating label / input text gap
// 16 - input text (ahem font size 16dps)
// 12 - bottom padding
// 8 - below the border padding
// 12 - help/error/counter text (ahem font size 12dps)
// Label is floating because isEmpty is false.
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 76.0));
expect(tester.getTopLeft(find.text('text')).dy, 28.0);
expect(tester.getBottomLeft(find.text('text')).dy, 44.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(getBorderBottom(tester), 56.0);
expect(getBorderWeight(tester), 1.0);
expect(tester.getTopLeft(find.text('helper')), const Offset(12.0, 64.0));
expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 64.0));
});
testWidgets('InputDecorator.toString()', (WidgetTester tester) async {
final Widget child = const InputDecorator(
key: const Key('key'),
decoration: const InputDecoration(),
baseStyle: const TextStyle(),
textAlign: TextAlign.center,
isFocused: false,
isEmpty: false,
child: const Placeholder(),
);
expect(
child.toString(),
"InputDecorator-[<'key'>](decoration: InputDecoration(border: UnderlineInputBorder()), baseStyle: TextStyle(<all styles inherited>), isFocused: false, isEmpty: false)",
);
}); });
} }
...@@ -104,6 +104,15 @@ Future<Null> skipPastScrollingAnimation(WidgetTester tester) async { ...@@ -104,6 +104,15 @@ Future<Null> skipPastScrollingAnimation(WidgetTester tester) async {
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
} }
double getOpacity(WidgetTester tester, Finder finder) {
return tester.widget<Opacity>(
find.ancestor(
of: finder,
matching: find.byType(Opacity),
)
).opacity;
}
void main() { void main() {
final MockClipboard mockClipboard = new MockClipboard(); final MockClipboard mockClipboard = new MockClipboard();
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall); SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
...@@ -1006,33 +1015,26 @@ void main() { ...@@ -1006,33 +1015,26 @@ void main() {
); );
// Neither the prefix or the suffix should initially be visible, only the hint. // Neither the prefix or the suffix should initially be visible, only the hint.
expect(find.text('Prefix'), findsNothing); expect(getOpacity(tester, find.text('Prefix')), 0.0);
expect(find.text('Suffix'), findsNothing); expect(getOpacity(tester, find.text('Suffix')), 0.0);
expect(find.text('Hint'), findsOneWidget); expect(getOpacity(tester, find.text('Hint')), 1.0);
await tester.tap(find.byKey(secondKey)); await tester.tap(find.byKey(secondKey));
await tester.pump(); await tester.pumpAndSettle();
// Focus the Input. The hint should display, but not the prefix and suffix. // Focus the Input. The hint, prefix, and suffix should appear
expect(find.text('Prefix'), findsNothing); expect(getOpacity(tester, find.text('Prefix')), 1.0);
expect(find.text('Suffix'), findsNothing); expect(getOpacity(tester, find.text('Suffix')), 1.0);
expect(find.text('Hint'), findsOneWidget); expect(getOpacity(tester, find.text('Hint')), 1.0);
// Enter some text, and the hint should disappear and the prefix and suffix // Enter some text, and the hint should disappear and the prefix and suffix
// should appear. // should continue to be visible
await tester.enterText(find.byKey(secondKey), 'Hi'); await tester.enterText(find.byKey(secondKey), 'Hi');
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Prefix'), findsOneWidget); expect(getOpacity(tester, find.text('Prefix')), 1.0);
expect(find.text('Suffix'), findsOneWidget); expect(getOpacity(tester, find.text('Suffix')), 1.0);
expect(getOpacity(tester, find.text('Hint')), 0.0);
// It's onstage, but animated to zero opacity.
expect(find.text('Hint'), findsOneWidget);
final Element target = tester.element(find.text('Hint'));
final Opacity opacity = target.ancestorWidgetOfExactType(Opacity);
expect(opacity, isNotNull);
expect(opacity.opacity, equals(0.0));
// Check and make sure that the right styles were applied. // Check and make sure that the right styles were applied.
final Text prefixText = tester.widget(find.text('Prefix')); final Text prefixText = tester.widget(find.text('Prefix'));
...@@ -1077,27 +1079,25 @@ void main() { ...@@ -1077,27 +1079,25 @@ void main() {
), ),
); );
// Not focused. The prefix should not display, but the label should. // Not focused. The prefix and suffix should not appear, but the label should.
expect(find.text('Prefix'), findsNothing); expect(getOpacity(tester, find.text('Prefix')), 0.0);
expect(find.text('Suffix'), findsNothing); expect(getOpacity(tester, find.text('Suffix')), 0.0);
expect(find.text('Label'), findsOneWidget); expect(find.text('Label'), findsOneWidget);
// Focus the input. The label, prefix, and suffix should appear.
await tester.tap(find.byKey(secondKey)); await tester.tap(find.byKey(secondKey));
await tester.pump(); await tester.pumpAndSettle();
// Focus the input. The label should display, and also the prefix. expect(getOpacity(tester, find.text('Prefix')), 1.0);
expect(find.text('Prefix'), findsOneWidget); expect(getOpacity(tester, find.text('Suffix')), 1.0);
expect(find.text('Suffix'), findsOneWidget);
expect(find.text('Label'), findsOneWidget); expect(find.text('Label'), findsOneWidget);
// Enter some text, and the label should stay and the prefix should // Enter some text. The label, prefix, and suffix should remain visible.
// remain.
await tester.enterText(find.byKey(secondKey), 'Hi'); await tester.enterText(find.byKey(secondKey), 'Hi');
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Prefix'), findsOneWidget); expect(getOpacity(tester, find.text('Prefix')), 1.0);
expect(find.text('Suffix'), findsOneWidget); expect(getOpacity(tester, find.text('Suffix')), 1.0);
expect(find.text('Label'), findsOneWidget); expect(find.text('Label'), findsOneWidget);
// Check and make sure that the right styles were applied. // Check and make sure that the right styles were applied.
...@@ -1148,21 +1148,25 @@ void main() { ...@@ -1148,21 +1148,25 @@ void main() {
expect(newPos.dy, lessThan(pos.dy)); expect(newPos.dy, lessThan(pos.dy));
}); });
testWidgets('No space between Input icon and text', (WidgetTester tester) async { testWidgets('Icon is separated from input/label by 16+12', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const TextField( child: const TextField(
decoration: const InputDecoration( decoration: const InputDecoration(
icon: const Icon(Icons.phone), icon: const Icon(Icons.phone),
labelText: 'label', labelText: 'label',
filled: true,
), ),
), ),
), ),
); );
final double iconRight = tester.getTopRight(find.byType(Icon)).dx; final double iconRight = tester.getTopRight(find.byType(Icon)).dx;
expect(iconRight, equals(tester.getTopLeft(find.text('label')).dx)); // Per https://material.io/guidelines/components/text-fields.html#text-fields-layout
expect(iconRight, equals(tester.getTopLeft(find.byType(EditableText)).dx)); // There's a 16 dps gap between the right edge of the icon and the text field's
// container, and the 12dps more padding between the left edge of the container
// and the left edge of the input and label.
expect(iconRight + 28.0, equals(tester.getTopLeft(find.text('label')).dx));
expect(iconRight + 28.0, equals(tester.getTopLeft(find.byType(EditableText)).dx));
}); });
testWidgets('Collapsed hint text placement', (WidgetTester tester) async { testWidgets('Collapsed hint text placement', (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment