Cool Flutter packages: flutter_physics
Last week I discovered a pretty fun little animations package built by @sangddn that lets you replace classic Flutter animation controller with one that follows spring-based simulations: flutter_physics.
The concept of simulation is present in Flutter, but I’ve always found it a bit tricky to implement. I tried it few times, but eventually would switch to either custom calculations or basic curves.
Normally when building this kind of pseudo-3d movement you wouldn’t use typical tweens and curves, as they lack inertia. With flutter_physics you can follow the same approach as with any animation, but just replace the controller with PhysicsController or TweenAnimationBuilder with PhysicsBuilder. In the example above the card starts to move more naturally and just feels better to the eye and finger.
One area where I feel the physics-based animation would be beneficial is bottom sheet movement. I think most of the implementations, including the Flutter bottom sheets, don’t feel as nice as some of the provided by iOS. I’d love to change it and will experimented with using flutter_physics for sheets animations.
Here’s the behavior with ordinary Flutter AnimationController:
And here after just replacing the controller with PhysicsController (via adapter - source code below):
If you want to try something refreshing, play with the flutter_physics and maybe share some feedback about how you approach animations with John Ryan here on the Flutter Forum.
Source code
import 'package:flutter/material.dart';
import 'package:flutter_physics/flutter_physics.dart';
class AnimationsControllerAdapter implements AnimationController {
AnimationsControllerAdapter(this.physicsController);
final PhysicsController physicsController;
@override
Duration? get duration => physicsController.duration;
@override
set duration(Duration? value) {
physicsController.duration = value;
}
@override
Duration? get reverseDuration => physicsController.reverseDuration;
@override
set reverseDuration(Duration? value) {
physicsController.reverseDuration = value;
}
@override
double get value => physicsController.value;
@override
set value(double value) {
physicsController.value = value;
}
@override
void addListener(VoidCallback listener) {
physicsController.addListener(listener);
}
@override
void addStatusListener(AnimationStatusListener listener) {
physicsController.addStatusListener(listener);
}
@override
TickerFuture animateBack(double target,
{Duration? duration, Curve curve = Curves.linear}) {
return physicsController.animateTo(
target,
duration: duration,
physics: curve,
);
}
@override
TickerFuture animateTo(double target,
{Duration? duration, Curve curve = Curves.linear}) {
return physicsController.animateTo(
target,
duration: duration,
physics: curve,
);
}
@override
TickerFuture animateWith(Simulation simulation) {
return physicsController.animateWith(simulation);
}
@override
AnimationBehavior get animationBehavior =>
physicsController.animationBehavior;
@override
void clearListeners() {
physicsController.clearListeners();
}
@override
void clearStatusListeners() {
physicsController.clearStatusListeners();
}
@override
String? get debugLabel => physicsController.debugLabel;
@override
void didRegisterListener() {
// TODO: implement didRegisterListener
}
@override
void didUnregisterListener() {
physicsController.didUnregisterListener();
}
@override
void dispose() {
physicsController.dispose();
}
@override
Animation<U> drive<U>(Animatable<U> child) {
return physicsController.drive(child);
}
@override
TickerFuture fling(
{double velocity = 1.0,
SpringDescription? springDescription,
AnimationBehavior? animationBehavior}) {
return physicsController.fling(
velocity: velocity,
springDescription: springDescription,
animationBehavior: animationBehavior,
);
}
@override
TickerFuture forward({double? from}) {
return physicsController.forward(from: from);
}
@override
bool get isAnimating => physicsController.isAnimating;
@override
bool get isCompleted => physicsController.isCompleted;
@override
bool get isDismissed => physicsController.isDismissed;
@override
bool get isForwardOrCompleted => physicsController.isForwardOrCompleted;
@override
Duration? get lastElapsedDuration => physicsController.lastElapsedDuration;
@override
double get lowerBound => physicsController.lowerBound;
@override
void notifyListeners() {
physicsController.notifyListeners();
}
@override
void notifyStatusListeners(AnimationStatus status) {
physicsController.notifyStatusListeners(status);
}
@override
void removeListener(VoidCallback listener) {
physicsController.removeListener(listener);
}
@override
void removeStatusListener(AnimationStatusListener listener) {
physicsController.removeStatusListener(listener);
}
@override
TickerFuture repeat(
{double? min,
double? max,
bool reverse = false,
Duration? period,
int? count}) {
return physicsController.repeat(
min: min,
max: max,
reverse: reverse,
period: period,
count: count,
);
}
@override
void reset() {
physicsController.reset();
}
@override
void resync(TickerProvider vsync) {
physicsController.resync(vsync);
}
@override
TickerFuture reverse({double? from}) {
return physicsController.reverse(from: from);
}
@override
AnimationStatus get status => physicsController.status;
@override
void stop({bool canceled = true}) {
physicsController.stop(canceled: canceled);
}
@override
String toStringDetails() {
return physicsController.toStringDetails();
}
@override
TickerFuture toggle({double? from}) {
throw UnimplementedError('toggle is not implemented yet');
}
@override
double get upperBound => physicsController.upperBound;
@override
double get velocity => physicsController.velocity;
@override
Animation<double> get view => physicsController.view;
}
class BottomSheetPage extends StatefulWidget {
const BottomSheetPage({super.key});
@override
State<BottomSheetPage> createState() => _BottomSheetPageState();
}
class _BottomSheetPageState extends State<BottomSheetPage>
with TickerProviderStateMixin {
late PhysicsController controller;
@override
void initState() {
controller = PhysicsController(vsync: this);
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
transitionAnimationController:
AnimationsControllerAdapter(controller),
builder: (context) {
return Container(
height: 200,
color: Colors.blueGrey,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Modal BottomSheet'),
ElevatedButton(
child: const Text('Close BottomSheet'),
onPressed: () => Navigator.pop(context),
),
],
),
),
);
},
);
},
child: Text('Show bottom sheet'),
),
),
);
}
}