Skip to content

Commit 257df5e

Browse files
authored
Add ability to disable FloatingActionButton scale and rotation animations using FloatingActionButtonAnimator.noAnimation (flutter#146126)
fixes [[Proposal] Allow disabling the scaling animation of the FloatingActionButton](flutter#145585) ### Using default `FloatingActionButton` animations ![ScreenRecording2024-04-02at16 19 03-ezgif com-video-to-gif-converter](https://github.com/flutter/flutter/assets/48603081/627ea564-7f60-4eb4-bed9-95c053ae2f56) ### Using `FloatingActionButtonAnimator.noAnimation` ![ScreenRecording2024-04-02at16 17 20-ezgif com-video-to-gif-converter](https://github.com/flutter/flutter/assets/48603081/d0a936ea-9e16-4225-8dc4-40a11ee8a975)
1 parent 098e7e7 commit 257df5e

File tree

5 files changed

+362
-6
lines changed

5 files changed

+362
-6
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
/// Flutter code sample for [Scaffold.floatingActionButtonAnimator].
8+
9+
void main() => runApp(const ScaffoldFloatingActionButtonAnimatorApp());
10+
11+
class ScaffoldFloatingActionButtonAnimatorApp extends StatelessWidget {
12+
const ScaffoldFloatingActionButtonAnimatorApp({super.key});
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
return const MaterialApp(
17+
home: ScaffoldFloatingActionButtonAnimatorExample(),
18+
);
19+
}
20+
}
21+
22+
enum FabAnimator { defaultStyle, none }
23+
const List<(FabAnimator, String)> fabAnimatoregments = <(FabAnimator, String)>[
24+
(FabAnimator.defaultStyle, 'Default'),
25+
(FabAnimator.none, 'None'),
26+
];
27+
28+
enum FabLocation { centerFloat, endFloat, endTop }
29+
const List<(FabLocation, String)> fabLocationegments = <(FabLocation, String)>[
30+
(FabLocation.centerFloat, 'centerFloat'),
31+
(FabLocation.endFloat, 'endFloat'),
32+
(FabLocation.endTop, 'endTop'),
33+
];
34+
35+
class ScaffoldFloatingActionButtonAnimatorExample extends StatefulWidget {
36+
const ScaffoldFloatingActionButtonAnimatorExample({super.key});
37+
38+
@override
39+
State<ScaffoldFloatingActionButtonAnimatorExample> createState() => _ScaffoldFloatingActionButtonAnimatorExampleState();
40+
}
41+
42+
class _ScaffoldFloatingActionButtonAnimatorExampleState extends State<ScaffoldFloatingActionButtonAnimatorExample> {
43+
Set<FabAnimator> _selectedFabAnimator = <FabAnimator>{FabAnimator.defaultStyle};
44+
Set<FabLocation> _selectedFabLocation = <FabLocation>{FabLocation.endFloat};
45+
FloatingActionButtonAnimator? _floatingActionButtonAnimator;
46+
FloatingActionButtonLocation? _floatingActionButtonLocation;
47+
bool _showFab = false;
48+
49+
@override
50+
Widget build(BuildContext context) {
51+
return Scaffold(
52+
floatingActionButtonLocation: _floatingActionButtonLocation,
53+
floatingActionButtonAnimator: _floatingActionButtonAnimator,
54+
appBar: AppBar(title: const Text('FloatingActionButtonAnimator Sample')),
55+
body: Center(
56+
child: Column(
57+
mainAxisAlignment: MainAxisAlignment.center,
58+
children: <Widget>[
59+
SegmentedButton<FabAnimator>(
60+
selected: _selectedFabAnimator,
61+
onSelectionChanged: (Set<FabAnimator> styles) {
62+
setState(() {
63+
_floatingActionButtonAnimator = switch (styles.first) {
64+
FabAnimator.defaultStyle => null,
65+
FabAnimator.none => FloatingActionButtonAnimator.noAnimation,
66+
};
67+
_selectedFabAnimator = styles;
68+
});
69+
},
70+
segments: fabAnimatoregments
71+
.map<ButtonSegment<FabAnimator>>(((FabAnimator, String) fabAnimator) {
72+
final FabAnimator animator = fabAnimator.$1;
73+
final String label = fabAnimator.$2;
74+
return ButtonSegment<FabAnimator>(value: animator, label: Text(label));
75+
})
76+
.toList(),
77+
),
78+
const SizedBox(height: 10),
79+
SegmentedButton<FabLocation>(
80+
selected: _selectedFabLocation,
81+
onSelectionChanged: (Set<FabLocation> styles) {
82+
setState(() {
83+
_floatingActionButtonLocation = switch (styles.first) {
84+
FabLocation.centerFloat => FloatingActionButtonLocation.centerFloat,
85+
FabLocation.endFloat => FloatingActionButtonLocation.endFloat,
86+
FabLocation.endTop => FloatingActionButtonLocation.endTop,
87+
};
88+
_selectedFabLocation = styles;
89+
});
90+
},
91+
segments: fabLocationegments
92+
.map<ButtonSegment<FabLocation>>(((FabLocation, String) fabLocation) {
93+
final FabLocation location = fabLocation.$1;
94+
final String label = fabLocation.$2;
95+
return ButtonSegment<FabLocation>(value: location, label: Text(label));
96+
})
97+
.toList(),
98+
),
99+
const SizedBox(height: 10),
100+
FilledButton.icon(
101+
onPressed: () {
102+
setState(() {
103+
_showFab = !_showFab;
104+
});
105+
},
106+
icon: Icon(_showFab ? Icons.visibility_off : Icons.visibility),
107+
label: const Text('Toggle FAB'),
108+
),
109+
],
110+
),
111+
),
112+
floatingActionButton: !_showFab
113+
? null
114+
: FloatingActionButton(
115+
onPressed: () {},
116+
child: const Icon(Icons.add),
117+
),
118+
);
119+
}
120+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_api_samples/material/scaffold/scaffold.floating_action_button_animator.0.dart' as example;
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
testWidgets('FloatingActionButton animation can be customized', (WidgetTester tester) async {
11+
await tester.pumpWidget(
12+
const example.ScaffoldFloatingActionButtonAnimatorApp(),
13+
);
14+
15+
expect(find.byType(FloatingActionButton), findsNothing);
16+
17+
// Test default FloatingActionButtonAnimator.
18+
// Tap the toggle button to show the FAB.
19+
await tester.tap(find.text('Toggle FAB'));
20+
await tester.pump();
21+
await tester.pump(const Duration(milliseconds: 100)); // Advance animation by 100ms.
22+
// FAB is partially animated in.
23+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, closeTo(743.8, 0.1));
24+
25+
await tester.pump(const Duration(milliseconds: 100)); // Advance animation by 100ms.
26+
// FAB is fully animated in.
27+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
28+
29+
// Tap the toggle button to hide the FAB.
30+
await tester.tap(find.text('Toggle FAB'));
31+
await tester.pump();
32+
await tester.pump(const Duration(milliseconds: 100)); // Advance animation by 100ms.
33+
// FAB is partially animated out.
34+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, closeTo(747.1, 0.1));
35+
36+
await tester.pump(const Duration(milliseconds: 100)); // Advance animation by 100ms.
37+
// FAB is fully animated out.
38+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(756.0));
39+
40+
await tester.pump(const Duration(milliseconds: 50)); // Advance animation by 50ms.
41+
// FAB is hidden.
42+
expect(find.byType(FloatingActionButton), findsNothing);
43+
44+
// Select 'None' to disable animation.
45+
await tester.tap(find.text('None'));
46+
await tester.pump();
47+
48+
// Test no animation FloatingActionButtonAnimator.
49+
await tester.tap(find.text('Toggle FAB'));
50+
await tester.pump();
51+
// FAB is immediately shown.
52+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
53+
54+
// Tap the toggle button to hide the FAB.
55+
await tester.tap(find.text('Toggle FAB'));
56+
await tester.pump();
57+
// FAB is immediately hidden.
58+
expect(find.byType(FloatingActionButton), findsNothing);
59+
});
60+
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,18 @@ abstract class FloatingActionButtonAnimator {
937937
/// the animation from the beginning, regardless of the original state of the animation.
938938
double getAnimationRestart(double previousValue) => 0.0;
939939

940+
/// Creates an instance of [FloatingActionButtonAnimator] where the [FloatingActionButton]
941+
/// does not animate on entrance and exit when [FloatingActionButtonLocation] is shown
942+
/// or hidden and when transitioning between [FloatingActionButtonLocation]s.
943+
///
944+
/// {@tool dartpad}
945+
/// This sample showcases how to override [FloatingActionButton] entrance and exit animations
946+
/// using [FloatingActionButtonAnimator.noAnimation] in [Scaffold.floatingActionButtonAnimator].
947+
///
948+
/// ** See code in examples/api/lib/material/scaffold/scaffold.floating_action_button_animator.0.dart **
949+
/// {@end-tool}
950+
static const FloatingActionButtonAnimator noAnimation = _NoAnimationFabMotionAnimator();
951+
940952
@override
941953
String toString() => objectRuntimeType(this, 'FloatingActionButtonAnimator');
942954
}
@@ -993,6 +1005,25 @@ class _ScalingFabMotionAnimator extends FloatingActionButtonAnimator {
9931005
double getAnimationRestart(double previousValue) => math.min(1.0 - previousValue, previousValue);
9941006
}
9951007

