Step Two: AnimatedWidget

Back on track, time to make those bars dance.

1. SetUp Animation boiler plate in BarLoadingState

The following is very similar to the ColorTween example.This has no functionality right now.

  1. class _BarLoadingScreenState extends State<BarLoadingScreen>
  2. with TickerProviderStateMixin {
  3. AnimationController _controller;
  4. Tween<double> tween;
  5. @override
  6. initState() {
  7. super.initState();
  8. _controller = new AnimationController(
  9. duration: const Duration(milliseconds: 3000),
  10. vsync: this,
  11. );
  12. tween = new Tween<double>(begin: 0.0, end: 1.00);
  13. // Just play the animation forever.
  14. _controller.repeat();
  15. }
  16. @override
  17. dispose() {
  18. _controller?.dispose();
  19. super.dispose();
  20. }
  21. @override
  22. Widget build(BuildContext context) {
  23. return new Container(
  24. child: new Center(
  25. child: new Row(
  26. mainAxisAlignment: MainAxisAlignment.center,
  27. children: <Widget>[
  28. new Bar(),
  29. new Bar(),
  30. new Bar(),
  31. new Bar(),
  32. ],
  33. ),
  34. ),
  35. );
  36. }
  37. }

2. Add Intervals to the Animation

The main challenge of this animation is that it requires 8 separatesteps to the overall animation, controlled by one AnimationController, distributed over only 4 widgets.

There are 8 steps in this animation because each of the four bars makes a180degree pivot twice, and by the end of the animation they've all turned afull turn.

Luckily, Flutter also provides a way to make animations that only occur duringcertain times of a Tween. It's called an Interval.

