Animation Flutter TikTok

How to Create TikTok Loading Animation in Flutter using the Flow Widget

TikTok Loading Animation in Flutter

Loading animations are very popular in mobile apps. They add elegance and quality to the user experience of the app. You can see them everywhere from Youtube to Reddit...etc.

And to be honest, designers and developers are very creative at creating ones that are awesome and sophisticated, and one of those for sure is TikTok loading animation, which consists of two colored balls spinning around each other, forming a 360° view, (look at the photo).

TikTok Loading Animation in Flutter

When I saw that animation, I had an idea to try creating it in Flutter. And the surprise was that I’ve succeeded. Actually, when I look at it now, I see it so easy to make. But, it took me some time (like two days) to figure out what I should do and how to start.

So in this Flutter example, I’ll show you how to create the TikTok loading animation using the Flow widget.

Is That Explicit or Implicit?


In Flutter there are two kinds of animations. Excplicit and implicit animations. The former is hard to implement comparing to the lattter. Because you need to use classes like AnimationController, AnimatedBuilder, Tween...etc. Unlike the implicit, which needs you to just set the beign and end values of the animation and it will take care of everything else. There are ready widgets for it, like AnimatedContainer.

Let's Begin...


Before anything, I'm gonna add some code to the Scaffold in main.dart.

@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Color(0xFF141921),
    appBar: AppBar(
      title: Text('TikTok Loading Animation'),
    ),
    body: Center(
      child: Container(
        width: 52,
        height: 30,
        alignment: Alignment.center,
        child: TikTokLoadingAnimation(),
      ),
    ),
  );
}

So, in our TikTokLoadingAnimation widget, I'm gonna focus on animating one ball at time (the Aqua) and then the second one (the Pink).

Animating the First Ball


First, We need to create an AnimationController. Hence its name, it’s gonna be the controller for all other animations (as we will see). Don’t forget to initialize it in the initState() method and to get rid of it in the dispose(). If you don’t do the last step, you can cause a leak in your app.

AnimationController _animationController;

@override
void initState() {
  super.initState();

  _animationController = AnimationController(
    vsync: this,
    duration: Duration(milliseconds: 2000),
  );
}

@override
void dispose() {
  _animationController.dispose();

  super.dispose();
}
    

Did you notice that we've set the duration to be 2000 milliseconds. It's gonna be so slow right now, but in the end we will make it faster.

There is a cool feature in Android Studio to slow motion the animations in your app. To use it, open a panel at the left side of the screen called 'Flutter Inspector' and then click the Yellow clock arrow (as you can see in the pic).

Slow-motion Animations in Android Studio

Now, we need to create three animations for the ball. One to make it moves to the left, the second to make it grow a little bit from its normal size, and the last one is to reduce its size back to the normal. look at the photo.

The Aqua ball animation in TikTok loading animation

The First Animation (Forward)


Let's create the first animation, which is gonna move it to the left.

Animation<double> _animationTranslateForward;
                  
@override
void initState() {
  super.initState();
      
  _animationTranslateForward = Tween(begin: 0.0, end: 25.0).animate(
    CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    ),
  );
}

After we've created the first animation, let's actually create the ball. And here we are gonna use the Flow widget.

What's the Flow Widget?


It's the same as the Stack or the Column widgets, but the difference is that it allows you to define the rules of laying out the children, through its delegate. Like in the Stack, the first child in its children list gets painted first and then the next one will be on top of it. But with the Flow widget, you can define an order of your choice for the children, the position (X and Y) for each child, the rotation, the size...etc. You can do all that using its delegate.

The Delegate of the Flow Widget


The delegate of the Flow widget is called FlowDelegate. But, you need to create your own delegate extending that one. Because in that subclass you will define your own painting rules for the children.

class TikTokLoadingAnimationDelegate extends FlowDelegate {

You have to override two methods. One where all the shit happen and it's called paintChildren(). Hence its name, it's for painting the children, and the other is for notifying the Flow about repainting the children (whether to call the first method again).

@override
void paintChildren(FlowPaintingContext context) {
           
}
      
@override
bool shouldRepaint(TikTokLoadingAnimationDelegate oldDelegate) {
          
}

Leave these methods empty for now and we will get to them later. I wanna focus on injecting the previous animation and its controller to the delegate, but why? Because they are gonna tell the Flow widget how to paint the ball, through the paintChildren() method.

In the constructor, I'm gonna play the animation and make it repeat itself. Also, Don't look at the super call now...da.

class TikTokLoadingAnimationDelegate extends FlowDelegate {
  final AnimationController animationController;

