The effect is as follows:
Sharing is an essential quality for every good programmer
1. Basic Concepts
Animation
An abstract class. It has nothing to do with UI rendering, and its main function is to save the interpolation and state of the animation. AddListener (): Add a “draw Animation” listener to the Animation. Each frame will be called. A common behavior is to change the state and call setState() to trigger a UI redraw. AddStatesListener (): Adds “Animation state change” listeners to the Animation, such as start, end, etc.
Note: The build method of the animated widget is constantly called during the animation.
Curve
The animation process of Flutter can be uniform speed, uniform acceleration, acceleration first and then deceleration, etc. Curve is used to describe the animation process of Flutter, and the animation of uniform speed is called linear and the animation of non-uniform speed is called nonlinear. The curve of the animation is usually specified by a CurveAnimation
/ / linear: uniform / / decelerate: uniformly retarded / / ease: began to accelerate, reduction/behind/easeIn: start slowly and fast/behind/easeOut: starting fast, slow/behind/easeInOut: Final CurveAnimation curve = CurveAnimation(Parent: Controller, curve: curve.easein);Copy the code
You can also customize Curve, such as a sine Curve
Class ShakeCurve extends Curve{@override double transform(double t) {return sin(t * PI * 2); }}Copy the code
AnimationController
A subclass of Animation that controls the Animation contains forward() : forward start (the Animation is running from the beginning to the end) Reverse () : reverse start Stop () : stop at the beginning completed() : the Animation stopped at the beginning completed() : The AnimationController generates a new value for each frame of the animation and, by default, linearly generates a number from 0.0 to 1.0 (the default range) for a given period of time. The code is as follows:
AnimationController controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
Copy the code
The resulting numeric range can be specified by lowerBound and lowerBound as follows:
AnimationController controller = AnimationController( vsync: this, duration: Duration(seconds: 1), lowerBound: 10.0, upperBound: 20.0);Copy the code
Ticker
Note from above that when creating an AnimationController, we need to pass a vsync parameter that takes a TickerProvider object and creates a Ticker as follows:
Abstract Class TickerProvider{// Create a Ticker with a callback Ticker createTicker(TickerCallback onTick); }Copy the code
The Flutter application is bound to a SchedulerBinding every time it starts up. The SchedulerBinding can add callbacks to the screen for each time a Flutter application starts up. The Ticker uses the SchedulerBinding to add callbacks to the screen refresh. TickerCallback is called every time the screen is refreshed. Using Ticker (instead of Timer) to drive animations prevents off-screen animations (when the animation’s UI is no longer on the current screen, such as the lock screen) from consuming unnecessary resources because the screen refresh notifies the bound SchedulerBinding in Flutter. The Ticker is driven by SchedulerBinding. Since the lock screen hi stops refreshing, the Ticker will no longer trigger. Usually we will SingleTickerProviderStateMixin added to the State and State object vsync value (this).
Tween
Tween animation. The default AnimationController object value is [0.0,1.0]. You can use Tween to add mappings to generate different ranges or data types (double, Color, EdgeInsets… The following code
Tween (the begin: 100.0, end: 300.0)Copy the code
Note: Tween inherits from Animatable
, not Animation
, which defines the mapping rules for Animation values.
The Tween object does not store any state and provides the evaluate
Animation > method to obtain the current value of the Animation (animation.value()).
Tween is just a mapping. Control of the animation is still controlled by the AnimationController, so you need tween.animate (Controller) to pass the controller to Tween.
Generate int values from 0 to 255 in 1s
AnimationController controller = AnimationController( vsync: this, duration: Duration(seconds: 1), ); Animation () returns an animation, not an Animatable animation <int> alpha = IntTween(begin: 0,end: 255).animate(controller);Copy the code
Second, the sample
Here is a closer look at Flutter animation with some examples
1. Transparency animation
Transparency changes from 1.0 to 0.0 with the following code:
/ / need to be mixed with SingleTickerProviderStateMixin class not extends StatefulWidget {@ override _Demo1State createState () = > _Demo1State(); } class _Demo1State extends State<Demo1> with SingleTickerProviderStateMixin{ AnimationController _controller; Animation _animation ; @override void initState() { super.initState(); AnimationController(vsync: this,duration:) AnimationController(vsync: this,duration:) Duration(seconds: 1)).. addListener(() { setState(() {}); }); _animation = Tween(begin: 1.0,end:) // Interval: begin indicates how long it takes to start the animation. 0.1). The animate (CurvedAnimation (parent: _controller, curve, the Interval (0.0, 0.5, the curve: Curves. The linear))); // _animation = Tween(begin: 1.0,end: 0.2). Chain (CurveTween(curve: Curves.easeIn)).animate(_controller); // _animation = _controller.drive(Tween(begin: 1.0,end: 0.1)). Drive (CurveTween(curve: Curves. LinearToEaseOut)); } @override Widget build(BuildContext context) { return InkWell( onTap: () => _controller.forward(), child: Opacity( opacity: _animation.value, child: Container( width: 100,height: 100, color: Colors.greenAccent, child: Center(child: Text("Demo1"),), ), ), ); } @override void dispose() { _controller? .dispose(); super.dispose(); }}Copy the code
2. Color changesColor. Lerp linear interpolation between two colors
Red – > green
class Demo2 extends StatefulWidget {
@override
_Demo2State createState() => _Demo2State();
}
class _Demo2State extends State<Demo2> with SingleTickerProviderStateMixin{
AnimationController _controller;
Color _color = Colors.red;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1))
..addListener(() {
setState(() {
_color = Color.lerp(Colors.red, Colors.green, _controller.value);
});
});
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
_controller.forward();
},
child: Container(
height: 100,width: 100,
color: _color,
child: Center(child: Text("Demo2"),),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Copy the code
3. Widget size changes
class Demo3 extends StatefulWidget {
@override
_Demo3State createState() => _Demo3State();
}
class _Demo3State extends State<Demo3> with SingleTickerProviderStateMixin{
double _size = 100;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1),lowerBound: 100,upperBound: 200)
..addListener(() {
setState(() {
_size = _controller.value;
});
})..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
_controller.forward();
},
child: Container(
width: _size,height: _size,
color: Colors.redAccent,
child: Center(child: Text("Demo3"),),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Copy the code
In fact, the system provides a large number of Tween animation, greatly convenient to use, commonly used data types are generally provided, when necessary, you might as well knock to see.
For example, the common displacement animation uses EdgeInsetsTween, and the Color change can be realized using ColorTween (check the source code, which is essentially also using color.lerp). The ColorTween code is as follows: The animation effect is the same as Demo3.
class ColorTweenDemo extends StatefulWidget {
@override
_ColorTweenDemoState createState() => _ColorTweenDemoState();
}
class _ColorTweenDemoState extends State<ColorTweenDemo> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation <Color> _animation;
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1))
..addListener(() {
setState(() { });
});
_animation = ColorTween(begin: Colors.red,end: Colors.green).animate(_controller);
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
_controller.forward();
},
child: Container(
height: 100,width: 100,
color: _animation.value,
),
);
}
}
Copy the code
4, a variety of animation combination
Mixin TickerProviderStateMixin with size change + rounded corner change + color change
class Demo4 extends StatefulWidget { @override _Demo4State createState() => _Demo4State(); } class _Demo4State extends State<Demo4> with TickerProviderStateMixin{// Each animation can create its own AnimationController to control and listen for different states AnimationController _controller; Animation <double> _sizeAnimation; Animation <BorderRadius> _radiusAnimation; Animation <Color> _colorAnimation; @override void initState() { super.initState(); _controller = AnimationController(vsync: this,duration: Duration(seconds: 1)).. addListener(() { setState(() {}); }).. addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); }}); _sizeAnimation = Tween(begin: 100.0,end: 200.0). Chain (CurveTween(curve: linear)). Animate (_controller); _radiusAnimation = BorderRadiusTween(begin: BorderRadius.zero,end: BorderRadius.circular(100)).animate(_controller); _colorAnimation = ColorTween(begin: Colors.redAccent,end: Colors.yellowAccent).chain(CurveTween(curve: Curves.linear)).animate(_controller); } @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, child: Center( child: InkWell( onTap: (){ _controller.forward(); }, child: Container( width: _sizeAnimation.value,height: _sizeAnimation.value, decoration: BoxDecoration( border: Border.all(width: 5,color: Colors.greenAccent), color: _colorAnimation.value, borderRadius: _radiusAnimation.value ), child: Center(child: Text("Demo4"),), ), ) ) ); } @override void dispose() { _controller.dispose(); super.dispose(); }}Copy the code
Effect:
5. Displacement of AnimatedWidget
Now we can find:
- Each time the UI is updated with addListener() and setState(), the code becomes redundant;
- SetState () means that the entire build method in the State class is re-executed.
To solve the above problem, we can use the AnimatedWidget to isolate the widgets that need to be animated. ‘ ‘AnimatedWidget’ encapsulates the details of calling setState()
Take displacement animation as an example:
class Demo5 extends StatefulWidget { @override _Demo5State createState() => _Demo5State(); } class _Demo5State extends State<Demo5> with SingleTickerProviderStateMixin{ AnimationController _controller; Animation <EdgeInsets>_animation; @override void initState() { super.initState(); _controller = AnimationController(vsync: this,duration: Duration(seconds: 2)).. addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); }}); _animation = _controller.drive(EdgeInsetsTween(begin: EdgeInsets.fromLTRB(50, 50, 0, 0),end: EdgeInsets.only(top: 200,left: 200))); } @override Widget build(BuildContext context) { Future.delayed(Duration(seconds: 1),(){ _controller.forward(); }); return EdgeInsetsDemo(animation: _animation,); } @override void dispose() { _controller.dispose(); super.dispose(); }} Class EdgeInsetsDemo extends AnimatedWidget{EdgeInsetsDemo({Key Key,Animation<EdgeInsets> animation}):super(key:key,listenable :animation); @override Widget build(BuildContext context) { final Animation<EdgeInsets> animation = listenable; return Container( margin: animation.value, width: 100, height: 100, color: Colors.redAccent, ); }}Copy the code
The effect is as follows:
6. Matrix changes for AnimatedBuilder
Using the AnimatedWidget, we can separate the Widget from the animation, but the animation rendering process is still in the AnimatedWidget. If we are animating a Widget, we need to implement another AnimatedWidget, but this is not elegant. AnimatedBuilder can separate out the rendering logic.
The example uses AnimatedBuilder to rotate the image around the X axis. The code is as follows:
class Demo6 extends StatefulWidget { @override _Demo6State createState() => _Demo6State(); } class _Demo6State extends State<Demo6> with SingleTickerProviderStateMixin{ AnimationController _controller; Animation <Matrix4> _animation; @override void initState() { super.initState(); _controller = AnimationController(vsync: this,duration: Duration(seconds: 2)).. addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); }}).. addListener(() { setState(() { }); }); _animation = Matrix4Tween(begin: matrix4.identity ().. RotateX (0.0), the end: Matrix4 identity (.). rotateX(pi)).animate(_controller); } @override Widget build(BuildContext context) { return Center( child: InkWell( onTap: (){ _controller.forward(); }, child: AnimatedBuilderDemo(animation: _animation,) ), ); } @override void dispose() { _controller.dispose(); super.dispose(); }} class AnimatedBuilderDemo extends AnimatedWidget{AnimatedBuilderDemo({Key) key,Animation<Matrix4> animation}):super(key :key,listenable:animation); // It looks like the child is specified twice, but the external child is passed to the AnimationBuilder, which then passes it to the anonymous constructor, and then the object is used as its child, The object returned by the AnimationBuilder is eventually inserted into the Widget tree. @override Widget build(BuildContext context) { final Animation animation = listenable; return AnimatedBuilder( animation: animation, child: Image.asset("assets/g.jpg",), builder: (BuildContext context,Widget child){ return Container( width: 120,height: 120, transform: animation.value, child: child, ); }); }}Copy the code
The effect is as follows:
Benefits:
- As with the AnimatedWidget, setSate() is not displayed;
- SetSate () will be called in the parent component, which will cause the parent component’s build to be re-called, meaning its child widgets will be re-built. With Builder, only the build of the animated widget itself is called again, avoiding unnecessary rebuild.
- AnimatedBuilder can encapsulate common transition effects to reuse animations, such as FadeTransition, ScaleTransition, Sizetransitin, etc. In many cases, these preset transition classes can be reused.
AnimatedContainer: Animation transition component
The animation described above requires the user to provide a manually managed AnimationController, which greatly increases the complexity of using the Flutter SDK. Many animation transition components are preset.
AnimatedContainer: When the Container property changes, it will perform a transition animation to the new position. AnimatedAlign: AnimatedAlign: AnimatedAlign: AnimatedAlign: AnimatedAlign: AnimatedAlign: AnimatedAlign AnimatedDefaultTextStyle: Animates the transition to the new state when the font style changes...Copy the code
The code is as follows:
class Demo7 extends StatefulWidget { @override _Demo7State createState() => _Demo7State(); } class _Demo7State extends State<Demo7> { Duration _duration = Duration(seconds: 1); bool _animation = false; @override Widget build(BuildContext context) { return Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Spacer(flex: 2,), AnimatedContainer( curve: Curves.linear, duration: _duration, width: _animation ? 200 : 100,height: _animation ? 200 : 100, decoration: BoxDecoration( borderRadius: _animation ? BorderRadius.circular(100) : BorderRadius.zero, color: _animation ? Colors.yellowAccent : Colors.redAccent, border: Border.all(width: 5,color: _animation ? Colors.greenAccent : Colors.black87) ), onEnd: (){setState(() => _animation =!_animation);}, child: InkWell(onTap: (){ setState(() => _animation = !_animation ); }, ), ), Spacer(), AnimatedContainer( alignment: Alignment.center, duration: _duration, width: _animation ? 200 : 150,height: _animation ? 100 : 80, decoration: BoxDecoration( color: Colors.greenAccent, borderRadius: _animation == false ? BorderRadius.circular(40) : BorderRadius.zero, ), child: AnimatedDefaultTextStyle( duration: _duration, child: Text("Hello world"), style: _animation ? TextStyle(color: Colors.black87,fontSize: 12,fontWeight: FontWeight.w100) : TextStyle(color: Colors.redAccent,fontSize: 20,fontWeight: Fontweight.w800),),), Spacer(), // AnimatedCrossFade when switching between two components. It is necessary to set the two child controls before and after animation. AnimatedCrossFade( duration: _duration, crossFadeState: _animation ? CrossFadeState.showSecond : CrossFadeState.showFirst, firstChild: Container( alignment: Alignment.center, width: 100,height: 120, color: Colors. RedAccent, Child: Text(" first ",style: TextStyle(color: color.greenaccent),), secondChild: Container( alignment: Alignment.center, width: 100,height: 100, decoration: BoxDecoration( color: Colors. GreenAccent, borderRadius: borderRadius. Circular (50)), Child: Text(" second ",style: TextStyle(color: Colors.redAccent)), ), ), Spacer(flex: 2,) ], ), ); }}Copy the code
The effect is as follows:
For example, the animation code for transparency changes in Demo1 can be simplified into the following code:
Class Demo7_1 extends StatefulWidget {@override _Demo7_1State createState() => _Demo7_1State(); } class _Demo7_1State extends State<Demo7_1> { bool _animation = false; @override Widget build(BuildContext context) {return Center(child: AnimatedOpacity(// opacity animation component duration: Duration(seconds: 1), opacity: _animation ? 0.0:1.0, onEnd: () {setState (() = > _animation =! _animation ); }, child: Container( width: 100,height: 100, color: Colors.redAccent, child: InkWell( onTap: (){ setState(() => _animation = ! _animation ); },),),),); }}Copy the code
8 AnimatedIcon.
One more interesting transition animation component is the AnimatedIcon. This is the animation icon that Flutter provides. For example, when the video playback button is clicked, the ICONS ▶️ — > ⏸ change. These Flutter ICONS are already built, and the Icon specifies another Icon by default after the animation is switched. The default Icon specified after the Icon ▶️ animation is ⏸.
The code is as follows:
class Demo8 extends StatefulWidget {
@override
_Demo8State createState() => _Demo8State();
}
class _Demo8State extends State<Demo8> with SingleTickerProviderStateMixin{
bool _animation = false;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: Duration(seconds: 1),vsync: this,)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
};
});
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
children: <Widget>[
setIcon(AnimatedIcons.play_pause),
setIcon(AnimatedIcons.add_event),
setIcon(AnimatedIcons.arrow_menu),
setIcon(AnimatedIcons.close_menu),
setIcon(AnimatedIcons.ellipsis_search),
setIcon(AnimatedIcons.event_add),
setIcon(AnimatedIcons.home_menu),
setIcon(AnimatedIcons.list_view),
InkWell(onTap: () => _controller.forward())
],
);
}
Widget setIcon (AnimatedIconData iconData) {
return Center(
child: AnimatedIcon(
size: 30,
icon: iconData,
progress: _controller,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Copy the code
The effect is as follows:
9. Route transition animation
Generally, the MaterialPageRoute or CupertinoPageRoute of iOS style is used for route jump. By default, the route jump is swiped up and down or swiped left and right.
So how do you customize the route switch animation? The answer is to use PageRouteBuilder. For example, the routing process can be animated in and out:
class Demo9 extends StatefulWidget { @override _Demo9State createState() => _Demo9State(); } class _Demo9State extends State<Demo9> { @override Widget build(BuildContext context) { return CupertinoButton( padding: EdgeInsets.zero, onPressed: (){ Navigator.push( context, PageRouteBuilder( transitionDuration: Duration(milliseconds: 500), pageBuilder: (BuildContext context,Animation animation,Animation secondAnimation){ return FadeTransition( opacity: animation, child: NewPage(), ); },),); }, child: Container( alignment: Alignment.center, width: double.infinity,height: double.infinity, child: TextStyle(color: color.black,fontSize: 30),),); }}Copy the code
The effect is as follows:
Let’s encapsulate the fade in transition,
class RCFadeRoute extends PageRoute{ final WidgetBuilder builder; @override final Duration transitionDuration; @override final bool opaque; @override final bool barrierDismissible; @override final Color barrierColor; @override final String barrierLabel; @override final bool maintainState; RCFadeRoute({ @required this.builder, this.transitionDuration = const Duration(milliseconds: 250), this.opaque = true, this.barrierDismissible = false, this.barrierColor, this.barrierLabel, this.maintainState = true }); @override Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => builder(context); @override Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { return FadeTransition( opacity: animation, child: builder(context), ); }}Copy the code
The jump animation code is simplified as follows:
Navigator.push(context, RCFadeRoute(builder: (context){ return NewPage(); }));Copy the code
Hero animation
Hero is a Widget that can “fly” between routes. Because the shared widget looks and looks different on the old and new routing pages, it transitions from the old route to the location specified in the new route. This generates a Hero animation that wraps the widget that Hero will share and provides the same tag. The specific code is as follows:
class Demo10 extends StatefulWidget { @override _Demo10State createState() => _Demo10State(); } class _Demo10State extends State<Demo10> { @override Widget build(BuildContext context) { return GridView.builder( padding: EdgeInsets.only(top: 10,left: 10,right: 10), itemCount: 15, itemBuilder: (BuildContext content,int index){ return Container( child: InkWell( child: Hero( tag: Child: image. asset("assets/g.jpg",fit: boxfit.cover,),), onTap: asset("assets/g.jpg",fit: boxfit.cover,), onTap: asset("assets/g.jpg",fit: boxfit.cover,) (){ Navigator.push( context, PageRouteBuilder( transitionDuration: Duration(milliseconds: 250), pageBuilder: (BuildContext context,Animation animation,Animation secondAnimation){ return FadeTransition( opacity: animation, child: HeroAnimationRoute("assets/g.jpg","assets/g.jpg"+"$index"), ); },),); },),); }, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount (mainAxisSpacing crossAxisCount: 3:10.0, crossAxisSpacing: 10.0,),); } } class HeroAnimationRoute extends StatelessWidget { final String imageName,tag; HeroAnimationRoute(this.imageName,this.tag); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black54, body: InkWell( onTap: (){ Navigator.of(context).pop(); }, child: Center( child: Hero( tag: tag, child: Image.asset(imageName,fit: BoxFit.cover,), ), ), ), ); }}Copy the code
The effect is as follows: