Reputation: 6038
I've followed the various animation tutorials on flutter.io, (tween, stagger, transitions, etc.) and its all great.
What I would like to explore is how to actually make custom animations based on the composition of a UI object.
Lets take a simple example, a Pause -> Play animation.
At first we have a Pause icon, two vertical bars.
Let's say I would like to
That would look like a play button, and not a pause button anymore.
How would I achieve that kind of custom animation ? I'm assuming I can't work with the icons class. And I'm pretty sure I shouldn't do that with Widgets and just move them around.
Where would I go to start exploring that kind of precision in animations?
Upvotes: 2
Views: 4005
Reputation: 40513
The answer from @Alaric points you at a couple of packages but doesn't really give any justification for why you'd use them.
The issue at hand is that the animation you're talking about is moderately complicated in terms of how it actually works. There are multiple items which change over time and possibly even become one bigger item.
There are two approaches you could take to solving this problem. The first is to use an external animation tool to create this animation, using whichever features the animation tool has to do item changing and merging. Then once you have an animation which runs to your satisfaction, you have to import it into your project somehow. That's where the fluttie and flare_flutter plugins come in - if you used Aftereffects, you use Lottie to export the file and then the fluttie plugin to show it. Flare is slightly simpler as it's meant for flutter, but you still create the animation externally and then add the file to your assets to be rendered.
The other approach is to do the animation yourself. That entails three things:
The widget containing the animation could probably also be the controller if you use a GlobalKey to access it and expose start/stop methods, but that's a bit messy. It's better to have an external object that is the controller - and you could probably even use an AnimationController as-is although it would be less 'clean'.
If you don't pass it in, you'd probably have an AnimationController in your widget that you start and stop from your controller or class. It would essentially just go from 0 to 1 and back, and would be responsible for rebuilding the CustomPainter (probably using an AnimatedBuilder).
This is a very basic example that doesn't need an external controller as the gesture detection happens within the widget. Note that I'm not calling setState every time the 'started' member is set, because I don't actually want it to rebuild when it changes.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: StartStop(),
),
),
);
}
}
class StartStop extends StatefulWidget {
@override
StartStopState createState() {
return new StartStopState();
}
}
class StartStopState extends State<StartStop> with TickerProviderStateMixin<StartStop> {
bool started = false;
AnimationController animationController;
@override
void initState() {
animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 300));
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
started ? animationController.forward() : animationController.reverse();
started = !started;
},
child: SizedBox(
width: 100,
height: 100,
child: AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return CustomPaint(
painter: StartStopPainter(animationController.value),
size: Size.infinite,
child: child,
);
},
),
),
);
}
}
class StartStopPainter extends CustomPainter {
final double percentAnimated;
StartStopPainter(this.percentAnimated) : assert(percentAnimated >= 0 && percentAnimated <= 1);
@override
void paint(Canvas canvas, Size size) {
var pausePaint = Paint()..color = Colors.black.withOpacity(1 - percentAnimated);
canvas.drawRect(Rect.fromLTRB(20, 10, 40, 90), pausePaint);
canvas.drawRect(Rect.fromLTRB(60, 10, 80, 90), pausePaint);
var playPaint = Paint()..color = Colors.black.withOpacity(percentAnimated);
canvas.drawPath(Path()..addPolygon([Offset(20, 10), Offset(20, 90), Offset(80, 50)], true), playPaint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
I'll leave the actual custom part of the animation (where you make the rectangle change to a triangle etc) to you. Instead of using opacity and a few different paint calls, you'd simply be using the input percentAnimated
to decide which path or polygon to draw.
Upvotes: 6