• Create medium’s clap Animation in Flutter
  • By Kartik Sharma
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Hoarfroster
  • Proofreader: Greycodee, HumanBeingXenon

In this article, we will explore Flutter animation from scratch. We will learn some of the core concepts of animation by mimicking Medium’s clap animation in Flutter.

As the title says, this article will focus more on animation than on the basics of Flutter.

An introduction to

We will start with a new Flutter project. Whenever we create a Flutter project, we will see this code:

// main.dart

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page')); }}class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',),new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: newIcon(Icons.add), ), ); }}Copy the code

Flutter provides us with a free lunch of code to get started with. It has managed the status of click counts and created a floating action button for us.

Here is the end result we want to achieve:

/ / wait for the upload / / miro.medium.com/max/1600/1 *…

We’re going to create the animation. Author: Thuy Gia Nguyen

Before adding animations, let’s take a quick look and solve some simple problems.

  1. Change the button icon and background.
  2. When we hold the button down, the button should continue adding counts.

Let’s add these 2 quick fixes and start animating:

// main.dart

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  final duration = new Duration(milliseconds: 300);
  Timer timer;


  initState() {
    super.initState();
  }

  dispose() {
   super.dispose();
  }

  void increment(Timer t) {
    setState(() {
      _counter++;
    });
  }

  void onTapDown(TapDownDetails tap) {
    // User pressed the button. This can be a tap or a hold.
    increment(null); // Take care of tap
    timer = new Timer.periodic(duration, increment); // Takes care of hold
  }

  void onTapUp(TapUpDetails tap) {
    // User removed his finger from button.
    timer.cancel();
  }

  Widget getScoreButton() {

    return new Positioned(
        child: new Opacity(opacity: 1.0, child: new Container(
            height: 50.0 ,
            width: 50.0 ,
            decoration: new ShapeDecoration(
              shape: new CircleBorder(
                  side: BorderSide.none
              ),
              color: Colors.pink,
            ),
            child: new Center(child:
            new Text("+" + _counter.toString(),
              style: new TextStyle(color: Colors.white,
                  fontWeight: FontWeight.bold,
                  fontSize: 15.0),))
        )),
        bottom: 100.0
    );
  }

  Widget getClapButton() {
    // Using custom gesture detector because we want to keep increasing the claps
    // when user holds the button.
    return new GestureDetector(
        onTapUp: onTapUp,
        onTapDown: onTapDown,
        child: new Container(
          height: 60.0 ,
          width: 60.0 ,
          padding: new EdgeInsets.all(10.0),
          decoration: new BoxDecoration(
              border: new Border.all(color: Colors.pink, width: 1.0),
              borderRadius: new BorderRadius.circular(50.0),
              color: Colors.white,
              boxShadow: [
                new BoxShadow(color: Colors.pink, blurRadius: 8.0)
              ]
          ),
          child: new ImageIcon(
              new AssetImage("images/clap.png"), color: Colors.pink,
              size: 40.0))); }@override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',),new Text(
              '$_counter',
              style: Theme
                  .of(context)
                  .textTheme
                  .display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new Padding(
          padding: new EdgeInsets.only(right: 20.0),
          child: newStack( alignment: FractionalOffset.center, overflow: Overflow.visible, children: <Widget>[ getScoreButton(), getClapButton(), ], ) ), ); }}Copy the code

In terms of the final product, we need to add 3 things.

  1. Change the size of the Widget
  2. Display the Widget that shows the number of claps when the button is pressed, and hide the Widget when the button is released
  3. Add those little widgets that scatter flowers around and animate them

Let’s take it one at a time and slowly advance our learning progress. Before we begin, we need to learn some basic information about animation in Flutter.

Learn about the basic animation widgets in Flutter

An animation is nothing more than a number of values that change over time. For example, when we click a button, we want the Widget that shows the number of claps to move up from the bottom. By the time the button is released it should have moved up quite a bit, so we should hide it.

Focusing on the Widget that shows the number of claps, we need to change its position and opacity over a period of time.

// The Widget that displays the number of claps

new Positioned(
  child: new Opacity(opacity: 1.0, 
    child: new Container(
      / /...
    )),
  bottom: 100.0
);
Copy the code

