Introduction

In one of our apps we had to implement a long operation when clicking on a button, the operation has multiple phases and each phase representing a different status. Well, sound like an easy task, just show a loading indicator on the screen and maybe add some changing text on it regarding the current status. A nice and common solution, but fortunately for us our UX designer had a cool idea how to give a better experience to the user while the process is in progress and at the same time keep the rest of the app responsive to the user needs. So, if you need a variation of this in your app and want to learn some animation in Flutter and achieve something like this:

This article is for you…

Design your own custom button

First, let’s customize our button to fit the design. In my case, we needed a rounded corner button with gradient background color but you can easily customize it however you need. Let’s create a new class under the name “ProgressButton”:

import 'package :flutter/material.dart'; import 'package :flutter_progress_button_animation/hex_color.dart'; class ProgressButton extends StatelessWidget { final Widget child; final Gradient gradient; final double width; final double height; final Function onPressed; const ProgressButton ({ Key key, this .child, this .gradient, this .width = 320.0 , this .height = 50.0 , this .onPressed, List < Widget > children, }) : super (key: key); Widget build( BuildContext context) { return Container ( width: width, height: height, decoration: BoxDecoration ( gradient: LinearGradient ( begin: FractionalOffset .topCenter, end: FractionalOffset .bottomCenter, colors: [ HexColor ( "#00C7E7" ), HexColor ( "#009CCD" ), ], stops: [ 0.0 , 1.0 ]), borderRadius: new BorderRadius .circular( 25.0 ), ), child: Material ( color: Colors .transparent, child: InkWell ( onTap: onPressed, child: Center ( child: child, )), ), ); } }

We set the button width and height and adding a BoxDecoration for the gradient background and the rounded corners. We use the InkWell widget here because it responds to touch, it must be a child of the Material widget because the Material widget is where the InkWell reactions are actually painted.

Create phase animation behavior

Now, let's create a reusable StatefulWidget for our phase animation. Each phase will use the same widget but with different parameters which will cause slightly different behavior. In our case each phase executes a “fade in\fade out” animation and “move” animation when the whole process is done:

import 'package:flutter/material.dart' ; enum PhaseState { Processing, Moving } class PhaseAnimation extends StatefulWidget { final PhaseAnimationState phaseState = PhaseAnimationState(); final Color dominantColor; PhaseAnimation( this .dominantColor); State<StatefulWidget> createState () => phaseState; void run () { phaseState.run(); } void stop () { phaseState.stop(); } void move ( double dx) { phaseState.move(dx); } }

We define an enum for the phase state. We set the widget color with a parameter, this way the widget can be initialized with different colors and we define three methods:

run – the phase will start the “fade in\fade out” animation.

– the phase will start the “fade in\fade out” animation. stop – the phase will stop the animation.

– the phase will stop the animation. move – the phase will start the “move” animation.

Now, we will implement the phase animation state. First, we need to add withTickerProviderStateMixin to the state class, “Mixin” is a class with methods that can be used by other classes without the need to inherit from that class. The TickerProviderStateMixin provides Ticker objects that are configured to only tick while the current tree is enabled:

