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
......@@ -57,20 +57,24 @@ class _HoverDemoState extends State<HoverDemo> {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => print('Button pressed.'),
child: const Text('Button'),
focusColor: Colors.deepOrangeAccent,
),
FlatButton(
onPressed: () => print('Button pressed.'),
child: const Text('Button'),
focusColor: Colors.deepOrangeAccent,
),
IconButton(
onPressed: () => print('Button pressed'),
icon: const Icon(Icons.access_alarm),
focusColor: Colors.deepOrangeAccent,
Row(
children: <Widget>[
RaisedButton(
onPressed: () => print('Button pressed.'),
child: const Text('Button'),
focusColor: Colors.deepOrangeAccent,
),
FlatButton(
onPressed: () => print('Button pressed.'),
child: const Text('Button'),
focusColor: Colors.deepOrangeAccent,
),
IconButton(
onPressed: () => print('Button pressed'),
icon: const Icon(Icons.access_alarm),
focusColor: Colors.deepOrangeAccent,
),
],
),
Padding(
padding: const EdgeInsets.all(8.0),
......
......@@ -62,13 +62,17 @@ class _InputBorderTween extends Tween<InputBorder> {
// 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,
this.fillColor,
@required Listenable repaint,
@required this.borderAnimation,
@required this.border,
@required this.gapAnimation,
@required this.gap,
@required this.textDirection,
@required this.fillColor,
@required this.focusAnimation,
@required this.focusColorTween,
@required this.hoverAnimation,
@required this.hoverColorTween,
}) : super(repaint: repaint);
final Animation<double> borderAnimation;
......@@ -77,17 +81,28 @@ class _InputBorderPainter extends CustomPainter {
final _InputBorderGap gap;
final TextDirection textDirection;
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
void paint(Canvas canvas, Size size) {
final InputBorder borderValue = border.evaluate(borderAnimation);
final Rect canvasRect = Offset.zero & size;
if (fillColor.alpha > 0) {
final Color blendedFillColor = blendedColor;
if (blendedFillColor.alpha > 0) {
canvas.drawPath(
borderValue.getOuterPath(canvasRect, textDirection: textDirection),
Paint()
..color = fillColor
..color = blendedFillColor
..style = PaintingStyle.fill,
);
}
......@@ -105,6 +120,8 @@ class _InputBorderPainter extends CustomPainter {
@override
bool shouldRepaint(_InputBorderPainter oldPainter) {
return borderAnimation != oldPainter.borderAnimation
|| focusAnimation != oldPainter.focusAnimation
|| hoverAnimation != oldPainter.hoverAnimation
|| gapAnimation != oldPainter.gapAnimation
|| border != oldPainter.border
|| gap != oldPainter.gap
......@@ -123,6 +140,10 @@ class _BorderContainer extends StatefulWidget {
@required this.gap,
@required this.gapAnimation,
@required this.fillColor,
@required this.focusColor,
@required this.isFocused,
@required this.hoverColor,
@required this.isHovering,
this.child,
}) : assert(border != null),
assert(gap != null),
......@@ -133,20 +154,44 @@ class _BorderContainer extends StatefulWidget {
final _InputBorderGap gap;
final Animation<double> gapAnimation;
final Color fillColor;
final Color focusColor;
final bool isFocused;
final Color hoverColor;
final bool isHovering;
final Widget child;
@override
_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 _focusColorController;
AnimationController _hoverColorController;
Animation<double> _borderAnimation;
_InputBorderTween _border;
Animation<double> _focusAnimation;
ColorTween _focusColorTween;
Animation<double> _hoverAnimation;
ColorTween _hoverColorTween;
@override
void 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(
duration: _kTransitionDuration,
vsync: this,
......@@ -159,11 +204,23 @@ class _BorderContainerState extends State<_BorderContainer> with SingleTickerPro
begin: 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
void dispose() {
_controller.dispose();
_focusColorController.dispose();
_hoverColorController.dispose();
super.dispose();
}
......@@ -179,19 +236,48 @@ class _BorderContainerState extends State<_BorderContainer> with SingleTickerPro
..value = 0.0
..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
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: _InputBorderPainter(
repaint: Listenable.merge(<Listenable>[_borderAnimation, widget.gap]),
repaint: Listenable.merge(<Listenable>[
_borderAnimation,
widget.gap,
_focusColorController,
_hoverColorController,
]),
borderAnimation: _borderAnimation,
border: _border,
gapAnimation: widget.gapAnimation,
gap: widget.gap,
textDirection: Directionality.of(context),
fillColor: widget.fillColor,
focusColorTween: _focusColorTween,
focusAnimation: _focusAnimation,
hoverColorTween: _hoverColorTween,
hoverAnimation: _hoverAnimation,
),
child: widget.child,
);
......@@ -1562,17 +1648,21 @@ 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.
/// The [isFocused], [isHovering], [expands], and [isEmpty] arguments must not
/// be null.
const InputDecorator({
Key key,
this.decoration,
this.baseStyle,
this.textAlign,
this.isFocused = false,
this.isHovering = false,
this.expands = false,
this.isEmpty = false,
this.child,
}) : assert(isFocused != null),
assert(isHovering != null),
assert(expands != null),
assert(isEmpty != null),
super(key: key);
......@@ -1598,12 +1688,35 @@ class InputDecorator extends StatefulWidget {
/// Whether the input field has focus.
///
/// Determines the position of the label text and the color and weight
/// of the border.
/// Determines the position of the label text and the color and weight of the
/// border, as well as the container fill color, which is a blend of
/// [InputDecoration.focusColor] with [InputDecoration.fillColor] when
/// focused, and [InputDecoration.fillColor] when not.
///
/// Defaults to false.
///
/// See also:
///
/// - [InputDecoration.hoverColor], which is also blended into the focus
/// color and fill color when the [isHovering] is true to produce the final
/// color.
final bool isFocused;
/// Whether the input field is being hovered over by a mouse pointer.
///
/// Determines the container fill color, which is a blend of
/// [InputDecoration.hoverColor] with [InputDecoration.fillColor] when
/// true, and [InputDecoration.fillColor] when not.
///
/// Defaults to false.
///
/// See also:
///
/// - [InputDecoration.focusColor], which is also blended into the hover
/// color and fill color when [isFocused] is true to produce the final
/// color.
final bool isHovering;
/// If true, the height of the input field will be as large as possible.
///
/// If wrapped in a widget that constrains its child's height, like Expanded
......@@ -1710,6 +1823,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
TextAlign get textAlign => widget.textAlign;
bool get isFocused => widget.isFocused;
bool get isHovering => widget.isHovering;
bool get isEmpty => widget.isEmpty;
@override
......@@ -1747,6 +1861,27 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
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) {
if (decoration.filled != true) // filled == null same as filled == false
return Colors.transparent;
......@@ -1769,6 +1904,18 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
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) {
if (!decoration.enabled)
return themeData.disabledColor;
......@@ -1827,7 +1974,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
Color borderColor;
if (decoration.enabled) {
borderColor = decoration.errorText == null
? _getActiveColor(themeData)
? _getBorderColor(themeData)
: themeData.errorColor;
} else {
borderColor = (decoration.filled == true && decoration.border?.isOutline != true)
......@@ -1880,6 +2027,10 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
gap: _borderGap,
gapAnimation: _floatingLabelController.view,
fillColor: _getFillColor(themeData),
focusColor: _getFocusColor(themeData),
hoverColor: _getHoverColor(themeData),
isFocused: isFocused,
isHovering: isHovering,
);
final TextStyle inlineLabelStyle = inlineStyle.merge(decoration.labelStyle);
......@@ -2114,6 +2265,8 @@ class InputDecoration {
this.counterStyle,
this.filled,
this.fillColor,
this.focusColor,
this.hoverColor,
this.errorBorder,
this.focusedBorder,
this.focusedErrorBorder,
......@@ -2139,6 +2292,8 @@ class InputDecoration {
this.hintStyle,
this.filled = false,
this.fillColor,
this.focusColor,
this.hoverColor,
this.border = InputBorder.none,
this.enabled = true,
}) : assert(enabled != null),
......@@ -2274,9 +2429,9 @@ class InputDecoration {
/// Whether the label floats on focus.
///
/// 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
/// the input has focus or inputted text.
/// the input has focus or text has been entered.
///
/// Defaults to true.
final bool hasFloatingPlaceholder;
......@@ -2476,6 +2631,10 @@ class InputDecoration {
/// If true the decoration's container is filled with [fillColor].
///
/// When [isFocused] is true, the [focusColor] is also blended into the final
/// fill color. When [isHovering] is true, the [hoverColor] is also blended
/// into the final fill color.
///
/// Typically this field set to true if [border] is an
/// [UnderlineInputBorder].
///
......@@ -2487,7 +2646,11 @@ class InputDecoration {
/// This property is false by default.
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].
///
......@@ -2495,8 +2658,42 @@ class InputDecoration {
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
///
/// This color is blended with [focusColor] if the decoration is focused.
final Color fillColor;
/// The color to blend with [fillColor] and fill the decoration's container
/// with, if [filled] is true and the container has input focus.
///
/// When [isHovering] is true, the [hoverColor] is also blended into the final
/// fill color.
///
/// By default the [focusColor] is based on the current [Theme].
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
final Color focusColor;
/// The color of the focus highlight for the decoration shown if the container
/// is being hovered over by a mouse.
///
/// If [filled] is true, the color is blended with [fillColor] and fills the
/// decoration's container. When [isFocused] is true, the [focusColor] is also
/// blended into the final fill color.
///
/// If [filled] is false, and [isFocused] is false, the color is blended over
/// the [enabledBorder]'s color.
///
/// By default the [hoverColor] is based on the current [Theme].
///
/// The decoration's container is the area which is filled if [filled] is
/// true and bordered per the [border]. It's the area adjacent to
/// [decoration.icon] and above the widgets that contain [helperText],
/// [errorText], and [counterText].
final Color hoverColor;
/// The border to display when the [InputDecorator] does not have the focus and
/// is showing an error.
///
......@@ -2703,6 +2900,8 @@ class InputDecoration {
TextStyle counterStyle,
bool filled,
Color fillColor,
Color focusColor,
Color hoverColor,
InputBorder errorBorder,
InputBorder focusedBorder,
InputBorder focusedErrorBorder,
......@@ -2741,6 +2940,8 @@ class InputDecoration {
counterStyle: counterStyle ?? this.counterStyle,
filled: filled ?? this.filled,
fillColor: fillColor ?? this.fillColor,
focusColor: focusColor ?? this.focusColor,
hoverColor: hoverColor ?? this.hoverColor,
errorBorder: errorBorder ?? this.errorBorder,
focusedBorder: focusedBorder ?? this.focusedBorder,
focusedErrorBorder: focusedErrorBorder ?? this.focusedErrorBorder,
......@@ -2773,6 +2974,8 @@ class InputDecoration {
counterStyle: counterStyle ?? theme.counterStyle,
filled: filled ?? theme.filled,
fillColor: fillColor ?? theme.fillColor,
focusColor: focusColor ?? theme.focusColor,
hoverColor: hoverColor ?? theme.hoverColor,
errorBorder: errorBorder ?? theme.errorBorder,
focusedBorder: focusedBorder ?? theme.focusedBorder,
focusedErrorBorder: focusedErrorBorder ?? theme.focusedErrorBorder,
......@@ -2818,6 +3021,8 @@ class InputDecoration {
&& typedOther.counterStyle == counterStyle
&& typedOther.filled == filled
&& typedOther.fillColor == fillColor
&& typedOther.focusColor == focusColor
&& typedOther.hoverColor == hoverColor
&& typedOther.errorBorder == errorBorder
&& typedOther.focusedBorder == focusedBorder
&& typedOther.focusedErrorBorder == focusedErrorBorder
......@@ -2831,9 +3036,7 @@ class InputDecoration {
@override
int get hashCode {
// Split into several hashValues calls because the hashValues function is
// limited to 20 parameters.
return hashValues(
final List<Object> values = <Object>[
icon,
labelText,
labelStyle,
......@@ -2851,35 +3054,32 @@ class InputDecoration {
isCollapsed,
filled,
fillColor,
focusColor,
hoverColor,
border,
enabled,
hashValues(
prefixIcon,
prefix,
prefixText,
prefixStyle,
suffixIcon,
suffix,
suffixText,
suffixStyle,
counter,
counterText,
counterStyle,
filled,
fillColor,
errorBorder,
focusedBorder,
focusedErrorBorder,
disabledBorder,
enabledBorder,
border,
hashValues(
enabled,
semanticCounterText,
alignLabelWithHint,
),
),
);
prefixIcon,
prefix,
prefixText,
prefixStyle,
suffixIcon,
suffix,
suffixText,
suffixStyle,
counter,
counterText,
counterStyle,
errorBorder,
focusedBorder,
focusedErrorBorder,
disabledBorder,
enabledBorder,
border,
enabled,
semanticCounterText,
alignLabelWithHint,
];
return hashList(values);
}
@override
......@@ -2935,6 +3135,10 @@ class InputDecoration {
description.add('filled: true');
if (fillColor != null)
description.add('fillColor: $fillColor');
if (focusColor != null)
description.add('focusColor: $focusColor');
if (hoverColor != null)
description.add('hoverColor: $hoverColor');
if (errorBorder != null)
description.add('errorBorder: $errorBorder');
if (focusedBorder != null)
......@@ -2988,6 +3192,8 @@ class InputDecorationTheme extends Diagnosticable {
this.counterStyle,
this.filled = false,
this.fillColor,
this.focusColor,
this.hoverColor,
this.errorBorder,
this.focusedBorder,
this.focusedErrorBorder,
......@@ -3041,9 +3247,9 @@ class InputDecorationTheme extends Diagnosticable {
/// Whether the placeholder text floats to become a label on focus.
///
/// 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
/// the input has focus or inputted text.
/// the input has focus or text has been entered.
///
/// Defaults to true.
final bool hasFloatingPlaceholder;
......@@ -3110,6 +3316,28 @@ class InputDecorationTheme extends Diagnosticable {
/// true and bordered per the [border].
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
/// is showing an error.
///
......@@ -3281,6 +3509,8 @@ class InputDecorationTheme extends Diagnosticable {
properties.add(DiagnosticsProperty<TextStyle>('counterStyle', counterStyle, defaultValue: defaultTheme.counterStyle));
properties.add(DiagnosticsProperty<bool>('filled', filled, defaultValue: defaultTheme.filled));
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>('focusedBorder', focusedBorder, defaultValue: defaultTheme.focusedErrorBorder));
properties.add(DiagnosticsProperty<InputBorder>('focusedErrorBorder', focusedErrorBorder, defaultValue: defaultTheme.focusedErrorBorder));
......
......@@ -509,6 +509,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
FocusNode _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
bool _isHovering = false;
bool get needsCounter => widget.maxLength != null
&& widget.decoration != null
&& widget.decoration.counterText == null;
......@@ -848,6 +850,17 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
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
Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin.
......@@ -956,6 +969,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
decoration: _getEffectiveDecoration(),
baseStyle: widget.style,
textAlign: widget.textAlign,
isHovering: _isHovering,
isFocused: focusNode.hasFocus,
isEmpty: controller.value.text.isEmpty,
expands: widget.expands,
......@@ -972,21 +986,25 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
_effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
_requestKeyboard();
},
child: IgnorePointer(
ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
child: TextSelectionGestureDetector(
onTapDown: _handleTapDown,
onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null,
onSingleTapUp: _handleSingleTapUp,
onSingleTapCancel: _handleSingleTapCancel,
onSingleLongTapStart: _handleSingleLongTapStart,
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
onSingleLongTapEnd: _handleSingleLongTapEnd,
onDoubleTapDown: _handleDoubleTapDown,
onDragSelectionStart: _handleMouseDragSelectionStart,
onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
behavior: HitTestBehavior.translucent,
child: child,
child: Listener(
onPointerEnter: _handlePointerEnter,
onPointerExit: _handlePointerExit,
child: IgnorePointer(
ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
child: TextSelectionGestureDetector(
onTapDown: _handleTapDown,
onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null,
onSingleTapUp: _handleSingleTapUp,
onSingleTapCancel: _handleSingleTapCancel,
onSingleLongTapStart: _handleSingleLongTapStart,
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
onSingleLongTapEnd: _handleSingleLongTapEnd,
onDoubleTapDown: _handleDoubleTapDown,
onDragSelectionStart: _handleMouseDragSelectionStart,
onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
behavior: HitTestBehavior.translucent,
child: child,
),
),
),
);
......
......@@ -237,8 +237,8 @@ class ThemeData extends Diagnosticable {
// Used as the default color (fill color) for RaisedButtons. Computing the
// default for ButtonThemeData for the sake of backwards compatibility.
buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300];
focusColor ??= buttonColor;
hoverColor ??= buttonColor;
focusColor ??= isDark ? Colors.white.withOpacity(0.12) : Colors.black.withOpacity(0.12);
hoverColor ??= isDark ? Colors.white.withOpacity(0.04) : Colors.black.withOpacity(0.04);
buttonTheme ??= ButtonThemeData(
colorScheme: colorScheme,
buttonColor: buttonColor,
......
......@@ -17,6 +17,7 @@ Widget buildInputDecorator({
TextDirection textDirection = TextDirection.ltr,
bool isEmpty = false,
bool isFocused = false,
bool isHovering = false,
TextStyle baseStyle,
Widget child = const Text(
'text',
......@@ -39,6 +40,7 @@ Widget buildInputDecorator({
decoration: decoration,
isEmpty: isEmpty,
isFocused: isFocused,
isHovering: isHovering,
baseStyle: baseStyle,
child: child,
),
......@@ -90,6 +92,12 @@ double getBorderWeight(WidgetTester tester) => getBorderSide(tester)?.width;
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) {
final FadeTransition opacityWidget = tester.widget<FadeTransition>(
find.ancestor(
......@@ -1825,6 +1833,7 @@ void main() {
counterStyle: themeStyle,
filled: true,
fillColor: Colors.red,
focusColor: Colors.blue,
border: InputBorder.none,
alignLabelWithHint: true,
)
......@@ -1873,6 +1882,7 @@ void main() {
counterStyle: themeStyle,
filled: true,
fillColor: Colors.red,
focusColor: Colors.blue,
border: InputBorder.none,
alignLabelWithHint: true,
),
......@@ -2034,6 +2044,107 @@ void main() {
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 {
// Regression test for https://github.com/flutter/flutter/issues/19305
expect(
......@@ -2065,7 +2176,8 @@ void main() {
suffixStyle: TextStyle(height: 8.0),
counterStyle: TextStyle(height: 9.0),
filled: true,
fillColor: Color(10),
fillColor: Color(0x10),
focusColor: Color(0x20),
errorBorder: UnderlineInputBorder(),
focusedBorder: OutlineInputBorder(),
focusedErrorBorder: UnderlineInputBorder(),
......@@ -2077,7 +2189,8 @@ void main() {
// Spot check
expect(debugString, contains('labelStyle: TextStyle(inherit: true, height: 1.0x)'));
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('focusedBorder: OutlineInputBorder()'));
});
......@@ -2320,8 +2433,8 @@ void main() {
gapPadding: 32.0,
);
expect(outlineInputBorder.hashCode, const OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue),
borderRadius: BorderRadius.all(Radius.circular(9.0)),
borderSide: BorderSide(color: Colors.blue),
gapPadding: 32.0,
).hashCode);
expect(outlineInputBorder.hashCode, isNot(const OutlineInputBorder().hashCode));
......@@ -2346,6 +2459,7 @@ void main() {
counterStyle: TextStyle(),
filled: true,
fillColor: Colors.red,
focusColor: Colors.blue,
errorBorder: UnderlineInputBorder(),
focusedBorder: UnderlineInputBorder(),
focusedErrorBorder: UnderlineInputBorder(),
......@@ -2369,6 +2483,7 @@ void main() {
'counterStyle: TextStyle(<all styles inherited>)',
'filled: true',
'fillColor: MaterialColor(primary value: Color(0xfff44336))',
'focusColor: MaterialColor(primary value: Color(0xff2196f3))',
'errorBorder: UnderlineInputBorder()',
'focusedBorder: 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