For example, we want the Widget that displays the number of claps to fade in from the bottom up after 150 milliseconds. Let’s consider the timeline, as follows:

This is a simple two-dimensional image, with positions changing over time.

Notice that this is an oblique line, but it could actually be a curve if you like.

You can let the position increase slowly over time, and then faster and faster. Or you can let it come in at super high speed and then slow down at the end.

This is our first Widget AnimationController.

The AnimationController constructor looks like this:

scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);
Copy the code

Here, we have created a simple controller for the animation and specified a duration of 150ms for the animation to run. But what is that Vsync?

Mobile devices refresh the screen every few milliseconds. This is how we perceive a set of images as a continuous stream or a movie.

The screen refresh rate can vary from device to device. For example, if the phone refreshes the screen 60 times per second (60 frames per second), that’s a new interface drawn every 16.67 milliseconds. Sometimes the image might be misaligned (we sent a different image when the screen refreshed) and we would see the screen rip. Vsync solves this problem.

Let’s add a listener to the controller and run the animation:

scoreInAnimationController.addListener(() {
  print(scoreInAnimationController.value);
});
scoreInAnimationController.forward(from: 0.0);

/* OUTPUT I/flutter (1913): 0.0i /flutter (1913): 0.0I /flutter (1913): 0.22297333333333333 I/flutter (1913): 0.222973333333333 0.334453333333333333 I/flutter (1913): 0.44593333333334 I/flutter (1913): 0.5574133333333334 I/flutter (1913): I/flutter (1913): 0.7803666666666668 I/flutter (1913): 0.89184666666668 I/flutter (1913): 1.0 */
Copy the code

The controller generated the numbers from 0.0 to 1.0 in 150 milliseconds — note that the generated values are almost linear (0.2, 0.3, 0.4…). . How can we change this behavior? This is done by the second Widget, CurvedAnimation:

bounceInAnimation = new CurvedAnimation(parent: scoreInAnimationController, curve: Curves.bounceIn);
bounceInAnimation.addListener(() {
  print(bounceInAnimation.value);
});

/* OUTPUT I/flutter (5221): 0 I/flutter (5221): 0 I/flutter (5221): 0 I/flutter (5221): 0 I/flutter (5221): 0 0.16975716286388898 I/flutter (5221): 0.17177866222222238 I/flutter (5221): 0.6359024059750003 I/ FLUTTER (5221): 1.0 */
Copy the code

We create a curve animation by setting parent as our controller and providing the curve we want to follow. On the Flutter Curves class reference page you can see a list of Curves that you can use. The controller provides values from 0.0 to 1.0 to the curve animation Widget in 150 milliseconds, and the curve animation Widget interpolates these values according to the curve we set.

We now have values from 0.0 to 1.0, and we want our Widget showing the likes to have animated values in the range of [0.0, 100.0]. We can simply multiply the value of the previous step by 100 to get the result. Or we could use a third widget, the Tween class.

tweenAnimation = new Tween(begin: 0.0, end: 100.0).animate(scoreInAnimationController);
tweenAnimation.addListener(() {
  print(tweenAnimation.value);
});

/* Output I/flutter (2639): 0.0i /flutter (2639): 0.0I /flutter (2639): 0.0I /flutter (2639): 33.452000000000005 I/flutter (2639): 33.452000000000005 44.602000000000004 I/flutter (2639): 55.7513333333333334 I/flutter (2639): 66.90133333333334 78.05133333333333 I/ FLUTTER (2639): 89.200666666668 I/flutter (2639): 100.0 */
Copy the code

The Tween class generates values from begin to end. We use the front scoreInAnimationController, it USES a linear curve. (Of course we can also use our rebound curve to get different values). The advantage of Tween doesn’t stop there — you can Tween other things, too. You can directly Tween colors, offsets, positions, and other Widget properties that inherit from the Tween base class.

Animation of the Widget’s position showing the number of claps

At this point, we’ve learned enough to have our Widget that shows how many times we clap pop up from the bottom when we press the button and hide when we release the button.

initState() {
  super.initState();
  scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);
  scoreInAnimationController.addListener((){
    setState(() {}); // Call the render function.
  });
}