class PhaseAnimationState extends State<PhaseAnimation>with TickerProviderStateMixin { PhaseState _phaseState;

When the state is created the initState method is called and it is called only once, so we’ll use it to initialize our animations. But first, some animation concepts:

AnimationController – an object that generates a new value whenever the hardware on the device running on your app is ready to display a new frame. The AnimationController produces values between 0.0 to 1.0 during a given duration. We add the vsync property because it will make sure that if our object is not visible it will not consume resources.

– an object that generates a new value whenever the hardware on the device running on your app is ready to display a new frame. The AnimationController produces values between 0.0 to 1.0 during a given duration. We add the vsync property because it will make sure that if our object is not visible it will not consume resources. CurvedAnimation – it defines the animation progress as a non-linear curve, in our case we used an easeInOut curve which means that our animation will be slow in the start and in the end but speeds up in between. There are many different other types you can use to your needs.

– it defines the animation progress as a non-linear curve, in our case we used an easeInOut curve which means that our animation will be slow in the start and in the end but speeds up in between. There are many different other types you can use to your needs. Tween – with Tween we can set a range of values by defining lower and upper bounds to properties in our animation. In our case we wanted to change the opacity of the object, we want to start at 0.4 and finish at 1.0. We set our AnimationController in order to apply this behavior on it.

– with Tween we can set a range of values by defining lower and upper bounds to properties in our animation. In our case we wanted to change the opacity of the object, we want to start at 0.4 and finish at 1.0. We set our AnimationController in order to apply this behavior on it. AnimationListener – This will help us to respond to status changes in our animation. This method will be called whenever the value of our AnimationConroller changes. In our case, we want to distinguish between to scenarios: 1. The current status is completed, in this case, we want to run backward with reverse 2. The status is dismissed which means that the animation hasn't started yet or reached the beginning again so we want to set it motion with forward.

– This will help us to respond to status changes in our animation. This method will be called whenever the value of our AnimationConroller changes. In our case, we want to distinguish between to scenarios: 1. The current status is completed, in this case, we want to run backward with reverse 2. The status is dismissed which means that the animation hasn't started yet or reached the beginning again so we want to set it motion with forward. Dispose Animation – it is very important to clean up the animation resources in the dispose method. We will call dispose on every AnimationController we defined here.

Now that we understand the animation concepts we can use them in our app. We have two Animation Controllers, one for “fade in\fade out” repeating behavior and the other for the “move” behavior:

class PhaseAnimationState extends State < PhaseAnimation > with TickerProviderStateMixin { PhaseState _phaseState; AnimationController _blinkController; Animation< double > _blinkAnimation; Animation< double > _opacityAnimation; AnimationController _moveController; Animation<Offset> _phasePosition; void initState () { super .initState(); _phaseState = PhaseState.Processing; _blinkController = new AnimationController( duration: const Duration (milliseconds: 500 ) , vsync: this ) ; _blinkAnimation = CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut); _opacityAnimation = Tween< double >(begin: 0.4 , end: 1 ).animate(_blinkController); _moveController = new AnimationController( duration: const Duration (milliseconds: 1000 ) , vsync: this ) ; _blinkAnimation.addStatusListener((status) { if (status == AnimationStatus.completed) { _blinkController.reverse(); } else if (status == AnimationStatus.dismissed) { _blinkController.forward(); } }); } void dispose () { _blinkController.dispose(); _moveController.dispose(); super .dispose(); }

Before implementing the build method let's implement the run, stop and move functionality:

void run () { _b linkController.forward(); } void stop () { _b linkController.stop(); } void move ( double dx) { setState(() { _ phaseState = PhaseState.Moving; _ phasePosition = Tween<Offset>(begin: Offset.zero, end: Offset(dx, 0.0 )) .animate( _ moveController); _ moveController.forward(); }); }

Each phase object will call these methods when it wants to start the animation, stop the animation and finally move to it's finale position.

We can now implement the build method:

@ override Widget build ( BuildContext context ) { var fadeTransition = new FadeTransition( opacity: _opacityAnimation, child: new Container( margin: const EdgeInsets.only(left: 9.0 ), width: 17.0 , height: 17.0 , decoration: new BoxDecoration( shape: BoxShape.circle, gradient: new LinearGradient( begin: FractionalOffset.center, end: FractionalOffset.topCenter, colors: [widget.dominantColor, Colors.white], ), ), )); if (_phaseState == PhaseState.Processing) { return fadeTransition; } else { return new SlideTransition( position: _phasePosition, child: fadeTransition); } }

Here we need to distinguish when the current phase is "processing" and when it is supposed to move. While processing we return a FadeTransition widget, it helps us to perform the fade animation. It uses the Tween Opacity Animation we defined earlier in order to bound the values range. After that, we just need to define the phase shape as a circle with whatever size and color we want. Remember you can customize it however you want :)

When the phase needs to move we return a SlideTransition, its child container remains the same FadeTransition as before because we still want to display the same UI element but now we want to move it as well.