  final Animation<double> animationTranslateForward;
  
  TikTokLoadingAnimationDelegate({
    this.animationController,
    this.animationTranslateForward,
  }) : super(repaint: animationController) {
  
    animationController.repeat();
  }

Before writing the code for paintChildren(), I wanna create the Flow widget and the ball in our build() method. Notice how I'm gonna instantiate the delegate and inject the animations to it.

@override
Widget build(BuildContext context) {
  return Flow(
    delegate: TikTokLoadingAnimationDelegate(
      animationController: _animationController,
      animationTranslateForward: _animationTranslateForward,
    ),
    children: [
      //The first ball (Aqua).
      Container(
        width: 25,
        height: 25,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Color(0xFF37ffec),
        ),
      ),
    ]
}

After creating the ball, I want you to get back to the methods of the delegate, that we left them alone.

The paintChildren() Method


In paintChildren(), if you've noticed, it gives us a parameter called FlowPaintingContext (not BuildContext the usual one). On that class you will call a method called paintChild() that takes three parameters:
  • The index of the child. When you are providing the children for the Flow using a list of widgets (just like the Stack), the index of the first child will be zero, the next one will be one and so on. So, in our case, it will be zero.
  • The transform for that child. Here you will define the rules of painting that child, you can rotate it, scale it...whatever you want. In our case, I'm gonna define the ball position on the screen using the animationTranslateForward value.
  • The opacity of that child, but we are never gonna use that one.

@override
void paintChildren(FlowPaintingContext context) {
  context.paintChild(
    0,
    transform: Matrix4.identity()
      ..translate(animationTranslateBackward.value),
  );
}

The shouldRapaint() Method


The next method shouldRepaint() as I said is gonna notifiy the Flow widget about repainting the children, whether to call the first method again based on its returing bool value. But, we are never gonna use it, because we've already gave that mission to the animationController. If you recall in the constructor of the delegate > in the super call > in the repaint property, I've specified our beloved animationController.

}) : super(repaint: animationController) {

So, it's gonna notifiy the Flow about repainting the children every frame, as its animation value changes. I mean paintChildren() will be called on every frame as the ball will be painted in a new position at every call, because the animationTranslateForward value will change.

So, you can return true of false, whatever dude.

@override
bool shouldRepaint(TikTokLoadingAnimationDelegate oldDelegate) {
  return false;
}

Did you see everything we have talked about, here its code in one neat snippet.

class _TikTokLoadingAnimationState extends State<TikTokLoadingAnimation> with SingleTickerProviderStateMixin {
  AnimationController _animationController;

  Animation<double> _animationTranslateForward;
  
  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this,
    );
    
    _animationTranslateForward = Tween(begin: 0.0, end: 25.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeInOut,
      ),
    );
  }
  
  @override
  void dispose() {
    _animationController.dispose();
   
    super.dispose();
  }
  
  Widget build(BuildContext context) {
  return Flow(
    delegate: TikTokLoadingAnimationDelegate(
      animationController: _animationController,
      animationTranslateForward: _animationTranslateForward,
    ),
    children: [
      //The first ball (Aqua).
      Container(
        width: 25,
        height: 25,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Color(0xFF37ffec),
        ),
      ),
    ],
  }
}

class TikTokLoadingAnimationDelegate extends FlowDelegate {
  final AnimationController animationController;

  final Animation<double> animationTranslateForward;
  
  TikTokLoadingAnimationDelegate({
    this.animationController,
    this.animationTranslateForward,
  }) : super(repaint: animationController) {
  
    animationController.repeat();
  }

  @override
  void paintChildren(FlowPaintingContext context) {

    context.paintChild(
      0,
      transform: Matrix4.identity()
        ..translate(animationTranslateForward.value),
    );
  }

  @override
  bool shouldRepaint(TikTokLoadingAnimationDelegate oldDelegate) {
    return false;
  }
}

Now, if you run that shit, you will get that result

The Aqua ball in TikTok loading animation

The Second and the Third Animations (Grow and Reduce)


We are gonna repeat the scenario of the first animation with the second and third ones. So I'm gonna write some code like a quiet little boy, without saying anyword. But before, I want you to notice that the second animation is gonna work the first half of the time and the thrid one the second half.