void onTapDown(TapDownDetails tap) {
  scoreInAnimationController.forward(from: 0.0); . } Widget getScoreButton() {var scorePosition = scoreInAnimationController.value * 100;
  var scoreOpacity = scoreInAnimationController.value;
  return new Positioned(
    child: new Opacity(opacity: scoreOpacity, 
      child: new Container(/ *... * /)
    ),
    bottom: scorePosition
  );
}
Copy the code

Click on the Widget that displays the number of claps, but there’s one problem:

When we click the button multiple times, the Widget that shows how many times we clap pops up constantly. This is because of a small error in the code above. We tell the controller to go forward from zero every time the button is clicked.

Now, let’s add an output animation for the Widget that shows the number of claps.

First, we add an enumeration to make it easier to manage the state of the Widget that displays the number of claps.

enum ScoreWidgetStatus {
  HIDDEN,
  BECOMING_VISIBLE,
  BECOMING_INVISIBLE
}
Copy the code

We then create an animation controller that non-linear animates the Widget’s position values in the range [100, 150]. We also added a status listener to the animation, and once the animation is over, we set the status of the Widget showing the number of claps to hidden.

scoreOutAnimationController = new AnimationController(vsync: this, duration: duration);
scoreOutPositionAnimation = new Tween(begin: 100.0, end: 150.0).animate(
  new CurvedAnimation(parent: scoreOutAnimationController, curve: Curves.easeOut)
);
scoreOutPositionAnimation.addListener((){
  setState(() {});
});
scoreOutAnimationController.addStatusListener((status) {
  if(status == AnimationStatus.completed) { _scoreWidgetStatus = ScoreWidgetStatus.HIDDEN; }});Copy the code

When the user removes their finger from the Widget, we set the appropriate state and start a timer for 300 milliseconds. After 300 milliseconds, we will animate the Widget’s position and opacity:

void onTapUp(TapUpDetails tap) {
  // The user removes his finger
  scoreOutETA = new Timer(duration, () {
    scoreOutAnimationController.forward(from: 0.0);
    _scoreWidgetStatus = ScoreWidgetStatus.BECOMING_INVISIBLE;
  });
  holdTimer.cancel();
}
Copy the code

When the user clicks a finger on the Widget, we set the appropriate state and start a 300ms timer:

void onTapDown(TapDownDetails tap) {
  // The user pressed the button -- either long or tap
  if(scoreOutETA ! =null) scoreOutETA.cancel(); // We don't want times to disappear!
  if (_scoreWidgetStatus == ScoreWidgetStatus.HIDDEN) {
    scoreInAnimationController.forward(from: 0.0);
    _scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;
  }
  increment(null); // Focus press
  holdTimer = new Timer.periodic(duration, increment); // Focus on the long press
}
Copy the code

We also modified the TapDown event to handle some special cases. Finally, we need to choose which controller values we want to use to handle the position and opacity of our Widget that displays the number of claps. A simple switch can do the job:

Widget getScoreButton() {
  var scorePosition = 0.0;
  var scoreOpacity = 0.0;
  switch(_scoreWidgetStatus) {
    case ScoreWidgetStatus.HIDDEN:
      break;
    case ScoreWidgetStatus.BECOMING_VISIBLE:
      scorePosition = scoreInAnimationController.value * 100;
      scoreOpacity = scoreInAnimationController.value;
      break;
    case ScoreWidgetStatus.BECOMING_INVISIBLE:
      scorePosition = scoreOutPositionAnimation.value;
      scoreOpacity = 1.0 - scoreOutAnimationController.value;
  }
  return... }Copy the code

Current output:

Finally, we need to choose which controller’s value we want to use to set the position and opacity of the Widget showing the number of claps — it should pop + fade out.

Widget-size animations showing the number of claps

At this point, we also know how to change the magnitude as the degree increases. Let’s quickly add the size animation, and then let’s move on to the scatter animation.

I updated the ScoreWidgetStatus enumeration to hold an additional VISIBLE value. Now let’s add a new controller for the size property.

scoreSizeAnimationController = new AnimationController(vsync: this, duration: new Duration(milliseconds: 150));
scoreSizeAnimationController.addStatusListener((status) {
  if(status == AnimationStatus.completed) { scoreSizeAnimationController.reverse(); }}); scoreSizeAnimationController.addListener((){ setState(() {}); });Copy the code

