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> { ...@@ -57,20 +57,24 @@ class _HoverDemoState extends State<HoverDemo> {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
RaisedButton( Row(
onPressed: () => print('Button pressed.'), children: <Widget>[
child: const Text('Button'), RaisedButton(
focusColor: Colors.deepOrangeAccent, onPressed: () => print('Button pressed.'),
), child: const Text('Button'),
FlatButton( focusColor: Colors.deepOrangeAccent,
onPressed: () => print('Button pressed.'), ),
child: const Text('Button'), FlatButton(
focusColor: Colors.deepOrangeAccent, onPressed: () => print('Button pressed.'),
), child: const Text('Button'),
IconButton( focusColor: Colors.deepOrangeAccent,
onPressed: () => print('Button pressed'), ),
icon: const Icon(Icons.access_alarm), IconButton(
focusColor: Colors.deepOrangeAccent, onPressed: () => print('Button pressed'),
icon: const Icon(Icons.access_alarm),
focusColor: Colors.deepOrangeAccent,
),
],
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
......
...@@ -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,21 +986,25 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -972,21 +986,25 @@ 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: IgnorePointer( child: Listener(
ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true), onPointerEnter: _handlePointerEnter,
child: TextSelectionGestureDetector( onPointerExit: _handlePointerExit,
onTapDown: _handleTapDown, child: IgnorePointer(
onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null, ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
onSingleTapUp: _handleSingleTapUp, child: TextSelectionGestureDetector(
onSingleTapCancel: _handleSingleTapCancel, onTapDown: _handleTapDown,
onSingleLongTapStart: _handleSingleLongTapStart, onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null,
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate, onSingleTapUp: _handleSingleTapUp,
onSingleLongTapEnd: _handleSingleLongTapEnd, onSingleTapCancel: _handleSingleTapCancel,
onDoubleTapDown: _handleDoubleTapDown, onSingleLongTapStart: _handleSingleLongTapStart,
onDragSelectionStart: _handleMouseDragSelectionStart, onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
onDragSelectionUpdate: _handleMouseDragSelectionUpdate, onSingleLongTapEnd: _handleSingleLongTapEnd,
behavior: HitTestBehavior.translucent, onDoubleTapDown: _handleDoubleTapDown,
child: child, onDragSelectionStart: _handleMouseDragSelectionStart,
onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
behavior: HitTestBehavior.translucent,
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