Animation<double> _animationGrowForward;
Animation<double> _animationReduceForward;

@override
void initState() {
  super.initState();
  
  //That one works the first half of the time.
  _animationGrowForward = Tween(begin: 1.0, end: 1.05).animate(
    CurvedAnimation(
      parent: _animationController,
      curve: Interval(0.0, 0.50, curve: Curves.easeInOut),
    ),
  );

  //And that one the second half.
  _animationReduceForward = Tween(begin: 1.05, end: 1.0).animate(
    CurvedAnimation(
      parent: _animationController,
      curve: Interval(0.50, 1.0, curve: Curves.easeInOut),
    ),
  );
}

@override
Widget build(BuildContext context) {
  return Flow(
    delegate: TikTokLoadingAnimationDelegate(
      ...
      animationTranslateForward: _animationTranslateForward,
      animationGrowForward: _animationGrowForward,
    ),
    ...
}

class TikTokLoadingAnimationDelegate extends FlowDelegate {
  ...
  final Animation<double> animationGrowForward;
  final Animation<double> animationReduceForward;
  
  TikTokLoadingAnimationDelegate({
    ...
    this.animationTranslateForward,
    this.animationGrowForward,
    ...
    
  @override
  void paintChildren(FlowPaintingContext context) {
    context.paintChild(
      0,
      transform: Matrix4.identity()
        ..translate(animationTranslateForward.value)
        ..scale(animationGrowForward.value)
        ..scale(animationReduceForward.value),
    );
  }

After combining the three animations together, you will get that result. Pay attention to size of the ball how it's changing.

The Aqua ball in TikTok loading animation

Animating the Second Ball


Nothing new here, We are just gonna flip everthing. Look, the next photo, which will explain better than me.

The Pink ball in TikTok loading animation

Here's the full code for the second ball:

Animation<double> _animationTranslateBackward;
Animation<double> _animationGrowBackward;
Animation<double> _animationReduceBackward;

@override
void initState() {
  super.initState();
  
  _animationTranslateBackward = Tween(begin: 25.0, end: 0.0).animate(
    CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    ),
  );

  //That one works the first half of the time.
  _animationGrowBackward = Tween(begin: 1.0, end: 0.95).animate(
    CurvedAnimation(
      parent: _animationController,
      curve: Interval(0.0, 0.50, curve: Curves.easeInOut),
    ),
  );

  //And that one the second half.
  _animationReduceBackward = Tween(begin: 0.95, end: 1.0).animate(
    CurvedAnimation(
      parent: _animationController,
      curve: Interval(0.50, 1.0, curve: Curves.easeInOut),
    ),
  );
}

@override
Widget build(BuildContext context) {
  return Flow(
    delegate: TikTokLoadingAnimationDelegate(
      ...
      animationTranslateBackward: _animationTranslateBackward,
      animationGrowBackward: _animationGrowBackward,
      animationReduceBackward: _animationReduceBackward,
    ),
    children: [
      //The second ball.
      Container(
        width: 25,
        height: 25,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Color(0xFFf21458),
        ),
      ),
    ],
  );
}

class TikTokLoadingAnimationDelegate extends FlowDelegate {
  ...
  final Animation<double> animationTranslateBackward;
  final Animation<double> animationGrowBackward;
  final Animation<double> animationReduceBackward;

  TikTokLoadingAnimationDelegate({
    ...
    this.animationTranslateBackward,
    this.animationGrowBackward,
    this.animationReduceBackward,
  }) : super(repaint: animationController) {
    
    animationController.repeat();
  }

  @override
  void paintChildren(FlowPaintingContext context) {
    ...
    
    //Painting the second ball.
    context.paintChild(
      1,
      transform: Matrix4.identity()
        ..translate(animationTranslateBackward.value)
        ..scale(animationGrowBackward.value)
        ..scale(animationReduceBackward.value),
    );
  }
  ...
}

When we combine the code of the second ball with the first one (with animations obviously), we will get that:

TikTok Loading Animation in Reverse

There's one thing wrong with this, the backward ball (the Pink) is on the front, it should be on the back. Can you fix it? I'll give you a hint: In paintChildren() method, the last child gets painted, it will be on the front (regardless of its index). You're right, we need to flip the painting in the method and change the indices. Here's before and after code snippets.

Before

@override
void paintChildren(FlowPaintingContext context) {
  
  context.paintChild(
    0,
    transform: Matrix4.identity()
      ..translate(animationTranslateForward.value)
      ..scale(animationGrowForward.value)
      ..scale(animationReduceForward.value),
  );
    
  context.paintChild(
    1,
    transform: Matrix4.identity()
      ..translate(animationTranslateBackward.value)
      ..scale(animationGrowBackward.value)
      ..scale(animationReduceBackward.value),
  );
}

After

@override
void paintChildren(FlowPaintingContext context) {
    
  context.paintChild(
    0,
    transform: Matrix4.identity()
      ..translate(animationTranslateBackward.value)
      ..scale(animationGrowBackward.value)
      ..scale(animationReduceBackward.value),
  );

  context.paintChild(
    1,
    transform: Matrix4.identity()
      ..translate(animationTranslateForward.value)
      ..scale(animationGrowForward.value)
      ..scale(animationReduceForward.value),
  );
}

Please, press that hot restart button (Shift+F10 in AS) for me:

TikTok Loading animation

So far so good, but certainly that's unwanted, we want the balls to continue spinning around each other, forming a 360° view. And the secret to do that will be in reversing them. So, when the animation completes its round (before repeating), we are gonna reverse the positions of the balls, and the user will see them as they are spinning around each other. Does that sound gibberish to you? This illustrator photo will help.

TikTok loading animation illustrator

We need to add a listener to the end of the animation, so we can reverse the indices, _reverseChildren() method will do that. Also, notice how I'm gonna make the animation plays once and then from the listener I'm gonna play again (no more repeating as before).

TikTokLoadingAnimationDelegate({
  ...
}) : super(repaint: animationController) {
  
  animationController.forward();

  animationController.addStatusListener((status) {
    if (status == AnimationStatus.completed) {
      
      animationController.value = 0.0;
      animationController.animateTo(1.0);

      _reverseChildren();
    }
  });
}

Now, to reverse the children, we are gonna define two variables as the indices of the balls and keep swaping their values after each round of the animation.

int firstBallIndex = 0;
int secondBallIndex = 1;

_reverseChildren() {
  var tmp = firstBallIndex;
  firstBallIndex = secondBallIndex;
  secondBallIndex = tmp;
}

@override
void paintChildren(FlowPaintingContext context) {
  
  //Painting the first child.
  context.paintChild(
    firstBallIndex,
    ...
  );

  //Painting the second child.
  context.paintChild(
    secondBallIndex,
    transform: Matrix4.identity()
    ...
  );
}

let's run it.

TikTok loading animation

The Inner Ball


If you look at the original animation, you will see that there's an inner dark ball in the forward ball. It's like the shadow of the backward ball on the forward one. The important question is: How are we gonna do that in Flutter?

The answer is that: We are gonna add an inner dark ball to each of our two balls (the Aqua and the Pink). But, just the forward one will show the dark ball. And the mechanism for that is gonna be by using Stream and StreamBuilder.

Also, the inner dark ball should be moving inside the forward ball, we can't leave it static, and to do that, we will create an animation for it.

Animation<double> _translateInnerBall;

StreamController<bool> _innerBallStreamController;

Sink<bool> get _innerBallSink => _innerBallStreamController.sink;

Stream<bool> get _innerBallStream => _innerBallStreamController.stream;

@override
void initState() {
  super.initState();
  
  ...
  
  _translateInnerBall = Tween(begin: 20.0, end: -40.0).animate(
    CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    ),
  );

  _innerBallStreamController = StreamController.broadcast();
}

@override
void dispose() {
  ...
  _innerBallStreamController.close();

  super.dispose();
}

Now, let's create the inner ball in our build() method. Notice the use of ClipRRect here, it's very important, because it allows the inner ball to be inside the forward one, not on it. After that we will inject the sink of the StreamController to the Flow delegate and add the inner ball to each one of our balls.

@override
Widget build(BuildContext context) {
  //Creating the inner ball.
  var _innerBall = ClipRRect(
    borderRadius: BorderRadius.circular(32),
    child: Padding(
      padding: const EdgeInsets.all(4.0),
      child: AnimatedBuilder(
        animation: _translateInnerBall,
        builder: (context, child) {
          return Transform.translate(
            offset: Offset(_translateInnerBall.value, 0.0),
            child: child,
          );
        },
        child: Container(
          width: 8,
          height: 8,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: Color(0xFF252525),
          ),
        ),
      ),
    ),
  );
  
  return Flow(
    delegate: TikTokLoadingAnimationDelegate(
      ...
      innerBallSink: _innerBallSink,
    ),
    children: [
      //The first ball.
      Container(
        ...
        child: StreamBuilder(
          initialData: true,
          stream: _innerBallStream,
          builder: (context, snapshot) {
            return Visibility(
              visible: snapshot.data,
              child: _innerBall,
            );
          },
        ),
      ),
      //The second ball.
      Container(
        ...
        child: StreamBuilder(
          initialData: true,
          stream: _innerBallStream,
          builder: (context, snapshot) {
            return Visibility(
              visible: !snapshot.data,
              child: _innerBall,
            );
          },
        ),
      ),
    ],
  );
  
}

