Skip to content

Commit

Permalink
Add ability to disable FloatingActionButton scale and rotation anim…
Browse files Browse the repository at this point in the history
…ations using `FloatingActionButtonAnimator.noAnimation` (#146126)

fixes [[Proposal] Allow disabling the scaling animation of the FloatingActionButton](#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)
  • Loading branch information
TahaTesser committed Apr 29, 2024
1 parent 098e7e7 commit 257df5e
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -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);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down Expand Up @@ -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]
Expand Down
18 changes: 12 additions & 6 deletions packages/flutter/lib/src/material/scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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!);
_extendedCurrentScaleAnimation = _currentScaleAnimation.drive(CurveTween(curve: const Interval(0.0, 0.1)));

_previousRotationAnimation = TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation);
_currentRotationAnimation = TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation);
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)));
_currentScaleAnimation.addListener(_onProgressChanged);
_previousScaleAnimation.addListener(_onProgressChanged);
}
Expand Down
139 changes: 139 additions & 0 deletions packages/flutter/test/material/scaffold_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 257df5e

Please sign in to comment.