Flutter animation with Canvas

AppVesto LLC
7 min readNov 9, 2020

--

Hi, my name is Andrey, I am a Flutter developer. As I studied Flutter, I often worked with animations. In fact, for animation enthusiasts, flutter is a very good solution, since most of the solutions are already implemented. For example, with the AnimatedOpacity widget, you can make downscaling or transparency animations in a couple of clicks. And if you play a bit over a parameter, you can also create a curve along which this animation will take place.

Also, for those who are just beginning to learn the animation in flutter, I recommend that you read the AnimatedContainer widget. To create simple and simple animations, it will be very useful and easy to use. In short, it has almost the same parameters as a regular Container, but adds animation to the parameters you change.

@overrideWidget build(BuildContext context) {return AnimatedContainer(decoration: BoxDecoration(color: startAnimation ? Colors.lightGreen : Colors.red,borderRadius: BorderRadius.circular(startAnimation ? 15.0 : 0.0),),width: startAnimation ? 100 : 200,height: startAnimation ? 100 : 200,curve: Curves.easeInOutCubic,duration: Duration(seconds: 1),);}

However, there are situations where we want more than just a Container widget changing parameters. To do this, we can consider two other approaches to creating animations.

  • The creation of animations with Animation and AnimationController.
  • Animation creation with Tween.

We will not dwell on them during this article. Rather, we will discuss their comparison and examples of their use in the next article. However, to create an example, we will touch upon the first approach.

By learning how to work with the 2 approaches described above, we will be able to do a lot, but sometimes we may need to go even further. With the animation, we can only modify an existing widget. What if we want to change a widget that we drew ourselves with CustomPaint? This is the situation we will discuss in this article.

For example, let’s imagine that we need to make our own animated “Play” button for our video player. The first option, the solution to this problem, is to use the standard Flutter widget, AnimatedIcon.

import ‘package:flutter/material.dart’;class FlutterAnimatedIconWidget extends StatefulWidget {FlutterAnimatedIconWidget();@override_FlutterAnimatedIconWidgetState createState() => _FlutterAnimatedIconWidgetState();}class _FlutterAnimatedIconWidgetState extends State<FlutterAnimatedIconWidget> with SingleTickerProviderStateMixin {AnimationController animationController;@overridevoid initState() {animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 400));super.initState();}@overrideWidget build(BuildContext context) {return InkWell(onTap: _onTap,child: AnimatedIcon(icon: AnimatedIcons.play_pause,progress: animationController,color: Colors.orange,size: 150.0,),);}void _onTap() {if (animationController.value == 0) {animationController.forward();return;}animationController.reverse();}}

This is the easiest and fastest option. However, it has many disadvantages:

  • Very small selection of icons;
  • It’s hard to edit;
  • It does not give complete freedom in creating animations.