In the delegate, since we are listening when the animation ends, we will hide the inner ball on the backward one and show it on the forward one.

class TikTokLoadingAnimationDelegate extends FlowDelegate {
  ...

  final Sink<bool> innerBallSink;

  bool showInnerBallOnForwardBall = true;

  TikTokLoadingAnimationDelegate({
    ...
    this.innerBallSink,
  }) : super(repaint: animationController) {
    ...

    animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        ...
        _showInnerBallOnForwardBall();
      }
    });
  }

  _showInnerBallOnForwardBall() {
    innerBallSink.add(showInnerBallOnForwardBall);
    showInnerBallOnForwardBall = !showInnerBallOnForwardBall;
  }
}

The last thing we will do is making the animation faster, by reducing its duration from 2000 milliseconds to 375 in the AnimationController initialization in initState().

The final result of the animation:

TikTok Loading Animation in Flutter

The Full Code to Everything


import 'dart:async';

import 'package:flutter/material.dart';

class TikTokLoadingAnimation extends StatefulWidget {
  @override
  _TikTokLoadingAnimationState createState() => _TikTokLoadingAnimationState();
}

class _TikTokLoadingAnimationState extends State<TikTokLoadingAnimation> with SingleTickerProviderStateMixin {
  AnimationController _animationController;

