Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reland "Fix text field label animation duration and curve (#105966)" #114661

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 14 additions & 5 deletions packages/flutter/lib/src/material/input_decorator.dart
Expand Up @@ -22,7 +22,9 @@ import 'theme_data.dart';
// Examples can assume:
// late Widget _myIcon;

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

Expand Down Expand Up @@ -192,6 +194,7 @@ class _BorderContainerState extends State<_BorderContainer> with TickerProviderS
_borderAnimation = CurvedAnimation(
parent: _controller,
curve: _kTransitionCurve,
reverseCurve: _kTransitionCurve.flipped,
);
_border = _InputBorderTween(
begin: widget.border,
Expand Down Expand Up @@ -1896,8 +1899,9 @@ class InputDecorator extends StatefulWidget {
}

class _InputDecoratorState extends State<InputDecorator> with TickerProviderStateMixin {
late AnimationController _floatingLabelController;
late AnimationController _shakingLabelController;
late final AnimationController _floatingLabelController;
late final Animation<double> _floatingLabelAnimation;
late final AnimationController _shakingLabelController;
final _InputBorderGap _borderGap = _InputBorderGap();

@override
Expand All @@ -1914,6 +1918,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
value: labelIsInitiallyFloating ? 1.0 : 0.0,
);
_floatingLabelController.addListener(_handleChange);
_floatingLabelAnimation = CurvedAnimation(
parent: _floatingLabelController,
curve: _kTransitionCurve,
reverseCurve: _kTransitionCurve.flipped,
);

_shakingLabelController = AnimationController(
duration: _kTransitionDuration,
Expand Down Expand Up @@ -2191,7 +2200,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
final Widget container = _BorderContainer(
border: border,
gap: _borderGap,
gapAnimation: _floatingLabelController.view,
gapAnimation: _floatingLabelAnimation,
fillColor: _getFillColor(themeData, defaults),
hoverColor: _getHoverColor(themeData),
isHovering: isHovering,
Expand Down Expand Up @@ -2367,7 +2376,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
isCollapsed: decoration.isCollapsed,
floatingLabelHeight: floatingLabelHeight,
floatingLabelAlignment: decoration.floatingLabelAlignment!,
floatingLabelProgress: _floatingLabelController.value,
floatingLabelProgress: _floatingLabelAnimation.value,
border: border,
borderGap: _borderGap,
alignLabelWithHint: decoration.alignLabelWithHint ?? false,
Expand Down
85 changes: 67 additions & 18 deletions packages/flutter/test/material/input_decorator_test.dart
Expand Up @@ -266,7 +266,7 @@ void main() {
);

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

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

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

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

});

testWidgets('InputDecorator floating label animation duration and curve', (WidgetTester tester) async {
Future<void> pumpInputDecorator({
required bool isFocused,
}) async {
return tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: isFocused,
decoration: const InputDecoration(
labelText: 'label',
floatingLabelBehavior: FloatingLabelBehavior.auto,
),
),
);
}
await pumpInputDecorator(isFocused: false);
expect(tester.getTopLeft(find.text('label')).dy, 20.0);

// The label animates upwards and scales down.
// The animation duration is 167ms and the curve is fastOutSlowIn.
await pumpInputDecorator(isFocused: true);
await tester.pump(const Duration(milliseconds: 42));
expect(tester.getTopLeft(find.text('label')).dy, closeTo(18.06, 0.5));
await tester.pump(const Duration(milliseconds: 42));
expect(tester.getTopLeft(find.text('label')).dy, closeTo(13.78, 0.5));
await tester.pump(const Duration(milliseconds: 42));
expect(tester.getTopLeft(find.text('label')).dy, closeTo(12.31, 0.5));
await tester.pump(const Duration(milliseconds: 41));
expect(tester.getTopLeft(find.text('label')).dy, 12.0);

// If the animation changes direction without first reaching the
// AnimationStatus.completed or AnimationStatus.dismissed status,
// the CurvedAnimation stays on the same curve in the opposite direction.
// The pumpAndSettle is used to prevent this behavior.
await tester.pumpAndSettle();

// The label animates downwards and scales up.
// The animation duration is 167ms and the curve is fastOutSlowIn.
await pumpInputDecorator(isFocused: false);
await tester.pump(const Duration(milliseconds: 42));
expect(tester.getTopLeft(find.text('label')).dy, closeTo(13.94, 0.5));
await tester.pump(const Duration(milliseconds: 42));
expect(tester.getTopLeft(find.text('label')).dy, closeTo(18.22, 0.5));
await tester.pump(const Duration(milliseconds: 42));
expect(tester.getTopLeft(find.text('label')).dy, closeTo(19.69, 0.5));
await tester.pump(const Duration(milliseconds: 41));
expect(tester.getTopLeft(find.text('label')).dy, 20.0);
});

