Unverified Commit 9f21ae0d authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Text field focus and hover support. (#32776)

This adds support for an animated focusColor and hoverColor to InputDecorator. This color will blend with the background over a fade in period whenever the InputDecorator is focused or hovered, respectively.

It also adds a Listener to the TextField to listen for hover events.
parent 8c05e8c1
...@@ -56,6 +56,8 @@ class _HoverDemoState extends State<HoverDemo> { ...@@ -56,6 +56,8 @@ class _HoverDemoState extends State<HoverDemo> {
child: Builder(builder: (BuildContext context) { child: Builder(builder: (BuildContext context) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[ children: <Widget>[
RaisedButton( RaisedButton(
onPressed: () => print('Button pressed.'), onPressed: () => print('Button pressed.'),
...@@ -72,6 +74,8 @@ class _HoverDemoState extends State<HoverDemo> { ...@@ -72,6 +74,8 @@ class _HoverDemoState extends State<HoverDemo> {
icon: const Icon(Icons.access_alarm), icon: const Icon(Icons.access_alarm),
focusColor: Colors.deepOrangeAccent, focusColor: Colors.deepOrangeAccent,
), ),
],
),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: TextField( child: TextField(
......
...@@ -62,13 +62,17 @@ class _InputBorderTween extends Tween<InputBorder> { ...@@ -62,13 +62,17 @@ class _InputBorderTween extends Tween<InputBorder> {
// Passes the _InputBorderGap parameters along to an InputBorder's paint method. // Passes the _InputBorderGap parameters along to an InputBorder's paint method.
class _InputBorderPainter extends CustomPainter { class _InputBorderPainter extends CustomPainter {
_InputBorderPainter({ _InputBorderPainter({
Listenable repaint, @required Listenable repaint,
this.borderAnimation, @required this.borderAnimation,
this.border, @required this.border,
this.gapAnimation, @required this.gapAnimation,
this.gap, @required this.gap,
this.textDirection, @required this.textDirection,
this.fillColor, @required this.fillColor,
@required this.focusAnimation,
@required this.focusColorTween,
@required this.hoverAnimation,
@required this.hoverColorTween,
}) : super(repaint: repaint); }) : super(repaint: repaint);
final Animation<double> borderAnimation; final Animation<double> borderAnimation;
...@@ -77,17 +81,28 @@ class _InputBorderPainter extends CustomPainter { ...@@ -77,17 +81,28 @@ class _InputBorderPainter extends CustomPainter {
final _InputBorderGap gap; final _InputBorderGap gap;
final TextDirection textDirection; final TextDirection textDirection;
final Color fillColor; final Color fillColor;
final ColorTween focusColorTween;
final Animation<double> focusAnimation;
final ColorTween hoverColorTween;
final Animation<double> hoverAnimation;
Color get blendedColor {
return Color.alphaBlend(
hoverColorTween.evaluate(hoverAnimation),
Color.alphaBlend(focusColorTween.evaluate(focusAnimation), fillColor),
);
}
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final InputBorder borderValue = border.evaluate(borderAnimation); final InputBorder borderValue = border.evaluate(borderAnimation);
final Rect canvasRect = Offset.zero & size; final Rect canvasRect = Offset.zero & size;
final Color blendedFillColor = blendedColor;
if (fillColor.alpha > 0) { if (blendedFillColor.alpha > 0) {
canvas.drawPath( canvas.drawPath(
borderValue.getOuterPath(canvasRect, textDirection: textDirection), borderValue.getOuterPath(canvasRect, textDirection: textDirection),
Paint() Paint()
..color = fillColor ..color = blendedFillColor
..style = PaintingStyle.fill, ..style = PaintingStyle.fill,
); );
} }
...@@ -105,6 +120,8 @@ class _InputBorderPainter extends CustomPainter { ...@@ -105,6 +120,8 @@ class _InputBorderPainter extends CustomPainter {
@override @override
bool shouldRepaint(_InputBorderPainter oldPainter) { bool shouldRepaint(_InputBorderPainter oldPainter) {
return borderAnimation != oldPainter.borderAnimation return borderAnimation != oldPainter.borderAnimation
|| focusAnimation != oldPainter.focusAnimation
|| hoverAnimation != oldPainter.hoverAnimation
|| gapAnimation != oldPainter.gapAnimation || gapAnimation != oldPainter.gapAnimation
|| border != oldPainter.border || border != oldPainter.border
|| gap != oldPainter.gap || gap != oldPainter.gap
...@@ -123,6 +140,10 @@ class _BorderContainer extends StatefulWidget { ...@@ -123,6 +140,10 @@ class _BorderContainer extends StatefulWidget {
@required this.gap, @required this.gap,
@required this.gapAnimation, @required this.gapAnimation,
@required this.fillColor, @required this.fillColor,
@required this.focusColor,
@required this.isFocused,
@required this.hoverColor,
@required this.isHovering,
this.child, this.child,
}) : assert(border != null), }) : assert(border != null),
assert(gap != null), assert(gap != null),
...@@ -133,20 +154,44 @@ class _BorderContainer extends StatefulWidget { ...@@ -133,20 +154,44 @@ class _BorderContainer extends StatefulWidget {
final _InputBorderGap gap; final _InputBorderGap gap;
final Animation<double> gapAnimation; final Animation<double> gapAnimation;
final Color fillColor; final Color fillColor;
final Color focusColor;
final bool isFocused;
final Color hoverColor;
final bool isHovering;
final Widget child; final Widget child;
@override @override
_BorderContainerState createState() => _BorderContainerState(); _BorderContainerState createState() => _BorderContainerState();
} }
class _BorderContainerState extends State<_BorderContainer> with SingleTickerProviderStateMixin { class _BorderContainerState extends State<_BorderContainer> with TickerProviderStateMixin {
static const Duration _kFocusInDuration = Duration(milliseconds: 45);
static const Duration _kHoverDuration = Duration(milliseconds: 15);
AnimationController _controller; AnimationController _controller;
AnimationController _focusColorController;
AnimationController _hoverColorController;
Animation<double> _borderAnimation; Animation<double> _borderAnimation;
_InputBorderTween _border; _InputBorderTween _border;
Animation<double> _focusAnimation;
ColorTween _focusColorTween;
Animation<double> _hoverAnimation;
ColorTween _hoverColorTween;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_focusColorController = AnimationController(
duration: _kFocusInDuration,
// TODO(gspencer): use reverseDuration set to 15ms, once available.
value: widget.isFocused ? 1.0 : 0.0,
vsync: this,
);
_hoverColorController = AnimationController(
duration: _kHoverDuration,
value: widget.isHovering ? 1.0 : 0.0,
vsync: this,
);
_controller = AnimationController( _controller = AnimationController(
duration: _kTransitionDuration, duration: _kTransitionDuration,
vsync: this, vsync: this,
...@@ -159,11 +204,23 @@ class _BorderContainerState extends State<_BorderContainer> with SingleTickerPro ...@@ -159,11 +204,23 @@ class _BorderContainerState extends State<_BorderContainer> with SingleTickerPro
begin: widget.border, begin: widget.border,
end: widget.border, end: widget.border,
); );
_focusAnimation = CurvedAnimation(
parent: _focusColorController,
curve: Curves.linear,
);
_focusColorTween = ColorTween(begin: Colors.transparent, end: widget.focusColor);
_hoverAnimation = CurvedAnimation(
parent: _hoverColorController,
curve: Curves.linear,
);
_hoverColorTween = ColorTween(begin: Colors.transparent, end: widget.hoverColor);
} }
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
_focusColorController.dispose();
_hoverColorController.dispose();
super.dispose(); super.dispose();
} }
...@@ -179,19 +236,48 @@ class _BorderContainerState extends State<_BorderContainer> with SingleTickerPro ...@@ -179,19 +236,48 @@ class _BorderContainerState extends State<_BorderContainer> with SingleTickerPro
..value = 0.0 ..value = 0.0
..forward(); ..forward();
} }
if (widget.focusColor != oldWidget.focusColor) {
_focusColorTween = ColorTween(begin: Colors.transparent, end: widget.focusColor);
}
if (widget.isFocused != oldWidget.isFocused) {
if (widget.isFocused) {
_focusColorController.forward();
} else {
_focusColorController.reverse();
}
}
if (widget.hoverColor != oldWidget.hoverColor) {
_hoverColorTween = ColorTween(begin: Colors.transparent, end: widget.hoverColor);
}
if (widget.isHovering != oldWidget.isHovering) {
if (widget.isHovering) {
_hoverColorController.forward();
} else {
_hoverColorController.reverse();
}
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomPaint( return CustomPaint(
foregroundPainter: _InputBorderPainter( foregroundPainter: _InputBorderPainter(
repaint: Listenable.merge(<Listenable>[_borderAnimation, widget.gap]), repaint: Listenable.merge(<Listenable>[
_borderAnimation,
widget.gap,
_focusColorController,
_hoverColorController,
]),
borderAnimation: _borderAnimation, borderAnimation: _borderAnimation,
border: _border, border: _border,
gapAnimation: widget.gapAnimation, gapAnimation: widget.gapAnimation,
gap: widget.gap, gap: widget.gap,
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
fillColor: widget.fillColor, fillColor: widget.fillColor,
focusColorTween: _focusColorTween,
focusAnimation: _focusAnimation,
hoverColorTween: _hoverColorTween,
hoverAnimation: _hoverAnimation,
), ),
child: widget.child, child: widget.child,
); );
...@@ -1562,17 +1648,21 @@ class InputDecorator extends StatefulWidget { ...@@ -1562,17 +1648,21 @@ class InputDecorator extends StatefulWidget {
/// Creates a widget that displays a border, labels, and icons, /// Creates a widget that displays a border, labels, and icons,
/// for a [TextField]. /// for a [TextField].
/// ///
/// The [isFocused] and [isEmpty] arguments must not be null. /// The [isFocused], [isHovering], [expands], and [isEmpty] arguments must not
/// be null.
const InputDecorator({ const InputDecorator({
Key key, Key key,
this.decoration, this.decoration,
this.baseStyle, this.baseStyle,
this.textAlign, this.textAlign,
this.isFocused = false, this.isFocused = false,
this.isHovering = false,
this.expands = false, this.expands = false,
this.isEmpty = false, this.isEmpty = false,
this.child, this.child,
}) : assert(isFocused != null), }) : assert(isFocused != null),
assert(isHovering != null),
assert(expands != null),
assert(isEmpty != null), assert(isEmpty != null),
super(key: key); super(key: key);
...@@ -1598,12 +1688,35 @@ class InputDecorator extends StatefulWidget { ...@@ -1598,12 +1688,35 @@ class InputDecorator extends StatefulWidget {
/// Whether the input field has focus. /// Whether the input field has focus.
/// ///
/// Determines the position of the label text and the color and weight /// Determines the position of the label text and the color and weight of the
/// of the border. /// border, as well as the container fill color, which is a blend of
/// [InputDecoration.focusColor] with [InputDecoration.fillColor] when
/// focused, and [InputDecoration.fillColor] when not.
/// ///
/// Defaults to false. /// Defaults to false.
///
/// See also:
///
/// - [InputDecoration.hoverColor], which is also blended into the focus
/// color and fill color when the [isHovering] is true to produce the final
/// color.
final bool isFocused; final bool isFocused;
/// Whether the input field is being hovered over by a mouse pointer.
///
/// Determines the container fill color, which is a blend of
/// [InputDecoration.hoverColor] with [InputDecoration.fillColor] when
/// true, and [InputDecoration.fillColor] when not.
///
/// Defaults to false.
///
/// See also:
///
/// - [InputDecoration.focusColor], which is also blended into the hover
/// color and fill color when [isFocused] is true to produce the final
/// color.
final bool isHovering;
/// If true, the height of the input field will be as large as possible. /// If true, the height of the input field will be as large as possible.
/// ///
/// If wrapped in a widget that constrains its child's height, like Expanded /// If wrapped in a widget that constrains its child's height, like Expanded
...@@ -1710,6 +1823,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -1710,6 +1823,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
TextAlign get textAlign => widget.textAlign; TextAlign get textAlign => widget.textAlign;
bool get isFocused => widget.isFocused; bool get isFocused => widget.isFocused;
bool get isHovering => widget.isHovering;
bool get isEmpty => widget.isEmpty; bool get isEmpty => widget.isEmpty;
@override @override
...@@ -1747,6 +1861,27 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -1747,6 +1861,27 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
return themeData.hintColor; return themeData.hintColor;
} }
Color _getBorderColor(ThemeData themeData) {
if (isFocused) {
switch (themeData.brightness) {
case Brightness.dark:
return themeData.accentColor;
case Brightness.light:
return themeData.primaryColor;
}
}
if (decoration.filled) {
return themeData.hintColor;
}
if (isHovering) {
// TODO(gspencer): Find out the actual value here from the spec writers.
final Color hoverColor = decoration.hoverColor ?? themeData.inputDecorationTheme?.hoverColor ?? themeData.hoverColor;
return Color.alphaBlend(hoverColor.withOpacity(0.16), themeData.colorScheme.onSurface.withOpacity(0.12));
}
// TODO(gspencer): Find out the actual value here from the spec writers.
return themeData.colorScheme.onSurface.withOpacity(0.12);
}
Color _getFillColor(ThemeData themeData) { Color _getFillColor(ThemeData themeData) {
if (decoration.filled != true) // filled == null same as filled == false if (decoration.filled != true) // filled == null same as filled == false
return Colors.transparent; return Colors.transparent;
...@@ -1769,6 +1904,18 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -1769,6 +1904,18 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
return lightEnabled; return lightEnabled;
} }
Color _getFocusColor(ThemeData themeData) {
if (decoration.filled != true) // filled == null same as filled == false
return Colors.transparent;
return decoration.focusColor ?? themeData.inputDecorationTheme?.focusColor ?? themeData.focusColor;
}
Color _getHoverColor(ThemeData themeData) {
if (isFocused || decoration.filled != true) // filled == null same as filled == false
return Colors.transparent;
return decoration.hoverColor ?? themeData.inputDecorationTheme?.hoverColor ?? themeData.hoverColor;
}
Color _getDefaultIconColor(ThemeData themeData) { Color _getDefaultIconColor(ThemeData themeData) {
if (!decoration.enabled) if (!decoration.enabled)
return themeData.disabledColor; return themeData.disabledColor;
...@@ -1827,7 +1974,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -1827,7 +1974,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
Color borderColor; Color borderColor;
if (decoration.enabled) { if (decoration.enabled) {
borderColor = decoration.errorText == null borderColor = decoration.errorText == null
? _getActiveColor(themeData) ? _getBorderColor(themeData)
: themeData.errorColor; : themeData.errorColor;
} else { } else {
borderColor = (decoration.filled == true && decoration.border?.isOutline != true) borderColor = (decoration.filled == true && decoration.border?.isOutline != true)
...@@ -1880,6 +2027,10 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -1880,6 +2027,10 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
gap: _borderGap, gap: _borderGap,
gapAnimation: _floatingLabelController.view, gapAnimation: _floatingLabelController.view,
fillColor: _getFillColor(themeData), fillColor: _getFillColor(themeData),
focusColor: _getFocusColor(themeData),
hoverColor: _getHoverColor(themeData),
isFocused: isFocused,
isHovering: isHovering,
); );
final TextStyle inlineLabelStyle = inlineStyle.merge(decoration.labelStyle); final TextStyle inlineLabelStyle = inlineStyle.merge(decoration.labelStyle);
...@@ -2114,6 +2265,8 @@ class InputDecoration { ...@@ -2114,6 +2265,8 @@ class InputDecoration {
this.counterStyle, this.counterStyle,
this.filled, this.filled,
this.fillColor, this.fillColor,
this.focusColor,
this.hoverColor,
this.errorBorder, this.errorBorder,
this.focusedBorder, this.focusedBorder,
this.focusedErrorBorder, this.focusedErrorBorder,
...@@ -2139,6 +2292,8 @@ class InputDecoration { ...@@ -2139,6 +2292,8 @@ class InputDecoration {
this.hintStyle, this.hintStyle,
this.filled = false, this.filled = false,
this.fillColor, this.fillColor,
this.focusColor,
this.hoverColor,
this.border = InputBorder.none, this.border = InputBorder.none,
this.enabled = true, this.enabled = true,
}) : assert(enabled != null), }) : assert(enabled != null),
...@@ -2274,9 +2429,9 @@ class InputDecoration { ...@@ -2274,9 +2429,9 @@ class InputDecoration {
/// Whether the label floats on focus. /// Whether the label floats on focus.
/// ///
/// If this is false, the placeholder disappears when the input has focus or /// If this is false, the placeholder disappears when the input has focus or
/// inputted text. /// text has been entered.
/// If this is true, the placeholder will rise to the top of the input when /// If this is true, the placeholder will rise to the top of the input when
/// the input has focus or inputted text. /// the input has focus or text has been entered.
/// ///
/// Defaults to true. /// Defaults to true.
final bool hasFloatingPlaceholder; final bool hasFloatingPlaceholder;
...@@ -2476,6 +2631,10 @@ class InputDecoration { ...@@ -2476,6 +2631,10 @@ class InputDecoration {
/// If true the decoration's container is filled with [fillColor]. /// If true the decoration's container is filled with [fillColor].
/// ///
/// When [isFocused] is true, the [focusColor] is also blended into the final
/// fill color. When [isHovering] is true, the [hoverColor] is also blended
/// into the final fill color.
///
/// Typically this field set to true if [border] is an /// Typically this field set to true if [border] is an
/// [UnderlineInputBorder]. /// [UnderlineInputBorder].
/// ///
...@@ -2487,7 +2646,11 @@ class InputDecoration { ...@@ -2487,7 +2646,11 @@ class InputDecoration {
/// This property is false by default. /// This property is false by default.
final bool filled; final bool filled;
/// The color to fill the decoration's container with, if [filled] is true. /// The base fill color of the decoration's container color.
///
/// When [isFocused] is true, the [focusColor] is also blended into the final
/// fill color. When [isHovering] is true, the [hoverColor] is also blended
/// into the final fill color.
/// ///
/// By default the fillColor is based on the current [Theme]. /// By default the fillColor is based on the current [Theme].
/// ///
...@@ -2495,8 +2658,42 @@ class InputDecoration { ...@@ -2495,8 +2658,42 @@ class InputDecoration {
/// true and bordered per the [border]. It's the area adjacent to /// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText], /// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText]. /// [errorText], and [counterText].
///
/// This color is blended with [focusColor] if the decoration is focused.
final Color fillColor; final Color fillColor;
/// The color to blend with [fillColor] and fill the decoration's container
/// with, if [filled] is true and the container has input focus.
///
/// When [isHovering] is true, the [hoverColor] is also blended into the final
/// fill color.
///
/// By default the [focusColor] is based on the current [Theme].
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
final Color focusColor;
/// The color of the focus highlight for the decoration shown if the container
/// is being hovered over by a mouse.
///
/// If [filled] is true, the color is blended with [fillColor] and fills the
/// decoration's container. When [isFocused] is true, the [focusColor] is also
/// blended into the final fill color.
///
/// If [filled] is false, and [isFocused] is false, the color is blended over
/// the [enabledBorder]'s color.
///
/// By default the [hoverColor] is based on the current [Theme].
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
final Color hoverColor;
/// The border to display when the [InputDecorator] does not have the focus and /// The border to display when the [InputDecorator] does not have the focus and
/// is showing an error. /// is showing an error.
/// ///
...@@ -2703,6 +2900,8 @@ class InputDecoration { ...@@ -2703,6 +2900,8 @@ class InputDecoration {
TextStyle counterStyle, TextStyle counterStyle,
bool filled, bool filled,
Color fillColor, Color fillColor,
Color focusColor,
Color hoverColor,
InputBorder errorBorder, InputBorder errorBorder,
InputBorder focusedBorder, InputBorder focusedBorder,
InputBorder focusedErrorBorder, InputBorder focusedErrorBorder,
...@@ -2741,6 +2940,8 @@ class InputDecoration { ...@@ -2741,6 +2940,8 @@ class InputDecoration {
counterStyle: counterStyle ?? this.counterStyle, counterStyle: counterStyle ?? this.counterStyle,
filled: filled ?? this.filled, filled: filled ?? this.filled,
fillColor: fillColor ?? this.fillColor, fillColor: fillColor ?? this.fillColor,
focusColor: focusColor ?? this.focusColor,
hoverColor: hoverColor ?? this.hoverColor,
errorBorder: errorBorder ?? this.errorBorder, errorBorder: errorBorder ?? this.errorBorder,
focusedBorder: focusedBorder ?? this.focusedBorder, focusedBorder: focusedBorder ?? this.focusedBorder,
focusedErrorBorder: focusedErrorBorder ?? this.focusedErrorBorder, focusedErrorBorder: focusedErrorBorder ?? this.focusedErrorBorder,
...@@ -2773,6 +2974,8 @@ class InputDecoration { ...@@ -2773,6 +2974,8 @@ class InputDecoration {
counterStyle: counterStyle ?? theme.counterStyle, counterStyle: counterStyle ?? theme.counterStyle,
filled: filled ?? theme.filled, filled: filled ?? theme.filled,
fillColor: fillColor ?? theme.fillColor, fillColor: fillColor ?? theme.fillColor,
focusColor: focusColor ?? theme.focusColor,
hoverColor: hoverColor ?? theme.hoverColor,
errorBorder: errorBorder ?? theme.errorBorder, errorBorder: errorBorder ?? theme.errorBorder,
focusedBorder: focusedBorder ?? theme.focusedBorder, focusedBorder: focusedBorder ?? theme.focusedBorder,
focusedErrorBorder: focusedErrorBorder ?? theme.focusedErrorBorder, focusedErrorBorder: focusedErrorBorder ?? theme.focusedErrorBorder,
...@@ -2818,6 +3021,8 @@ class InputDecoration { ...@@ -2818,6 +3021,8 @@ class InputDecoration {
&& typedOther.counterStyle == counterStyle && typedOther.counterStyle == counterStyle
&& typedOther.filled == filled && typedOther.filled == filled
&& typedOther.fillColor == fillColor && typedOther.fillColor == fillColor
&& typedOther.focusColor == focusColor
&& typedOther.hoverColor == hoverColor
&& typedOther.errorBorder == errorBorder && typedOther.errorBorder == errorBorder
&& typedOther.focusedBorder == focusedBorder && typedOther.focusedBorder == focusedBorder
&& typedOther.focusedErrorBorder == focusedErrorBorder && typedOther.focusedErrorBorder == focusedErrorBorder
...@@ -2831,9 +3036,7 @@ class InputDecoration { ...@@ -2831,9 +3036,7 @@ class InputDecoration {
@override @override
int get hashCode { int get hashCode {
// Split into several hashValues calls because the hashValues function is final List<Object> values = <Object>[
// limited to 20 parameters.
return hashValues(
icon, icon,
labelText, labelText,
labelStyle, labelStyle,
...@@ -2851,9 +3054,10 @@ class InputDecoration { ...@@ -2851,9 +3054,10 @@ class InputDecoration {
isCollapsed, isCollapsed,
filled, filled,
fillColor, fillColor,
focusColor,
hoverColor,
border, border,
enabled, enabled,
hashValues(
prefixIcon, prefixIcon,
prefix, prefix,
prefixText, prefixText,
...@@ -2865,21 +3069,17 @@ class InputDecoration { ...@@ -2865,21 +3069,17 @@ class InputDecoration {
counter, counter,
counterText, counterText,
counterStyle, counterStyle,
filled,
fillColor,
errorBorder, errorBorder,
focusedBorder, focusedBorder,
focusedErrorBorder, focusedErrorBorder,
disabledBorder, disabledBorder,
enabledBorder, enabledBorder,
border, border,
hashValues(
enabled, enabled,
semanticCounterText, semanticCounterText,
alignLabelWithHint, alignLabelWithHint,
), ];
), return hashList(values);
);
} }
@override @override
...@@ -2935,6 +3135,10 @@ class InputDecoration { ...@@ -2935,6 +3135,10 @@ class InputDecoration {
description.add('filled: true'); description.add('filled: true');
if (fillColor != null) if (fillColor != null)
description.add('fillColor: $fillColor'); description.add('fillColor: $fillColor');
if (focusColor != null)
description.add('focusColor: $focusColor');
if (hoverColor != null)
description.add('hoverColor: $hoverColor');
if (errorBorder != null) if (errorBorder != null)
description.add('errorBorder: $errorBorder'); description.add('errorBorder: $errorBorder');
if (focusedBorder != null) if (focusedBorder != null)
...@@ -2988,6 +3192,8 @@ class InputDecorationTheme extends Diagnosticable { ...@@ -2988,6 +3192,8 @@ class InputDecorationTheme extends Diagnosticable {
this.counterStyle, this.counterStyle,
this.filled = false, this.filled = false,
this.fillColor, this.fillColor,
this.focusColor,
this.hoverColor,
this.errorBorder, this.errorBorder,
this.focusedBorder, this.focusedBorder,
this.focusedErrorBorder, this.focusedErrorBorder,
...@@ -3041,9 +3247,9 @@ class InputDecorationTheme extends Diagnosticable { ...@@ -3041,9 +3247,9 @@ class InputDecorationTheme extends Diagnosticable {
/// Whether the placeholder text floats to become a label on focus. /// Whether the placeholder text floats to become a label on focus.
/// ///
/// If this is false, the placeholder disappears when the input has focus or /// If this is false, the placeholder disappears when the input has focus or
/// inputted text. /// text has been entered.
/// If this is true, the placeholder will rise to the top of the input when /// If this is true, the placeholder will rise to the top of the input when
/// the input has focus or inputted text. /// the input has focus or text has been entered.
/// ///
/// Defaults to true. /// Defaults to true.
final bool hasFloatingPlaceholder; final bool hasFloatingPlaceholder;
...@@ -3110,6 +3316,28 @@ class InputDecorationTheme extends Diagnosticable { ...@@ -3110,6 +3316,28 @@ class InputDecorationTheme extends Diagnosticable {
/// true and bordered per the [border]. /// true and bordered per the [border].
final Color fillColor; final Color fillColor;
/// The color to blend with the decoration's [fillColor] with, if [filled] is
/// true and the container has the input focus.
///
/// By default the [focusColor] is based on the current [Theme].
///
/// The decoration's container is the area, defined by the border's
/// [InputBorder.getOuterPath], which is filled if [filled] is
/// true and bordered per the [border].
final Color focusColor;
/// The color to blend with the decoration's [fillColor] with, if the
/// decoration is being hovered over by a mouse pointer.
///
/// By default the [hoverColor] is based on the current [Theme].
///
/// The decoration's container is the area, defined by the border's
/// [InputBorder.getOuterPath], which is filled if [filled] is
/// true and bordered per the [border].
///
/// The container will be filled when hovered over even if [filled] is false.
final Color hoverColor;
/// The border to display when the [InputDecorator] does not have the focus and /// The border to display when the [InputDecorator] does not have the focus and
/// is showing an error. /// is showing an error.
/// ///
...@@ -3281,6 +3509,8 @@ class InputDecorationTheme extends Diagnosticable { ...@@ -3281,6 +3509,8 @@ class InputDecorationTheme extends Diagnosticable {
properties.add(DiagnosticsProperty<TextStyle>('counterStyle', counterStyle, defaultValue: defaultTheme.counterStyle)); properties.add(DiagnosticsProperty<TextStyle>('counterStyle', counterStyle, defaultValue: defaultTheme.counterStyle));
properties.add(DiagnosticsProperty<bool>('filled', filled, defaultValue: defaultTheme.filled)); properties.add(DiagnosticsProperty<bool>('filled', filled, defaultValue: defaultTheme.filled));
properties.add(DiagnosticsProperty<Color>('fillColor', fillColor, defaultValue: defaultTheme.fillColor)); properties.add(DiagnosticsProperty<Color>('fillColor', fillColor, defaultValue: defaultTheme.fillColor));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: defaultTheme.focusColor));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: defaultTheme.hoverColor));
properties.add(DiagnosticsProperty<InputBorder>('errorBorder', errorBorder, defaultValue: defaultTheme.errorBorder)); properties.add(DiagnosticsProperty<InputBorder>('errorBorder', errorBorder, defaultValue: defaultTheme.errorBorder));
properties.add(DiagnosticsProperty<InputBorder>('focusedBorder', focusedBorder, defaultValue: defaultTheme.focusedErrorBorder)); properties.add(DiagnosticsProperty<InputBorder>('focusedBorder', focusedBorder, defaultValue: defaultTheme.focusedErrorBorder));
properties.add(DiagnosticsProperty<InputBorder>('focusedErrorBorder', focusedErrorBorder, defaultValue: defaultTheme.focusedErrorBorder)); properties.add(DiagnosticsProperty<InputBorder>('focusedErrorBorder', focusedErrorBorder, defaultValue: defaultTheme.focusedErrorBorder));
......
...@@ -509,6 +509,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -509,6 +509,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
FocusNode _focusNode; FocusNode _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
bool _isHovering = false;
bool get needsCounter => widget.maxLength != null bool get needsCounter => widget.maxLength != null
&& widget.decoration != null && widget.decoration != null
&& widget.decoration.counterText == null; && widget.decoration.counterText == null;
...@@ -848,6 +850,17 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -848,6 +850,17 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
super.deactivate(); super.deactivate();
} }
void _handlePointerEnter(PointerEnterEvent event) => _handleHover(true);
void _handlePointerExit(PointerExitEvent event) => _handleHover(false);
void _handleHover(bool hovering) {
if (hovering != _isHovering) {
setState(() {
return _isHovering = hovering;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
...@@ -956,6 +969,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -956,6 +969,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
decoration: _getEffectiveDecoration(), decoration: _getEffectiveDecoration(),
baseStyle: widget.style, baseStyle: widget.style,
textAlign: widget.textAlign, textAlign: widget.textAlign,
isHovering: _isHovering,
isFocused: focusNode.hasFocus, isFocused: focusNode.hasFocus,
isEmpty: controller.value.text.isEmpty, isEmpty: controller.value.text.isEmpty,
expands: widget.expands, expands: widget.expands,
...@@ -972,6 +986,9 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -972,6 +986,9 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
_effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length); _effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
_requestKeyboard(); _requestKeyboard();
}, },
child: Listener(
onPointerEnter: _handlePointerEnter,
onPointerExit: _handlePointerExit,
child: IgnorePointer( child: IgnorePointer(
ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true), ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
child: TextSelectionGestureDetector( child: TextSelectionGestureDetector(
...@@ -989,6 +1006,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -989,6 +1006,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
child: child, child: child,
), ),
), ),
),
); );
} }
} }
...@@ -237,8 +237,8 @@ class ThemeData extends Diagnosticable { ...@@ -237,8 +237,8 @@ class ThemeData extends Diagnosticable {
// Used as the default color (fill color) for RaisedButtons. Computing the // Used as the default color (fill color) for RaisedButtons. Computing the
// default for ButtonThemeData for the sake of backwards compatibility. // default for ButtonThemeData for the sake of backwards compatibility.
buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300]; buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300];
focusColor ??= buttonColor; focusColor ??= isDark ? Colors.white.withOpacity(0.12) : Colors.black.withOpacity(0.12);
hoverColor ??= buttonColor; hoverColor ??= isDark ? Colors.white.withOpacity(0.04) : Colors.black.withOpacity(0.04);
buttonTheme ??= ButtonThemeData( buttonTheme ??= ButtonThemeData(
colorScheme: colorScheme, colorScheme: colorScheme,
buttonColor: buttonColor, buttonColor: buttonColor,
......
...@@ -17,6 +17,7 @@ Widget buildInputDecorator({ ...@@ -17,6 +17,7 @@ Widget buildInputDecorator({
TextDirection textDirection = TextDirection.ltr, TextDirection textDirection = TextDirection.ltr,
bool isEmpty = false, bool isEmpty = false,
bool isFocused = false, bool isFocused = false,
bool isHovering = false,
TextStyle baseStyle, TextStyle baseStyle,
Widget child = const Text( Widget child = const Text(
'text', 'text',
...@@ -39,6 +40,7 @@ Widget buildInputDecorator({ ...@@ -39,6 +40,7 @@ Widget buildInputDecorator({
decoration: decoration, decoration: decoration,
isEmpty: isEmpty, isEmpty: isEmpty,
isFocused: isFocused, isFocused: isFocused,
isHovering: isHovering,
baseStyle: baseStyle, baseStyle: baseStyle,
child: child, child: child,
), ),
...@@ -90,6 +92,12 @@ double getBorderWeight(WidgetTester tester) => getBorderSide(tester)?.width; ...@@ -90,6 +92,12 @@ double getBorderWeight(WidgetTester tester) => getBorderSide(tester)?.width;
Color getBorderColor(WidgetTester tester) => getBorderSide(tester)?.color; Color getBorderColor(WidgetTester tester) => getBorderSide(tester)?.color;
Color getContainerColor(WidgetTester tester) {
final CustomPaint customPaint = tester.widget(findBorderPainter());
final dynamic/*_InputBorderPainter*/ inputBorderPainter = customPaint.foregroundPainter;
return inputBorderPainter.blendedColor;
}
double getOpacity(WidgetTester tester, String textValue) { double getOpacity(WidgetTester tester, String textValue) {
final FadeTransition opacityWidget = tester.widget<FadeTransition>( final FadeTransition opacityWidget = tester.widget<FadeTransition>(
find.ancestor( find.ancestor(
...@@ -1825,6 +1833,7 @@ void main() { ...@@ -1825,6 +1833,7 @@ void main() {
counterStyle: themeStyle, counterStyle: themeStyle,
filled: true, filled: true,
fillColor: Colors.red, fillColor: Colors.red,
focusColor: Colors.blue,
border: InputBorder.none, border: InputBorder.none,
alignLabelWithHint: true, alignLabelWithHint: true,
) )
...@@ -1873,6 +1882,7 @@ void main() { ...@@ -1873,6 +1882,7 @@ void main() {
counterStyle: themeStyle, counterStyle: themeStyle,
filled: true, filled: true,
fillColor: Colors.red, fillColor: Colors.red,
focusColor: Colors.blue,
border: InputBorder.none, border: InputBorder.none,
alignLabelWithHint: true, alignLabelWithHint: true,
), ),
...@@ -2034,6 +2044,107 @@ void main() { ...@@ -2034,6 +2044,107 @@ void main() {
skip: !Platform.isLinux, skip: !Platform.isLinux,
); );
testWidgets('InputDecorator draws and animates hoverColor', (WidgetTester tester) async {
const Color fillColor = Color(0xFF00FF00);
const Color hoverColor = Color(0xFF0000FF);
await tester.pumpWidget(
buildInputDecorator(
isHovering: false,
decoration: const InputDecoration(
filled: true,
fillColor: fillColor,
hoverColor: hoverColor,
),
),
);
expect(getContainerColor(tester), equals(fillColor));
await tester.pump(const Duration(seconds: 10));
expect(getContainerColor(tester), equals(fillColor));
await tester.pumpWidget(
buildInputDecorator(
isHovering: true,
decoration: const InputDecoration(
filled: true,
fillColor: fillColor,
hoverColor: hoverColor,
),
),
);
expect(getContainerColor(tester), equals(fillColor));
await tester.pump(const Duration(milliseconds: 15));
expect(getContainerColor(tester), equals(hoverColor));
await tester.pumpWidget(
buildInputDecorator(
isHovering: false,
decoration: const InputDecoration(
filled: true,
fillColor: fillColor,
hoverColor: hoverColor,
),
),
);
expect(getContainerColor(tester), equals(hoverColor));
await tester.pump(const Duration(milliseconds: 15));
expect(getContainerColor(tester), equals(fillColor));
});
testWidgets('InputDecorator draws and animates focusColor', (WidgetTester tester) async {
const Color fillColor = Color(0xFF00FF00);
const Color focusColor = Color(0xFF0000FF);
await tester.pumpWidget(
buildInputDecorator(
isFocused: false,
decoration: const InputDecoration(
filled: true,
fillColor: fillColor,
focusColor: focusColor,
),
),
);
expect(getContainerColor(tester), equals(fillColor));
await tester.pump(const Duration(seconds: 10));
expect(getContainerColor(tester), equals(fillColor));
await tester.pumpWidget(
buildInputDecorator(
isFocused: true,
decoration: const InputDecoration(
filled: true,
fillColor: fillColor,
focusColor: focusColor,
),
),
);
expect(getContainerColor(tester), equals(fillColor));
await tester.pump(const Duration(milliseconds: 45));
expect(getContainerColor(tester), equals(focusColor));
await tester.pumpWidget(
buildInputDecorator(
isFocused: false,
decoration: const InputDecoration(
filled: true,
fillColor: fillColor,
focusColor: focusColor,
),
),
);
expect(getContainerColor(tester), equals(focusColor));
// TODO(gspencer): convert this to 15ms once reverseDuration for AnimationController lands.
await tester.pump(const Duration(milliseconds: 45));
expect(getContainerColor(tester), equals(fillColor));
});
testWidgets('InputDecorationTheme.toString()', (WidgetTester tester) async { testWidgets('InputDecorationTheme.toString()', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/19305 // Regression test for https://github.com/flutter/flutter/issues/19305
expect( expect(
...@@ -2065,7 +2176,8 @@ void main() { ...@@ -2065,7 +2176,8 @@ void main() {
suffixStyle: TextStyle(height: 8.0), suffixStyle: TextStyle(height: 8.0),
counterStyle: TextStyle(height: 9.0), counterStyle: TextStyle(height: 9.0),
filled: true, filled: true,
fillColor: Color(10), fillColor: Color(0x10),
focusColor: Color(0x20),
errorBorder: UnderlineInputBorder(), errorBorder: UnderlineInputBorder(),
focusedBorder: OutlineInputBorder(), focusedBorder: OutlineInputBorder(),
focusedErrorBorder: UnderlineInputBorder(), focusedErrorBorder: UnderlineInputBorder(),
...@@ -2077,7 +2189,8 @@ void main() { ...@@ -2077,7 +2189,8 @@ void main() {
// Spot check // Spot check
expect(debugString, contains('labelStyle: TextStyle(inherit: true, height: 1.0x)')); expect(debugString, contains('labelStyle: TextStyle(inherit: true, height: 1.0x)'));
expect(debugString, contains('isDense: true')); expect(debugString, contains('isDense: true'));
expect(debugString, contains('fillColor: Color(0x0000000a)')); expect(debugString, contains('fillColor: Color(0x00000010)'));
expect(debugString, contains('focusColor: Color(0x00000020)'));
expect(debugString, contains('errorBorder: UnderlineInputBorder()')); expect(debugString, contains('errorBorder: UnderlineInputBorder()'));
expect(debugString, contains('focusedBorder: OutlineInputBorder()')); expect(debugString, contains('focusedBorder: OutlineInputBorder()'));
}); });
...@@ -2320,8 +2433,8 @@ void main() { ...@@ -2320,8 +2433,8 @@ void main() {
gapPadding: 32.0, gapPadding: 32.0,
); );
expect(outlineInputBorder.hashCode, const OutlineInputBorder( expect(outlineInputBorder.hashCode, const OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue),
borderRadius: BorderRadius.all(Radius.circular(9.0)), borderRadius: BorderRadius.all(Radius.circular(9.0)),
borderSide: BorderSide(color: Colors.blue),
gapPadding: 32.0, gapPadding: 32.0,
).hashCode); ).hashCode);
expect(outlineInputBorder.hashCode, isNot(const OutlineInputBorder().hashCode)); expect(outlineInputBorder.hashCode, isNot(const OutlineInputBorder().hashCode));
...@@ -2346,6 +2459,7 @@ void main() { ...@@ -2346,6 +2459,7 @@ void main() {
counterStyle: TextStyle(), counterStyle: TextStyle(),
filled: true, filled: true,
fillColor: Colors.red, fillColor: Colors.red,
focusColor: Colors.blue,
errorBorder: UnderlineInputBorder(), errorBorder: UnderlineInputBorder(),
focusedBorder: UnderlineInputBorder(), focusedBorder: UnderlineInputBorder(),
focusedErrorBorder: UnderlineInputBorder(), focusedErrorBorder: UnderlineInputBorder(),
...@@ -2369,6 +2483,7 @@ void main() { ...@@ -2369,6 +2483,7 @@ void main() {
'counterStyle: TextStyle(<all styles inherited>)', 'counterStyle: TextStyle(<all styles inherited>)',
'filled: true', 'filled: true',
'fillColor: MaterialColor(primary value: Color(0xfff44336))', 'fillColor: MaterialColor(primary value: Color(0xfff44336))',
'focusColor: MaterialColor(primary value: Color(0xff2196f3))',
'errorBorder: UnderlineInputBorder()', 'errorBorder: UnderlineInputBorder()',
'focusedBorder: UnderlineInputBorder()', 'focusedBorder: UnderlineInputBorder()',
'focusedErrorBorder: UnderlineInputBorder()', 'focusedErrorBorder: UnderlineInputBorder()',
......
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