Second option: Using the package animate_icons (https://pub.dev/packages/animate_icons), this package gives us the ability to animate any icons. We won’t dwell on it in detail because I think it’s too unfinished at the moment. Let’s just stop at a small example.

Widget build(BuildContext context) {return AnimateIcons(startIcon: Icons.play_arrow,endIcon: Icons.pause,size: 150.0,onStartIconPress: () {return true;},onEndIconPress: () {return true;},duration: Duration(milliseconds: 500),color: Colors.orange,clockwise: false,);}

However, the full freedom of action in creating animations can be obtained by using Canvas. This method has disadvantages in that it is a more difficult process.

So, first we need to create a CustomIconPaint class that extends from CustomPainter.

class CustomIconPaint extends CustomPainter {final double value;final double sizeIcon;final Color color;final Color strokeColor;CustomIconPaint({@required this.value,@required this.sizeIcon,@required this.color,@required this.strokeColor,});@overridevoid paint(Canvas canvas, Size size) {size = Size(sizeIcon, sizeIcon);}@overridebool shouldRepaint(CustomPainter oldDelegate) => true;}

We will pass several parameters into it so that we can change it from the outside. Let’s go through the parameters quickly. The value parameter is the variable parameter that will be responsible for our animation. SizeIcon is responsible for the size of the icon. color and strokeColor, are responsible for color and stroke.

The next step is to create brushes.

We will create 2 brushes:

  • This will be used for filling;
  • Will be used to create a path.
@overridevoid paint(Canvas canvas, Size size) {size = Size(sizeIcon, sizeIcon);Paint paint = Paint()..strokeCap = StrokeCap.round..style = PaintingStyle.fill..color = color ?? Colors.black;Paint paintStroke = Paint()..strokeCap = StrokeCap.square..strokeJoin = StrokeJoin.round..strokeWidth = sizeIcon /10..style = PaintingStyle.stroke..color = strokeColor ?? Colors.black;}

To create a dynamically changing path, we use this line.

strokeWidth = sizeIcon /10. In case we don’t transfer the color, Colors.black will be supplied.

Now we’re all set, to create an icon, first we need to create a private function and pass all the necessary parameters.

@overridevoid paint(Canvas canvas, Size size) {size = Size(sizeIcon, sizeIcon);Paint paint = Paint()..strokeCap = StrokeCap.round..style = PaintingStyle.fill..color = color ?? Colors.black;Paint paintStroke = Paint()..strokeCap = StrokeCap.square..strokeJoin = StrokeJoin.round..strokeWidth = sizeIcon /10..style = PaintingStyle.stroke..color = strokeColor ?? Colors.black;_drawPath(canvas, paint, paintStroke);}void _drawPath(Canvas canvas, Paint paint, Paint paintStroke) {canvas.save();}

To use canvas, we have to write canvas.save() first;.

To write, we will use the Path class and its addPolygon function.

So first, we need to draw one of our icon positions by points.

void _drawPath(Canvas canvas, Paint paint, Paint paintStroke) {canvas.save();Path path = Path();double space = sizeIcon / 8;path.addPolygon([Offset(sizeIcon, (sizeIcon / 2 — space)),Offset(0, (sizeIcon / 2 — space)),Offset(0, 0),Offset(sizeIcon, 0),],true,);path.addPolygon([Offset(0, sizeIcon),Offset(sizeIcon, sizeIcon),Offset(sizeIcon, (sizeIcon / 2 + space)),Offset(0, (sizeIcon / 2 + space)),],true,);canvas.drawPath(path, paintStroke);canvas.drawPath(path, paint);}

The result is the following.

At the moment, it is in a horizontal position, because it is easier for me to draw, and in the future we will add a rotation of the icon, and from that position it will be easier to do. In order not to repeat the same code, I put sizeIcon / 8, in the space variable, this variable is responsible for the indent between the rectangles, which is ⅛ of the icon size. At the very end we draw the result twice, first the contour then the fill itself.

canvas.drawPath(path, paintStroke);canvas.drawPath(path, paint);

And so on, we have to create the animation for the transition from one icon state to another. For this we have the value parameter. When the animation starts the parameter value = 0 and at the end value = 1. Specifically how value works, and where it is taken, we will now consider. To do this, we need to create a wrapper widget over our canvas.

import ‘dart:math’;import ‘package:flutter/cupertino.dart’;import ‘package:flutter/material.dart’;import ‘package:animation_with_canvas_example/ui/canvas/custom_icon_paint.dart’;class CanvasAnimatedIconWidget extends StatefulWidget {final Duration duration;final void Function() onTap;final Color color;final Color strokeColor;final double size;CanvasAnimatedIconWidget({@required this.duration,@required this.onTap,this.size = 25.0,this.color,this.strokeColor,}) : assert(duration != null);@override_CanvasAnimatedIconWidgetState createState() => _CanvasAnimatedIconWidgetState();}class _CanvasAnimatedIconWidgetState extends State<CanvasAnimatedIconWidget> with TickerProviderStateMixin {AnimationController rotateController;AnimationController animationController;@overridevoid initState() {rotateController = AnimationController(duration: widget.duration, vsync: this, value: 0.0);animationController = AnimationController(duration: widget.duration, vsync: this, value: 0.0);rotateController.addListener(_rotateControllerUpdateListener);animationController.addListener(_animationControllerUpdateListener);super.initState();}@overridevoid dispose() {rotateController.removeListener(_rotateControllerUpdateListener);animationController.removeListener(_animationControllerUpdateListener);rotateController.dispose();animationController.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return Center(child: Transform.rotate(angle: -(pi / (2 / (1 — rotateController.value))),child: Container(width: widget.size,height: widget.size,child: InkWell(highlightColor: Colors.transparent,splashColor: Colors.transparent,onTap: _buttonTap,child: CustomPaint(foregroundPainter: CustomIconPaint(color: widget.color,strokeColor: widget.strokeColor,sizeIcon: widget.size,value: animationController.value,),),),),),);}void _buttonTap() {widget.onTap();if (rotateController.value == 0) {rotateController.forward();} else {rotateController.reverse();animationController.reverse();}}void _rotateControllerUpdateListener() {if (rotateController.value == 1) {if (animationController.value != 1) {animationController.forward();}} else {animationController.reverse();}setState(() {});}void _animationControllerUpdateListener() => setState(() {});}

And so, let’s find out what’s going on here and why. To start with, 2 rotateController and animationController are created. The first one is needed to create the rotation animation, the second one will be responsible for the animation itself. Next, we add a listener that will react to changes and redraw the widget as the animation progresses. When the first animation is finished, the animation will start at animationController. This is done to improve the visual. Reverse animation will be executed in parallel and there at the same time. For rotation we use Transform.rotate, the rotation of which depends on the rotateController.value. For our CustomIconPaint, we pass the animationController.value as a value parameter. An intermediate result between 0 and 1, for the time we specify in the duration parameter, will be transmitted to our paint, our animation will already depend on it. At this point, we have this result.

--

--

AppVesto LLC
AppVesto LLC

Written by AppVesto LLC

We are a team of rock-star developers, which provides fully-flexible and powerful products 💥.

No responses yet