group('alignLabelWithHint', () {
group('expands false', () {
testWidgets('multiline TextField no-strut', (WidgetTester tester) async {
Expand Down Expand Up @@ -1014,7 +1063,7 @@ void main() {
);

// The hint's opacity animates from 0.0 to 1.0.
// The animation's duration is 200ms.
// The animation's duration is 167ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
Expand Down Expand Up @@ -1048,7 +1097,7 @@ void main() {
);

// The hint's opacity animates from 1.0 to 0.0.
// The animation's duration is 200ms.
// The animation's duration is 167ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
Expand Down Expand Up @@ -1969,7 +2018,7 @@ void main() {
);

// The hint's opacity animates from 0.0 to 1.0.
// The animation's duration is 200ms.
// The animation's duration is 167ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
Expand Down Expand Up @@ -2004,7 +2053,7 @@ void main() {
);

// The hint's opacity animates from 1.0 to 0.0.
// The animation's duration is 200ms.
// The animation's duration is 167ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
Expand Down Expand Up @@ -2066,7 +2115,7 @@ void main() {
);

// The hint's opacity animates from 0.0 to 1.0.
// The animation's duration is 200ms.
// The animation's duration is 167ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
Expand Down Expand Up @@ -2101,7 +2150,7 @@ void main() {
);

// The hint's opacity animates from 1.0 to 0.0.
// The animation's duration is 200ms.
// The animation's duration is 167ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
Expand Down Expand Up @@ -4512,17 +4561,17 @@ void main() {

await pumpDecorator(hovering: true, filled: false);
expect(getBorderColor(tester), equals(enabledBorderColor));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 167));
expect(getBorderColor(tester), equals(blendedHoverColor));

await pumpDecorator(hovering: false, filled: false);
expect(getBorderColor(tester), equals(blendedHoverColor));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 167));
expect(getBorderColor(tester), equals(enabledBorderColor));

await pumpDecorator(hovering: false, filled: false, enabled: false);
expect(getBorderColor(tester), equals(enabledBorderColor));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 167));
expect(getBorderColor(tester), equals(disabledColor));

await pumpDecorator(hovering: true, filled: false, enabled: false);
Expand Down Expand Up @@ -4566,17 +4615,17 @@ void main() {

await pumpDecorator(focused: true, filled: false);
expect(getBorderColor(tester), equals(enabledBorderColor));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 167));
expect(getBorderColor(tester), equals(focusColor));

await pumpDecorator(focused: false, filled: false);
expect(getBorderColor(tester), equals(focusColor));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 167));
expect(getBorderColor(tester), equals(enabledBorderColor));

await pumpDecorator(focused: false, filled: false, enabled: false);
expect(getBorderColor(tester), equals(enabledBorderColor));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 167));
expect(getBorderColor(tester), equals(disabledColor));

await pumpDecorator(focused: true, filled: false, enabled: false);
Expand Down Expand Up @@ -5661,8 +5710,8 @@ void main() {

// Click for Focus.
await tester.tap(find.byType(TextField));
// Default animation duration is 200 millisecond.
await tester.pumpFrames(target, const Duration(milliseconds: 100));
// Default animation duration is 167ms.
await tester.pumpFrames(target, const Duration(milliseconds: 80));

expect(getLabelRect(tester).width, greaterThan(labelWidth));
expect(getLabelRect(tester).width, lessThanOrEqualTo(floatedLabelWidth));
Expand Down