  //That animation makes the ball moves to the left.
  Animation<double> _animationTranslateForward;

  //This one makes the ball grow a little bit from its normal size.
  Animation<double> _animationGrowForward;

  //And this one makes the ball shrink back to its normal size.
  Animation<double> _animationReduceForward;

  //That animation makes the ball moves to the right.
  Animation<double> _animationTranslateBackward;

  //This one makes the ball grow a little bit from its normal size.
  Animation<double> _animationGrowBackward;

  //And this one makes the ball shrink back to its normal size.
  Animation<double> _animationReduceBackward;

  Animation<double> _translateInnerBall;

  StreamController<bool> _innerBallStreamController;

  Sink<bool> get _innerBallSink => _innerBallStreamController.sink;

  Stream<bool> get _innerBallStream => _innerBallStreamController.stream;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 375),
    );

    _animationTranslateForward = Tween(begin: 0.0, end: 25.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeInOut,
      ),
    );

    //That one works the first half of the time.
    _animationGrowForward = Tween(begin: 1.0, end: 1.05).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Interval(0.0, 0.50, curve: Curves.easeInOut),
      ),
    );

    //And that one the second half.
    _animationReduceForward = Tween(begin: 1.05, end: 1.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Interval(0.50, 1.0, curve: Curves.easeInOut),
      ),
    );

    _animationTranslateBackward = Tween(begin: 25.0, end: 0.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeInOut,
      ),
    );

    //That one works the first half of the time.
    _animationGrowBackward = Tween(begin: 1.0, end: 0.95).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Interval(0.0, 0.50, curve: Curves.easeInOut),
      ),
    );

    //And that one the second half.
    _animationReduceBackward = Tween(begin: 0.95, end: 1.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Interval(0.50, 1.0, curve: Curves.easeInOut),
      ),
    );

    _translateInnerBall = Tween(begin: 20.0, end: -40.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeInOut,
      ),
    );

    _innerBallStreamController = StreamController.broadcast();
  }

  @override
  void dispose() {
    _animationController.dispose();
    _innerBallStreamController.close();

    super.dispose();
  }

  //We are gonna use the Flow widget, it allows us to define
  //the rules of laying out the children through its delegate.

  @override
  Widget build(BuildContext context) {
    //Creating the inner ball.
    var _innerBall = ClipRRect(
      borderRadius: BorderRadius.circular(32),
      child: Padding(
        padding: const EdgeInsets.all(4.0),
        child: AnimatedBuilder(
          animation: _translateInnerBall,
          builder: (context, child) {
            return Transform.translate(
              offset: Offset(_translateInnerBall.value, 0.0),
              child: child,
            );
          },
          child: Container(
            width: 8,
            height: 8,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: Color(0xFF252525),
            ),
          ),
        ),
      ),
    );

    return Flow(
      delegate: TikTokLoadingAnimationDelegate(
        animationController: _animationController,
        animationTranslateForward: _animationTranslateForward,
        animationGrowForward: _animationGrowForward,
        animationReduceForward: _animationReduceForward,
        animationTranslateBackward: _animationTranslateBackward,
        animationGrowBackward: _animationGrowBackward,
        animationReduceBackward: _animationReduceBackward,
        innerBallSink: _innerBallSink,
      ),
      children: [
        //The first ball.
        Container(
          width: 25,
          height: 25,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: Color(0xFF37ffec),
          ),
          child: StreamBuilder(
            initialData: true,
            stream: _innerBallStream,
            builder: (context, snapshot) {
              return Visibility(
                visible: snapshot.data,
                child: _innerBall,
              );
            },
          ),
        ),
        //The second ball.
        Container(
          width: 25,
          height: 25,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: Color(0xFFf21458),
          ),
          child: StreamBuilder(
            initialData: true,
            stream: _innerBallStream,
            builder: (context, snapshot) {
              return Visibility(
                visible: !snapshot.data,
                child: _innerBall,
              );
            },
          ),
        ),
      ],
    );
  }
}