Create done animation behavior

Here everything will be much simpler after what we have learned, we just need to implement an animation with a listener that will change the size of the image from start to end:

class DoneAnimation extends StatefulWidget { State < StatefulWidget > createState() => DoneAnimationState (); } class DoneAnimationState extends State<DoneAnimation>with SingleTickerProviderStateMixin { AnimationController _controller; Animation <double> _animation; double imageSize = 0.0 ; final double imageFinalSize = 30.0 ; void initState() { super .initState(); _controller = AnimationController ( duration: const Duration (milliseconds: 2000 ), vsync: this ); _animation = CurvedAnimation (parent: _controller, curve: Curves .easeInOut); _animation.addListener(() { setState(() { imageSize = imageFinalSize * _animation.value; }); }); _controller.forward(); } void dispose() { _controller.dispose(); super .dispose(); } Widget build( BuildContext context) { return new Container ( margin: const EdgeInsets .only(left: 60.0 ), child: new Image .asset( 'assets /images/done.png', width: imageSize, height: imageSize, )); } }

Manage the whole animations on our home page

First, we create a StatefulWidget because obviously the state of our button is changing during its lifetime. As we already know we have to override the createState method and create a state for our ProgressButtonState:

class Home extends StatefulWidget { State < StatefulWidget > createState() { return new ProgressButtonState (); } }

Now we can create the ProgressButtonState, this class will create our UI and here we will manage and control every animation we talked about so far. We set the initial button text and create 3 PhaseAnimation objects once in the initState method:

class ProgressButtonState extends State<Home> { String buttonText = "START" ; PhaseAnimation phaseOne; PhaseAnimation phaseTwo; PhaseAnimation phaseThree; bool showCheckIcon = false ; void initState() { super .initState(); phaseOne = new PhaseAnimation ( Colors .red); phaseTwo = new PhaseAnimation ( Colors .yellow); phaseThree = new PhaseAnimation ( Colors .green); }

Lets divide the build method into two parts:

The button itself – We can notice the I'm wrapping the phases and the "done" animation in a Visibilty widget, this is because we want to see the phases at the beginning and once the proccesing has finished we want them to diappear and then show the "done" animation:

@ override Widget build ( BuildContext context ) { return new Scaffold( appBar: new AppBar( title: new Text( "Progress Button Animation App" ), ), body: new Center( child: new ProgressButton( child: new Row(children: <Widget>[ new Container( margin: const EdgeInsets.only(left: 30.0 , right: 85 ), width: 100.0 , child: Text( buttonText, style: TextStyle( color: Colors.white, fontSize: 15 , fontWeight: FontWeight.normal), ), ), Visibility( visible: !showCheckIcon, child: new Row( children: <Widget>[ phaseOne, phaseTwo, phaseThree, ], ), ), Visibility(visible: showCheckIcon, child: DoneAnimation()) ]),

Button Click – when the button click event fires up we set our animation in motion. I'm simulating a processing procedure with FutureDelay of 2500 milliseconds for each phase, additionally I'm changing the button text with the current phase name and stopping the animation once the time ran out. After all the phases animation has finished we can start the moving animation and finally show the "done" animation:

onPressed: () async { phaseOne.run(); setState(() { buttonText = "PHASE 1" ; }); await Future.delayed( const Duration ( milliseconds: 2500 )) ; phaseTwo.run(); setState(() { buttonText = "PHASE 2" ; }); phaseOne.stop(); await Future.delayed( const Duration ( milliseconds: 2500 )) ; phaseThree.run(); setState(() { buttonText = "PHASE 3" ; }); phaseTwo.stop(); await Future.delayed( const Duration ( milliseconds: 2500 )) ; phaseThree.stop(); phaseOne.move( 2.0 ); phaseTwo.move( 1.0 ); await Future.delayed( const Duration ( milliseconds: 1000 )) ; setState(() { showCheckIcon = true ; buttonText = "DONE!" ; }); }),

And we're done...

You can get the full source code here: https://github.com/AlonRom/Flutter-Progress-Button_Animation