Add ability to disable FloatingActionButton scale and rotation animations using FloatingActionButtonAnimator.noAnimation (#146126)
fixes [[Proposal] Allow disabling the scaling animation of the FloatingActionButton](https://github.com/flutter/flutter/issues/145585) ### Using default `FloatingActionButton` animations  ### Using `FloatingActionButtonAnimator.noAnimation` 
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [Scaffold.floatingActionButtonAnimator].
|
||||
|
||||
void main() => runApp(const ScaffoldFloatingActionButtonAnimatorApp());
|
||||
|
||||
class ScaffoldFloatingActionButtonAnimatorApp extends StatelessWidget {
|
||||
const ScaffoldFloatingActionButtonAnimatorApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: ScaffoldFloatingActionButtonAnimatorExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum FabAnimator { defaultStyle, none }
|
||||
const List<(FabAnimator, String)> fabAnimatoregments = <(FabAnimator, String)>[
|
||||
(FabAnimator.defaultStyle, 'Default'),
|
||||
(FabAnimator.none, 'None'),
|
||||
];
|
||||
|
||||
enum FabLocation { centerFloat, endFloat, endTop }
|
||||
const List<(FabLocation, String)> fabLocationegments = <(FabLocation, String)>[
|
||||
(FabLocation.centerFloat, 'centerFloat'),
|
||||
(FabLocation.endFloat, 'endFloat'),
|
||||
(FabLocation.endTop, 'endTop'),
|
||||
];
|
||||
|
||||
class ScaffoldFloatingActionButtonAnimatorExample extends StatefulWidget {
|
||||
const ScaffoldFloatingActionButtonAnimatorExample({super.key});
|
||||
|
||||
@override
|
||||
State<ScaffoldFloatingActionButtonAnimatorExample> createState() => _ScaffoldFloatingActionButtonAnimatorExampleState();
|
||||
}
|
||||
|
||||
class _ScaffoldFloatingActionButtonAnimatorExampleState extends State<ScaffoldFloatingActionButtonAnimatorExample> {
|
||||
Set<FabAnimator> _selectedFabAnimator = <FabAnimator>{FabAnimator.defaultStyle};
|
||||
Set<FabLocation> _selectedFabLocation = <FabLocation>{FabLocation.endFloat};
|
||||
FloatingActionButtonAnimator? _floatingActionButtonAnimator;
|
||||
FloatingActionButtonLocation? _floatingActionButtonLocation;
|
||||
bool _showFab = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButtonLocation: _floatingActionButtonLocation,
|
||||
floatingActionButtonAnimator: _floatingActionButtonAnimator,
|
||||
appBar: AppBar(title: const Text('FloatingActionButtonAnimator Sample')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SegmentedButton<FabAnimator>(
|
||||
selected: _selectedFabAnimator,
|
||||
onSelectionChanged: (Set<FabAnimator> styles) {
|
||||
setState(() {
|
||||
_floatingActionButtonAnimator = switch (styles.first) {
|
||||
FabAnimator.defaultStyle => null,
|
||||
FabAnimator.none => FloatingActionButtonAnimator.noAnimation,
|
||||
};
|
||||
_selectedFabAnimator = styles;
|
||||
});
|
||||
},
|
||||
segments: fabAnimatoregments
|
||||
.map<ButtonSegment<FabAnimator>>(((FabAnimator, String) fabAnimator) {
|
||||
final FabAnimator animator = fabAnimator.$1;
|
||||
final String label = fabAnimator.$2;
|
||||
return ButtonSegment<FabAnimator>(value: animator, label: Text(label));
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SegmentedButton<FabLocation>(
|
||||
selected: _selectedFabLocation,
|
||||
onSelectionChanged: (Set<FabLocation> styles) {
|
||||
setState(() {
|
||||
_floatingActionButtonLocation = switch (styles.first) {
|
||||
FabLocation.centerFloat => FloatingActionButtonLocation.centerFloat,
|
||||
FabLocation.endFloat => FloatingActionButtonLocation.endFloat,
|
||||
FabLocation.endTop => FloatingActionButtonLocation.endTop,
|
||||
};
|
||||
_selectedFabLocation = styles;
|
||||
});
|
||||
},
|
||||
segments: fabLocationegments
|
||||
.map<ButtonSegment<FabLocation>>(((FabLocation, String) fabLocation) {
|
||||
final FabLocation location = fabLocation.$1;
|
||||
final String label = fabLocation.$2;
|
||||
return ButtonSegment<FabLocation>(value: location, label: Text(label));
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showFab = !_showFab;
|
||||
});
|
||||
},
|
||||
icon: Icon(_showFab ? Icons.visibility_off : Icons.visibility),
|
||||
label: const Text('Toggle FAB'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: !_showFab
|
||||
? null
|
||||
: FloatingActionButton(
|
||||
onPressed: () {},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_api_samples/material/scaffold/scaffold.floating_action_button_animator.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('FloatingActionButton animation can be customized', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.ScaffoldFloatingActionButtonAnimatorApp(),
|
||||
);
|
||||
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
|
||||
// Test default FloatingActionButtonAnimator.
|
||||
// Tap the toggle button to show the FAB.
|
||||
await tester.tap(find.text('Toggle FAB'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100)); // Advance animation by 100ms.
|
||||
// FAB is partially animated in.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, closeTo(743.8, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100)); // Advance animation by 100ms.
|
||||
// FAB is fully animated in.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
|
||||
|
||||
// Tap the toggle button to hide the FAB.
|
||||
await tester.tap(find.text('Toggle FAB'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100)); // Advance animation by 100ms.
|
||||
// FAB is partially animated out.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, closeTo(747.1, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100)); // Advance animation by 100ms.
|
||||
// FAB is fully animated out.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(756.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50)); // Advance animation by 50ms.
|
||||
// FAB is hidden.
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
|
||||
// Select 'None' to disable animation.
|
||||
await tester.tap(find.text('None'));
|
||||
await tester.pump();
|
||||
|
||||
// Test no animation FloatingActionButtonAnimator.
|
||||
await tester.tap(find.text('Toggle FAB'));
|
||||
await tester.pump();
|
||||
// FAB is immediately shown.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
|
||||
|
||||
// Tap the toggle button to hide the FAB.
|
||||
await tester.tap(find.text('Toggle FAB'));
|
||||
await tester.pump();
|
||||
// FAB is immediately hidden.
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -937,6 +937,18 @@ abstract class FloatingActionButtonAnimator {
|
||||
/// the animation from the beginning, regardless of the original state of the animation.
|
||||
double getAnimationRestart(double previousValue) => 0.0;
|
||||
|
||||
/// Creates an instance of [FloatingActionButtonAnimator] where the [FloatingActionButton]
|
||||
/// does not animate on entrance and exit when [FloatingActionButtonLocation] is shown
|
||||
/// or hidden and when transitioning between [FloatingActionButtonLocation]s.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample showcases how to override [FloatingActionButton] entrance and exit animations
|
||||
/// using [FloatingActionButtonAnimator.noAnimation] in [Scaffold.floatingActionButtonAnimator].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/scaffold/scaffold.floating_action_button_animator.0.dart **
|
||||
/// {@end-tool}
|
||||
static const FloatingActionButtonAnimator noAnimation = _NoAnimationFabMotionAnimator();
|
||||
|
||||
@override
|
||||
String toString() => objectRuntimeType(this, 'FloatingActionButtonAnimator');
|
||||
}
|
||||
@@ -993,6 +1005,25 @@ class _ScalingFabMotionAnimator extends FloatingActionButtonAnimator {
|
||||
double getAnimationRestart(double previousValue) => math.min(1.0 - previousValue, previousValue);
|
||||
}
|
||||
|
||||
class _NoAnimationFabMotionAnimator extends FloatingActionButtonAnimator {
|
||||
const _NoAnimationFabMotionAnimator();
|
||||
|
||||
@override
|
||||
Offset getOffset({required Offset begin, required Offset end, required double progress}) {
|
||||
return end;
|
||||
}
|
||||
|
||||
@override
|
||||
Animation<double> getRotationAnimation({required Animation<double> parent}) {
|
||||
return const AlwaysStoppedAnimation<double>(1.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Animation<double> getScaleAnimation({required Animation<double> parent}) {
|
||||
return const AlwaysStoppedAnimation<double>(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// An animation that swaps from one animation to the next when the [parent] passes [swapThreshold].
|
||||
///
|
||||
/// The [value] of this animation is the value of [first] when [parent.value] < [swapThreshold]
|
||||
|
||||
@@ -1436,13 +1436,19 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
|
||||
final Animation<double> moveRotationAnimation = widget.fabMotionAnimator.getRotationAnimation(parent: widget.fabMoveAnimation);
|
||||
|
||||
// Aggregate the animations.
|
||||
_previousScaleAnimation = AnimationMin<double>(moveScaleAnimation, _previousExitScaleAnimation!);
|
||||
_currentScaleAnimation = AnimationMin<double>(moveScaleAnimation, _currentEntranceScaleAnimation!);
|
||||
if (widget.fabMotionAnimator == FloatingActionButtonAnimator.noAnimation) {
|
||||
_previousScaleAnimation = moveScaleAnimation;
|
||||
_currentScaleAnimation = moveScaleAnimation;
|
||||
_previousRotationAnimation = TrainHoppingAnimation(moveRotationAnimation, null);
|
||||
_currentRotationAnimation = TrainHoppingAnimation(moveRotationAnimation, null);
|
||||
} else {
|
||||
_previousScaleAnimation = AnimationMin<double>(moveScaleAnimation, _previousExitScaleAnimation!);
|
||||
_currentScaleAnimation = AnimationMin<double>(moveScaleAnimation, _currentEntranceScaleAnimation!);
|
||||
_previousRotationAnimation = TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation);
|
||||
_currentRotationAnimation = TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation);
|
||||
}
|
||||
|
||||
_extendedCurrentScaleAnimation = _currentScaleAnimation.drive(CurveTween(curve: const Interval(0.0, 0.1)));
|
||||
|
||||
_previousRotationAnimation = TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation);
|
||||
_currentRotationAnimation = TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation);
|
||||
|
||||
_currentScaleAnimation.addListener(_onProgressChanged);
|
||||
_previousScaleAnimation.addListener(_onProgressChanged);
|
||||
}
|
||||
|
||||
@@ -3256,6 +3256,145 @@ void main() {
|
||||
// The bottom sheet is dismissed.
|
||||
expect(find.byKey(sheetKey), findsNothing);
|
||||
});
|
||||
|
||||
// This is a regression test for https://github.com/flutter/flutter/issues/145585.
|
||||
testWidgets('FAB default entrance and exit animations', (WidgetTester tester) async {
|
||||
bool showFab = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Scaffold(
|
||||
body: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showFab = !showFab;
|
||||
});
|
||||
},
|
||||
child: const Text('Toggle FAB'),
|
||||
),
|
||||
floatingActionButton: !showFab
|
||||
? null
|
||||
: FloatingActionButton(
|
||||
onPressed: () {},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// FAB is not visible.
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
|
||||
// Tap the button to show the FAB.
|
||||
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100)); // Advance the animation by 100ms.
|
||||
// FAB is partially animated in.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, closeTo(743.8, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100)); // Advance the animation by 100ms.
|
||||
// FAB is fully animated in.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
|
||||
|
||||
// Tap the button to hide the FAB.
|
||||
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100)); // Advance the animation by 100ms.
|
||||
// FAB is partially animated out.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, closeTo(747.1, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100)); // Advance the animation by 100ms.
|
||||
// FAB is fully animated out.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(756.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50)); // Advance the animation by 50ms.
|
||||
// FAB is not visible.
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
});
|
||||
|
||||
// This is a regression test for https://github.com/flutter/flutter/issues/145585.
|
||||
testWidgets('FAB default entrance and exit animations can be disabled', (WidgetTester tester) async {
|
||||
bool showFab = false;
|
||||
FloatingActionButtonLocation fabLocation = FloatingActionButtonLocation.endFloat;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Scaffold(
|
||||
// Disable FAB animations.
|
||||
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
|
||||
floatingActionButtonLocation: fabLocation,
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showFab = !showFab;
|
||||
});
|
||||
},
|
||||
child: const Text('Toggle FAB'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
fabLocation = FloatingActionButtonLocation.centerFloat;
|
||||
});
|
||||
},
|
||||
child: const Text('Update FAB Location'),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: !showFab
|
||||
? null
|
||||
: FloatingActionButton(
|
||||
onPressed: () {},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// FAB is not visible.
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
|
||||
// Tap the button to show the FAB.
|
||||
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
|
||||
await tester.pump();
|
||||
// FAB is visible.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
|
||||
|
||||
// Tap the button to hide the FAB.
|
||||
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
|
||||
await tester.pump();
|
||||
// FAB is not visible.
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
|
||||
// Tap the button to show the FAB.
|
||||
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
|
||||
await tester.pump();
|
||||
// FAB is visible.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(728.0));
|
||||
|
||||
// Tap the update location button.
|
||||
await tester.tap(find.widgetWithText(ElevatedButton, 'Update FAB Location'));
|
||||
await tester.pump();
|
||||
|
||||
// FAB is visible at the new location.
|
||||
expect(tester.getTopLeft(find.byType(FloatingActionButton)).dx, equals(372.0));
|
||||
|
||||
// Tap the button to hide the FAB.
|
||||
await tester.tap(find.widgetWithText(ElevatedButton, 'Toggle FAB'));
|
||||
await tester.pump();
|
||||
// FAB is not visible.
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
class _GeometryListener extends StatefulWidget {
|
||||
|
||||
Reference in New Issue
Block a user