1008+
class _NoAnimationFabMotionAnimator extends FloatingActionButtonAnimator {
1009+
const _NoAnimationFabMotionAnimator();
1010+
1011+
@override
1012+
Offset getOffset({required Offset begin, required Offset end, required double progress}) {
1013+
return end;
1014+
}
1015+
1016+
@override
1017+
Animation<double> getRotationAnimation({required Animation<double> parent}) {
1018+
return const AlwaysStoppedAnimation<double>(1.0);
1019+
}
1020+
1021+
@override
1022+
Animation<double> getScaleAnimation({required Animation<double> parent}) {
1023+
return const AlwaysStoppedAnimation<double>(1.0);
1024+
}
1025+
}
1026+
9961027
/// An animation that swaps from one animation to the next when the [parent] passes [swapThreshold].
9971028
///
9981029
/// The [value] of this animation is the value of [first] when [parent.value] < [swapThreshold]

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,13 +1436,19 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
14361436
final Animation<double> moveRotationAnimation = widget.fabMotionAnimator.getRotationAnimation(parent: widget.fabMoveAnimation);
14371437

14381438
// Aggregate the animations.
1439-
_previousScaleAnimation = AnimationMin<double>(moveScaleAnimation, _previousExitScaleAnimation!);
1440-
_currentScaleAnimation = AnimationMin<double>(moveScaleAnimation, _currentEntranceScaleAnimation!);
1441-
_extendedCurrentScaleAnimation = _currentScaleAnimation.drive(CurveTween(curve: const Interval(0.0, 0.1)));
1442-
1443-
_previousRotationAnimation = TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation);
1444-
_currentRotationAnimation = TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation);
1439+
if (widget.fabMotionAnimator == FloatingActionButtonAnimator.noAnimation) {
1440+
_previousScaleAnimation = moveScaleAnimation;
1441+
_currentScaleAnimation = moveScaleAnimation;
1442+
_previousRotationAnimation = TrainHoppingAnimation(moveRotationAnimation, null);
1443+
_currentRotationAnimation = TrainHoppingAnimation(moveRotationAnimation, null);
1444+
} else {
1445+
_previousScaleAnimation = AnimationMin<double>(moveScaleAnimation, _previousExitScaleAnimation!);
1446+
_currentScaleAnimation = AnimationMin<double>(moveScaleAnimation, _currentEntranceScaleAnimation!);
1447+
_previousRotationAnimation = TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation);
1448+
_currentRotationAnimation = TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation);
1449+
}
14451450