class TikTokLoadingAnimationDelegate extends FlowDelegate {
  final AnimationController animationController;

  final Animation<double> animationTranslateForward;
  final Animation<double> animationGrowForward;
  final Animation<double> animationReduceForward;

  final Animation<double> animationTranslateBackward;
  final Animation<double> animationGrowBackward;
  final Animation<double> animationReduceBackward;

  int firstBallIndex = 0;
  int secondBallIndex = 1;

  final Sink<bool> innerBallSink;

  bool showInnerBallOnForwardBall = true;

  TikTokLoadingAnimationDelegate({
    this.animationController,
    this.animationTranslateForward,
    this.animationGrowForward,
    this.animationReduceForward,
    this.animationTranslateBackward,
    this.animationGrowBackward,
    this.animationReduceBackward,
    this.innerBallSink,
  }) : super(repaint: animationController) {
    animationController.forward();

    animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        animationController.value = 0.0;
        animationController.animateTo(1.0);

        _reverseChildren();
        _showInnerBallOnForwardBall();
      }
    });
  }

  _reverseChildren() {
    var tmp = firstBallIndex;
    firstBallIndex = secondBallIndex;
    secondBallIndex = tmp;
  }

  _showInnerBallOnForwardBall() {
    innerBallSink.add(showInnerBallOnForwardBall);
    showInnerBallOnForwardBall = !showInnerBallOnForwardBall;
  }

  //With that method we are gonna paint the children.
  @override
  void paintChildren(FlowPaintingContext context) {
    //Painting the first child.
    context.paintChild(
      firstBallIndex,
      transform: Matrix4.identity()
        ..translate(animationTranslateBackward.value)
        ..scale(animationGrowBackward.value)
        ..scale(animationReduceBackward.value),
    );

    //Painting the second child.
    context.paintChild(
      secondBallIndex,
      transform: Matrix4.identity()
        ..translate(animationTranslateForward.value)
        ..scale(animationGrowForward.value)
        ..scale(animationReduceForward.value),
    );
  }

  //That method is made to notify the Flow widget about
  //repainting the children.
  //But we are never gonna use it, because we've gave that mission
  //(of notifying the Flow widget) to the animationController.
  //Whenever its animation values changes, it's gonna notify the
  //Flow widget.

  @override
  bool shouldRepaint(TikTokLoadingAnimationDelegate oldDelegate) {
    return false;
  }
}

5 comments:

  1. It took me like 12 hours of working to write this article, so please, if you have anything to say, don't hesitate to do it.

    ReplyDelete
  2. great effort bro 😮😮..keep doing 💥💥

    ReplyDelete
  3. Thank you very much, your hard work help me a lot !

    ReplyDelete
  4. Very informative post! There is a lot of information here that can help any business get started with a successful social networking campaign. walkin interviews in uae

    ReplyDelete

| Designed by Colorlib