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),
......
......@@ -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