1451+
_extendedCurrentScaleAnimation = _currentScaleAnimation.drive(CurveTween(curve: const Interval(0.0, 0.1)));
14461452
_currentScaleAnimation.addListener(_onProgressChanged);
14471453
_previousScaleAnimation.addListener(_onProgressChanged);
14481454
}

packages/flutter/test/material/scaffold_test.dart

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3256,6 +3256,145 @@ void main() {
32563256
// The bottom sheet is dismissed.
32573257
expect(find.byKey(sheetKey), findsNothing);
32583258
});
3259+
3260+
// This is a regression test for https://github.com/flutter/flutter/issues/145585.
3261+
testWidgets('FAB default entrance and exit animations', (WidgetTester tester) async {
3262+
bool showFab = false;
3263+
3264+
await tester.pumpWidget(
3265+
MaterialApp(
3266+
home: StatefulBuilder(
3267+
builder: (BuildContext context, StateSetter setState) {
3268+
return Scaffold(
3269+
body: ElevatedButton(
3270+
onPressed: () {
3271+
setState(() {
3272+
showFab = !showFab;
3273+
});
3274+
},
3275+
child: const Text('Toggle FAB'),
3276+
),
3277+
floatingActionButton: !showFab
3278+
? null
3279+
: FloatingActionButton(
3280+
onPressed: () {},
3281+
child: const Icon(Icons.add),
3282+
),
3283+
);
3284+
},
3285+
),
3286+
),
3287+
);
3288+
3289+
// FAB is not visible.
3290+
expect(find.byType(FloatingActionButton), findsNothing);
3291+
3292+
// Tap the button to show the FAB.
3293+
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
3294+
await tester.pump();
3295+
await tester.pump(const Duration(milliseconds: 100)); // Advance the animation by 100ms.
3296+
// FAB is partially animated in.
3297+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, closeTo(743.8, 0.1));
3298+
3299+
await tester.pump(const Duration(milliseconds: 100)); // Advance the animation by 100ms.
3300+
// FAB is fully animated in.
3301+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
3302+
3303+
// Tap the button to hide the FAB.
3304+
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
3305+
await tester.pump();
3306+
await tester.pump(const Duration(milliseconds: 100)); // Advance the animation by 100ms.
3307+
// FAB is partially animated out.
3308+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, closeTo(747.1, 0.1));
3309+
3310+
await tester.pump(const Duration(milliseconds: 100)); // Advance the animation by 100ms.
3311+
// FAB is fully animated out.
3312+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(756.0));
3313+
3314+
await tester.pump(const Duration(milliseconds: 50)); // Advance the animation by 50ms.
3315+
// FAB is not visible.
3316+
expect(find.byType(FloatingActionButton), findsNothing);
3317+
});
3318+
3319+
// This is a regression test for https://github.com/flutter/flutter/issues/145585.
3320+
testWidgets('FAB default entrance and exit animations can be disabled', (WidgetTester tester) async {
3321+
bool showFab = false;
3322+
FloatingActionButtonLocation fabLocation = FloatingActionButtonLocation.endFloat;
3323+
3324+
await tester.pumpWidget(
3325+
MaterialApp(
3326+
home: StatefulBuilder(
3327+
builder: (BuildContext context, StateSetter setState) {
3328+
return Scaffold(
3329+
// Disable FAB animations.
3330+
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
3331+
floatingActionButtonLocation: fabLocation,
3332+
body: Column(
3333+
children: <Widget>[
3334+
ElevatedButton(
3335+
onPressed: () {
3336+
setState(() {
3337+
showFab = !showFab;
3338+
});
3339+
},
3340+
child: const Text('Toggle FAB'),
3341+
),
3342+
ElevatedButton(
3343+
onPressed: () {
3344+
setState(() {
3345+
fabLocation = FloatingActionButtonLocation.centerFloat;
3346+
});
3347+
},
3348+
child: const Text('Update FAB Location'),
3349+
),
3350+
],
3351+
),
3352+
floatingActionButton: !showFab
3353+
? null
3354+
: FloatingActionButton(
3355+
onPressed: () {},
3356+
child: const Icon(Icons.add),
3357+
),
3358+
);
3359+
},
3360+
),
3361+
),
3362+
);
3363+
3364+
// FAB is not visible.
3365+
expect(find.byType(FloatingActionButton), findsNothing);
3366+
3367+
// Tap the button to show the FAB.
3368+
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
3369+
await tester.pump();
3370+
// FAB is visible.
3371+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
3372+
3373+
// Tap the button to hide the FAB.
3374+
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
3375+
await tester.pump();
3376+
// FAB is not visible.
3377+
expect(find.byType(FloatingActionButton), findsNothing);
3378+
3379+
// Tap the button to show the FAB.
3380+
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
3381+
await tester.pump();
3382+
// FAB is visible.
3383+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
3384+
3385+
// Tap the update location button.
3386+
await tester.tap(find.widgetWithText(ElevatedButton, 'Update FAB Location'));
3387+
await tester.pump();
3388+
3389+
// FAB is visible at the new location.
3390+
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(372.0));
3391+
3392+
// Tap the button to hide the FAB.
3393+
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
3394+
await tester.pump();
3395+
// FAB is not visible.
3396+
expect(find.byType(FloatingActionButton), findsNothing);
3397+
});
32593398
}
32603399

32613400
class _GeometryListener extends StatefulWidget {

0 commit comments

Comments
 (0)