You're going to have to write an Interval for each of the eight steps. Here'sthe explanation of one:

  1. class _BarLoadingScreenState extends State<BarLoadingScreen>
  2. with TickerProviderStateMixin {
  3. AnimationController _controller;
  4. Tween<double> tween;
  5. @override
  6. initState() {
  7. super.initState();
  8. _controller = new AnimationController(
  9. duration: const Duration(milliseconds: 3000),
  10. vsync: this,
  11. );
  12. tween = new Tween<double>(begin: 0.0, end: 1.00);
  13. _controller.repeat().orCancel;
  14. }
  15. @override
  16. dispose() {
  17. _controller?.dispose();
  18. super.dispose();
  19. }
  20. // Animations always start with tweens, but you can reuse tweens.
  21. Animation<double> get stepOne => tween.animate(
  22. // For intervals, you can pass in an Animation rather than
  23. // the controller
  24. new CurvedAnimation(
  25. // But pass in the controller here
  26. parent: _controller,
  27. // The interval is basically what point of the tween
  28. // to start at, and what point to end at
  29. // this tween is 0 to 1,
  30. // so step one should only animate the first 1/8 of the tween
  31. // which is 0 to 0.125
  32. curve: new Interval(
  33. 0.0,
  34. 0.125,
  35. // the style curve to pass.
  36. curve: Curves.linear,
  37. ),
  38. ),
  39. );
  40. // ...

Again, you need to write an interval for each of the 8 steps. Or, you can copyand paste from the source code.

Once you've written all 8 intervals, you're going to need the widgets thatactually animate during each of these 8 steps.

3: Use Animated Widgets

Another awesome built in Flutter feature is AnimatedWidgets. These providetwo neat things:

  • You don't have to use addListener and setState on your animations totell Flutter to rebuild. AnimatedWidgets have a different technique.
  • There are some built in classes that extend AnimatedWidget and providesome common 'transformations'.This is the entire AnimatedWidget. That I built for this animation. Don't getbogged down in the common details(like margins): I highlighted the pieces thatare important to understand.
  1. class PivotBar extends AnimatedWidget {
  2. // Animated Widgets need to be passed an animation,
  3. // Or in this case, multiple animations.
  4. final List<Animation<double>> animations;
  5. // They also need the controller.
  6. final Animation<double> controller;
  7. // These are properties specific to this case.
  8. final FractionalOffset alignment;
  9. final bool isClockwise;
  10. final double marginLeft;
  11. final double marginRight;
  12. PivotBar({
  13. Key key,
  14. this.alignment: FractionalOffset.centerRight,
  15. @required this.controller,
  16. @required this.animations,
  17. @required this.isClockwise,
  18. this.marginLeft = 15.0,
  19. this.marginRight = 0.0,
  20. }) : super(key: key, listenable: controller);
  21. // The AnimatedWidget in this case is animating a relatively unused value.
  22. // Which is the transform value on the transform widget.
  23. // Transforms are much like CSS transform. It accepts a variety of functions
  24. // on it's Transform property. This specific property will rotate a widget
  25. // around a designated point.
  26. // The most important part to understand here is that it relies
  27. // on the value of the animation (Interval), so it's constantly being updated
  28. // by the AnimatedWidget
  29. Matrix4 clockwiseHalf(animation) =>
  30. new Matrix4.rotationZ((animation.value * math.pi * 2.0) * .5);
  31. Matrix4 counterClockwiseHalf(animation) =>
  32. new Matrix4.rotationZ(-(animation.value * math.pi * 2.0) * .5);
  33. @override
  34. Widget build(BuildContext context) {
  35. // Tell the widget which way to rotate based on its position
  36. var transformOne;
  37. var transformTwo;
  38. if (isClockwise) {
  39. transformOne = clockwiseHalf(animations[0]);
  40. transformTwo = clockwiseHalf(animations[1]);
  41. } else {
  42. transformOne = counterClockwiseHalf(animations[0]);
  43. transformTwo = counterClockwiseHalf(animations[1]);
  44. }
  45. // This is the real trick. Just wrap the Bar widget in two transforms, one
  46. // for each transformation (or Interval passed in as an Animation).
  47. return new Transform(
  48. transform: transformOne,
  49. alignment: alignment,
  50. child: new Transform(
  51. transform: transformTwo,
  52. alignment: alignment,
  53. child: new Bar(marginLeft: marginLeft, marginRight: marginRight),
  54. ),
  55. );
  56. }
  57. }

In order to get this all working perfectly, you'll also need to update yourBar class to respect the passed in margins.

  1. class Bar extends StatelessWidget {
  2. final double marginLeft;
  3. final double marginRight;
  4. const Bar({Key key, this.marginLeft, this.marginRight}) : super(key: key);
  5. @override
  6. Widget build(BuildContext context) {
  7. return new Container(
  8. width: 35.0,
  9. height: 15.0,
  10. margin: new EdgeInsets.only(left: marginLeft, right: marginRight),
  11. decoration: new BoxDecoration(
  12. color: const Color.fromRGBO(0, 0, 255, 1.0),
  13. borderRadius: new BorderRadius.circular(10.0),
  14. boxShadow: [
  15. new BoxShadow(
  16. color: Colors.black12,
  17. blurRadius: 8.0,
  18. spreadRadius: 1.0,
  19. offset: new Offset(1.0, 0.0),
  20. ),
  21. new BoxShadow(
  22. color: Colors.black26,
  23. blurRadius: 6.0,
  24. spreadRadius: 1.5,
  25. offset: new Offset(1.0, 0.0),
  26. ),
  27. ],
  28. ),
  29. );
  30. }
  31. }

4: Add the Animated Widget to the _BarLoadingScreenState

Now that you have 8 intervals to your animation, and an AnimatedWidget tofeed em to, the last step is just adding the PivotBar to your build method in_BarLoadingScreenState.

  1. @override
  2. Widget build(BuildContext context) {
  3. return new Container(
  4. decoration: new BoxDecoration(color: animation.value),
  5. child: new Center(
  6. child: new Row(
  7. mainAxisAlignment: MainAxisAlignment.center,
  8. children: <Widget>[
  9. // The left most bar gets the first two animations
  10. // because it always does a full turn.
  11. new PivotBar(
  12. alignment: FractionalOffset.centerLeft,
  13. controller: _controller,
  14. animations: [
  15. stepOne,
  16. stepTwo,
  17. ],
  18. marginRight: 0.0,
  19. marginLeft: 0.0,
  20. isClockwise: true,
  21. ),
  22. // This bar gets the third, but it only turns a half turn
  23. // Before the next bar's turn.
  24. new PivotBar(
  25. controller: _controller,
  26. animations: [
  27. stepThree,
  28. stepEight,
  29. ],
  30. marginRight: 0.0,
  31. marginLeft: 0.0,
  32. isClockwise: false,
  33. ),
  34. // Two more pivot bars
  35. ],
  36. ),
  37. ),
  38. );
  39. }

And that's it. I ommited some of the repeated code, but you can find it here inthe source code.