Skip to content

Commit d5c53b8

Browse files
authored
Fix text field label animation duration and curve (#105966)
1 parent e39fa7a commit d5c53b8

File tree

3 files changed

+82
-23
lines changed

3 files changed

+82
-23
lines changed

packages/flutter/lib/src/material/input_decorator.dart

+14-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import 'theme_data.dart';
2222
// Examples can assume:
2323
// late Widget _myIcon;
2424

25-
const Duration _kTransitionDuration = Duration(milliseconds: 200);
25+
// The duration value extracted from:
26+
// https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/textfield/TextInputLayout.java
27+
const Duration _kTransitionDuration = Duration(milliseconds: 167);
2628
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
2729
const double _kFinalLabelScale = 0.75;
2830

@@ -192,6 +194,7 @@ class _BorderContainerState extends State<_BorderContainer> with TickerProviderS
192194
_borderAnimation = CurvedAnimation(
193195
parent: _controller,
194196
curve: _kTransitionCurve,
197+
reverseCurve: _kTransitionCurve.flipped,
195198
);
196199
_border = _InputBorderTween(
197200
begin: widget.border,
@@ -1866,8 +1869,9 @@ class InputDecorator extends StatefulWidget {
18661869
}
18671870

18681871
class _InputDecoratorState extends State<InputDecorator> with TickerProviderStateMixin {
1869-
late AnimationController _floatingLabelController;
1870-
late AnimationController _shakingLabelController;
1872+
late final AnimationController _floatingLabelController;
1873+
late final Animation<double> _floatingLabelAnimation;
1874+
late final AnimationController _shakingLabelController;
18711875
final _InputBorderGap _borderGap = _InputBorderGap();
18721876

18731877
@override
@@ -1884,6 +1888,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
18841888
value: labelIsInitiallyFloating ? 1.0 : 0.0,
18851889
);
18861890
_floatingLabelController.addListener(_handleChange);
1891+
_floatingLabelAnimation = CurvedAnimation(
1892+
parent: _floatingLabelController,
1893+
curve: _kTransitionCurve,
1894+
reverseCurve: _kTransitionCurve.flipped,
1895+
);
18871896

18881897
_shakingLabelController = AnimationController(
18891898
duration: _kTransitionDuration,
@@ -2161,7 +2170,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
21612170
final Widget container = _BorderContainer(
21622171
border: border,
21632172
gap: _borderGap,
2164-
gapAnimation: _floatingLabelController.view,
2173+
gapAnimation: _floatingLabelAnimation,
21652174
fillColor: _getFillColor(themeData, defaults),
21662175
hoverColor: _getHoverColor(themeData),
21672176
isHovering: isHovering,
@@ -2341,7 +2350,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
23412350
isCollapsed: decoration.isCollapsed,
23422351
floatingLabelHeight: floatingLabelHeight,
23432352
floatingLabelAlignment: decoration.floatingLabelAlignment!,
2344-
floatingLabelProgress: _floatingLabelController.value,
2353+
floatingLabelProgress: _floatingLabelAnimation.value,
23452354
border: border,
23462355
borderGap: _borderGap,
23472356
alignLabelWithHint: decoration.alignLabelWithHint ?? false,

packages/flutter/lib/src/widgets/text_selection.dart

+1
Original file line numberDiff line numberDiff line change
@@ -2510,6 +2510,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
25102510
if (!_isDoubleTap) {
25112511
widget.onSingleTapUp?.call(details);
25122512
_lastTapOffset = details.globalPosition;
2513+
_doubleTapTimer?.cancel();
25132514
_doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout);
25142515
}
25152516
_isDoubleTap = false;

packages/flutter/test/material/input_decorator_test.dart

+67-18
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ void main() {
265265
);
266266

267267
// The label animates downwards from it's initial position
268-
// above the input text. The animation's duration is 200ms.
268+
// above the input text. The animation's duration is 167ms.
269269
{
270270
await tester.pump(const Duration(milliseconds: 50));
271271
final double labelY50ms = tester.getTopLeft(find.text('label')).dy;
@@ -296,7 +296,7 @@ void main() {
296296
);
297297

298298
// The label animates upwards from it's initial position
299-
// above the input text. The animation's duration is 200ms.
299+
// above the input text. The animation's duration is 167ms.
300300
await tester.pump(const Duration(milliseconds: 50));
301301
final double labelY50ms = tester.getTopLeft(find.text('label')).dy;
302302
expect(labelY50ms, inExclusiveRange(12.0, 28.0));
@@ -563,7 +563,7 @@ void main() {
563563
);
564564

565565
// The label animates downwards from it's initial position
566-
// above the input text. The animation's duration is 200ms.
566+
// above the input text. The animation's duration is 167ms.
567567
await tester.pump(const Duration(milliseconds: 50));
568568
final double labelY50ms = tester.getTopLeft(find.byKey(key)).dy;
569569
expect(labelY50ms, inExclusiveRange(12.0, 20.0));
@@ -604,7 +604,7 @@ void main() {
604604
);
605605

606606
// The label animates upwards from it's initial position
607-
// above the input text. The animation's duration is 200ms.
607+
// above the input text. The animation's duration is 167ms.
608608
{
609609
await tester.pump(const Duration(milliseconds: 50));
610610
final double labelY50ms = tester.getTopLeft(find.byKey(key)).dy;
@@ -720,6 +720,55 @@ void main() {
720720

721721
});
722722

723+
testWidgets('InputDecorator floating label animation duration and curve', (WidgetTester tester) async {
724+
Future<void> pumpInputDecorator({
725+
required bool isFocused,
726+
}) async {
727+
return tester.pumpWidget(
728+
buildInputDecorator(
729+
isEmpty: true,
730+
isFocused: isFocused,
731+
decoration: const InputDecoration(
732+
labelText: 'label',
733+
floatingLabelBehavior: FloatingLabelBehavior.auto,
734+
),
735+
),
736+
);
737+
}
738+
await pumpInputDecorator(isFocused: false);
739+
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
740+
741+
// The label animates upwards and scales down.
742+
// The animation duration is 167ms and the curve is fastOutSlowIn.
743+
await pumpInputDecorator(isFocused: true);
744+
await tester.pump(const Duration(milliseconds: 42));
745+
expect(tester.getTopLeft(find.text('label')).dy, closeTo(18.06, 0.5));
746+
await tester.pump(const Duration(milliseconds: 42));
747+
expect(tester.getTopLeft(find.text('label')).dy, closeTo(13.78, 0.5));
748+
await tester.pump(const Duration(milliseconds: 42));
749+
expect(tester.getTopLeft(find.text('label')).dy, closeTo(12.31, 0.5));
750+
await tester.pump(const Duration(milliseconds: 41));
751+
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
752+
753+
// If the animation changes direction without first reaching the
754+
// AnimationStatus.completed or AnimationStatus.dismissed status,
755+
// the CurvedAnimation stays on the same curve in the opposite direction.
756+
// The pumpAndSettle is used to prevent this behavior.
757+
await tester.pumpAndSettle();
758+
759+
// The label animates downwards and scales up.
760+
// The animation duration is 167ms and the curve is fastOutSlowIn.
761+
await pumpInputDecorator(isFocused: false);
762+
await tester.pump(const Duration(milliseconds: 42));
763+
expect(tester.getTopLeft(find.text('label')).dy, closeTo(13.94, 0.5));
764+
await tester.pump(const Duration(milliseconds: 42));
765+
expect(tester.getTopLeft(find.text('label')).dy, closeTo(18.22, 0.5));
766+
await tester.pump(const Duration(milliseconds: 42));
767+
expect(tester.getTopLeft(find.text('label')).dy, closeTo(19.69, 0.5));
768+
await tester.pump(const Duration(milliseconds: 41));
769+
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
770+
});
771+
723772
group('alignLabelWithHint', () {
724773
group('expands false', () {
725774
testWidgets('multiline TextField no-strut', (WidgetTester tester) async {
@@ -1013,7 +1062,7 @@ void main() {
10131062
);
10141063

10151064
// The hint's opacity animates from 0.0 to 1.0.
1016-
// The animation's duration is 200ms.
1065+
// The animation's duration is 167ms.
10171066
{
10181067
await tester.pump(const Duration(milliseconds: 50));
10191068
final double hintOpacity50ms = getOpacity(tester, 'hint');
@@ -1047,7 +1096,7 @@ void main() {
10471096
);
10481097

10491098
// The hint's opacity animates from 1.0 to 0.0.
1050-
// The animation's duration is 200ms.
1099+
// The animation's duration is 167ms.
10511100
{
10521101
await tester.pump(const Duration(milliseconds: 50));
10531102
final double hintOpacity50ms = getOpacity(tester, 'hint');
@@ -1968,7 +2017,7 @@ void main() {
19682017
);
19692018

19702019
// The hint's opacity animates from 0.0 to 1.0.
1971-
// The animation's duration is 200ms.
2020+
// The animation's duration is 167ms.
19722021
{
19732022
await tester.pump(const Duration(milliseconds: 50));
19742023
final double hintOpacity50ms = getOpacity(tester, 'hint');
@@ -2003,7 +2052,7 @@ void main() {
20032052
);
20042053

20052054
// The hint's opacity animates from 1.0 to 0.0.
2006-
// The animation's duration is 200ms.
2055+
// The animation's duration is 167ms.
20072056
{
20082057
await tester.pump(const Duration(milliseconds: 50));
20092058
final double hintOpacity50ms = getOpacity(tester, 'hint');
@@ -2065,7 +2114,7 @@ void main() {
20652114
);
20662115

20672116
// The hint's opacity animates from 0.0 to 1.0.
2068-
// The animation's duration is 200ms.
2117+
// The animation's duration is 167ms.
20692118
{
20702119
await tester.pump(const Duration(milliseconds: 50));
20712120
final double hintOpacity50ms = getOpacity(tester, 'hint');
@@ -2100,7 +2149,7 @@ void main() {
21002149
);
21012150

21022151
// The hint's opacity animates from 1.0 to 0.0.
2103-
// The animation's duration is 200ms.
2152+
// The animation's duration is 167ms.
21042153
{
21052154
await tester.pump(const Duration(milliseconds: 50));
21062155
final double hintOpacity50ms = getOpacity(tester, 'hint');
@@ -4414,17 +4463,17 @@ void main() {
44144463

44154464
await pumpDecorator(hovering: true, filled: false);
44164465
expect(getBorderColor(tester), equals(enabledBorderColor));
4417-
await tester.pump(const Duration(milliseconds: 200));
4466+
await tester.pump(const Duration(milliseconds: 167));
44184467
expect(getBorderColor(tester), equals(blendedHoverColor));
44194468

44204469
await pumpDecorator(hovering: false, filled: false);
44214470
expect(getBorderColor(tester), equals(blendedHoverColor));
4422-
await tester.pump(const Duration(milliseconds: 200));
4471+
await tester.pump(const Duration(milliseconds: 167));
44234472
expect(getBorderColor(tester), equals(enabledBorderColor));
44244473

44254474
await pumpDecorator(hovering: false, filled: false, enabled: false);
44264475
expect(getBorderColor(tester), equals(enabledBorderColor));
4427-
await tester.pump(const Duration(milliseconds: 200));
4476+
await tester.pump(const Duration(milliseconds: 167));
44284477
expect(getBorderColor(tester), equals(disabledColor));
44294478

44304479
await pumpDecorator(hovering: true, filled: false, enabled: false);
@@ -4468,17 +4517,17 @@ void main() {
44684517

44694518
await pumpDecorator(focused: true, filled: false);
44704519
expect(getBorderColor(tester), equals(enabledBorderColor));
4471-
await tester.pump(const Duration(milliseconds: 200));
4520+
await tester.pump(const Duration(milliseconds: 167));
44724521
expect(getBorderColor(tester), equals(focusColor));
44734522

44744523
await pumpDecorator(focused: false, filled: false);
44754524
expect(getBorderColor(tester), equals(focusColor));
4476-
await tester.pump(const Duration(milliseconds: 200));
4525+
await tester.pump(const Duration(milliseconds: 167));
44774526
expect(getBorderColor(tester), equals(enabledBorderColor));
44784527

44794528
await pumpDecorator(focused: false, filled: false, enabled: false);
44804529
expect(getBorderColor(tester), equals(enabledBorderColor));
4481-
await tester.pump(const Duration(milliseconds: 200));
4530+
await tester.pump(const Duration(milliseconds: 167));
44824531
expect(getBorderColor(tester), equals(disabledColor));
44834532

44844533
await pumpDecorator(focused: true, filled: false, enabled: false);
@@ -5562,8 +5611,8 @@ void main() {
55625611

55635612
// Click for Focus.
55645613
await tester.tap(find.byType(TextField));
5565-
// Default animation duration is 200 millisecond.
5566-
await tester.pumpFrames(target, const Duration(milliseconds: 100));
5614+
// Default animation duration is 167ms.
5615+
await tester.pumpFrames(target, const Duration(milliseconds: 80));
55675616

55685617
expect(getLabelRect(tester).width, greaterThan(labelWidth));
55695618
expect(getLabelRect(tester).width, lessThanOrEqualTo(floatedLabelWidth));

0 commit comments

Comments
 (0)