The controller produces a value from 0 to 1 in 150 milliseconds, and once that’s done, we produce a value from 1 to 0, so we have nice zooming in and out.

We’ve also updated our increment function to start animating when the number increases.

void increment(Timer t) {
  scoreSizeAnimationController.forward(from: 0.0);
  setState(() {
    _counter++;
  });
}
Copy the code

We need to deal with the case of enumerated Visible properties. To do this, we need to add some judgments to the TouchDown event:

void onTapDown(TapDownDetails tap) {
  // The user pressed the button -- either long or tap
  if(scoreOutETA ! =null) scoreOutETA.cancel(); // We don't want times to disappear!
  if(_scoreWidgetStatus == ScoreWidgetStatus.BECOMING_INVISIBLE) {
    // Click the button while the Widget is flying up to pause it!
    scoreOutAnimationController.stop(canceled: true);
    _scoreWidgetStatus = ScoreWidgetStatus.VISIBLE;
  }
  else if (_scoreWidgetStatus == ScoreWidgetStatus.HIDDEN ) {
    _scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;
    scoreInAnimationController.forward(from: 0.0);
  }
  increment(null); // Focus press
  holdTimer = new Timer.periodic(duration, increment); // Focus on the long press
}
Copy the code

Finally, we use the value of the controller in the Widget.

extraSize = scoreSizeAnimationController.value * 10; . height:50.0 + extraSize,
width: 50.0  + extraSize,
...
Copy the code

The full code can be found at GitHub Gist. Here we run both size and position animations. The sizing animations need to be tweaked a little bit, but we’ll talk about that at the end.

Then take the animation

Before we do the scatter animation, we need to make some adjustments to the sizing animation. For now, the button magnifies too much. The solution is simple: change the Extrasize coefficient from 10 to a smaller number.

Now come to the flower sprinkling animation. We can observe that the scattered flowers are just images of five changing positions.

I made a triangle and a circle image in Microsoft Paint and saved them to Flutter resources. Now we can use this Image as our Image Asset material.

Before we start animating, let’s think about positioning and some of the tasks we need to accomplish.

  1. We need to position 5 images, each showing a different Angle, to form a complete circle.
  2. We need to rotate the image according to the Angle.
  3. We need to increase the radius of the circle over time.
  4. We need to find coordinates based on angles and radii.

Simple trigonometry gives us formulas for x and y coordinates in terms of sines and cosines of angles.

var sparklesWidget =
  new Positioned(child: new Transform.rotate(
    angle: currentAngle - pi/2,
    child: new Opacity(opacity: sparklesOpacity,
      child : new Image.asset("images/sparkles.png", width: 14.0, height: 14.0, ))
    ),
    left:(sparkleRadius*cos(currentAngle)) + 20,
    top: (sparkleRadius* sin(currentAngle)) + 20,);Copy the code

Now we need to create five of these widgets, and each Widget should have a different Angle. A simple for loop should do the trick.

for(int i = 0; i <5; ++i) {
  var currentAngle = (firstAngle + ((2*pi)/5)*(i));
  var sparklesWidget = ...
  stackChildren.add(sparklesWidget);
}
Copy the code

All we need to do is split 2* PI (360 degrees) into 5 pieces and create a Widget accordingly. We then add these widgets to an array that acts as a stack child.

Now, at this point, most of the work is done. All we need to do is animate sparkleRadius and generate a new firstAngle as the score increases.

sparklesAnimationController = new AnimationController(vsync: this, duration: duration);
sparklesAnimation = new CurvedAnimation(parent: sparklesAnimationController, curve: Curves.easeIn);
sparklesAnimation.addListener((){
  setState(() { });
});

void increment(Timer t) {
  sparklesAnimationController.forward(from: 0.0); . setState(() { ... _sparklesAngle = random.nextDouble() * (2*pi);
});
     
Widget getScoreButton() {
  ...
  var firstAngle = _sparklesAngle;
  var sparkleRadius = (sparklesAnimationController.value * 50);var sparklesOpacity = (1- sparklesAnimation.value); . }Copy the code

This is our introduction to the basic animation of Flutter. We will continue to explore more about Flutter in the future in order to learn how to create a more advanced UI.

You can find the complete code in my Git repository.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.