Создаем анимации во Flutter с помощью Canvas
Доброго времени суток, меня зовут Андрей, я являюсь Flutter разработчиком. По мере моего изучения flutter, я часто работал с анимациями. На самом деле, Для любителей создания анимации, Flutter — хороший выбор, так как большинство решений в нем уже реализовано. К примеру, при помощи виджета AnimatedOpacity, можно в пару кликов сделать анимацию понижения или повышения прозрачности. А если немного поиграться с параметром curve, то можно создать еще и кривую, по которой будет происходить эта анимация.
Также для тех, кто только начинает изучения анимаций во flutter, рекомендую ознакомиться с виджетом AnimatedContainer. Для создания простых и несложных анимаций, он будет очень полезен и понятен для использования. Если в кратце то имеет почти такие же параметры что и обычный Container, однако добавляет на изменяемые параметры анимацию.
@override
Widget 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),
);
}

Однако бывают ситуации, когда мы хотим большего чем изменение параметров у виджета Container. Для этого мы можем рассмотреть еще два подхода к созданиям анимации.
- Создания анимации при помощи Animation и AnimationСontroller.
- Создание анимации при помощи Tween.
В ходе данной статьи мы не будем останавливаться на них. Скорее всего, их сравнение и примеры использования мы рассмотрим в следующей статье. Однако для создания примера, мы затронем первый подход.
Изучив принцип работы при помощи
Для примера представим, что нам необходимо сделать собственную анимированную кнопку «Play» для нашего видео-плеера. Первый вариант решения этой проблемы — использование стандартного во flutter виджета AnimatedIcon.
import 'package:flutter/material.dart';
class FlutterAnimatedIconWidget extends StatefulWidget {
FlutterAnimatedIconWidget();
@override
_FlutterAnimatedIconWidgetState createState() => _FlutterAnimatedIconWidgetState();
}
class _FlutterAnimatedIconWidgetState extends State<FlutterAnimatedIconWidget> with SingleTickerProviderStateMixin {
AnimationController animationController;
@override
void initState() {
animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 400));
super.initState();
}
@override
Widget 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();
}
}
![]()
Это самый простой и быстрый вариант. Однако, он имеет множество недостатков:
- Очень небольшой выбор иконок.
- Тяжело редактировать.
- Не дает полную свободу в создании анимаций.
Второй вариант: Использование пакета animate_icons (pub.dev/packages/animate_icons). Он дает нам возможность анимировать любые иконки. Подробно на нем не будем останавливаться, так как мне он кажется на данный момент слишком недоработанным. Остановимся просто на небольшом примере.
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,
);
}
![]()
Однако полную свободу действий в создании анимаций можно получить при использовании Canvas. Такой метод имеет недостатки в том что, это более трудный процесс.
Для начала нам необходимо создать класс CustomIconPaint, который расширяется от 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,
});
@override
void paint(Canvas canvas, Size size) {
size = Size(sizeIcon, sizeIcon);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
В него мы будем передавать несколько параметров, для того чтобы мы могли его изменять извне. Давайте пройдемся быстро по параметрам. Параметр value — это переменный параметр который и будет отвечать за нашу анимацию. SizeIcon — отвечает за размер иконки. Color и strokeColor отвечают за цвет и обводку.
Следующим шагом будет создание кистей.
Мы будем создавать две кисти:
- Будет использоваться для заливки.
- Будет использоваться для создания контура.
@override
void 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;
}
Для создания динамически изменяющегося контура, мы используем вот такую строчку: strokeWidth = sizeIcon /10. В случае, если мы не передадим цвет, будет поставлен Colors.black.
Теперь у нас все готово, для создание иконки, для начала нам необходимо создать приватную функцию и передать все необходимые параметры.
@override
void 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();
}
Для использования canvas, нам необходимо сначала написать canvas.save();.
Для написания мы будем использовать, класс Path и его функцию addPolygon.
ИДля начала нам необходимо по точкам нарисовать одно из положений нашей иконки.
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);
}
По итогу выходит такой результат.

На данный момент она находиться в горизонтальном положении, так как мне так легче отрисовывать, а в дальнейшем мы добавим поворот иконки. С такого положения это будет легче сделать. Для того чтобы не повторять одинаковый код, я вынес sizeIcon / 8, в переменную space, эта переменная отвечает за отступ между прямоугольниками, который равен ⅛ от размера иконки. В самом конце мы отрисовываем результат два раза, сначала контур потом саму заливку.
canvas.drawPath(path, paintStroke); canvas.drawPath(path, paint);
Далее нам надо создать саму анимацию перехода из одного состояния иконки в другое. Для этого у нас есть параметр value. При старте анимации параметр value = 0, а в конце value = 1. Конкретно как работает value и где он берется, мы сейчас и рассмотрим. Для этого нам необходимо создать виджет обертку над нашим канвасом.
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;
@override
void 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();
}
@override
void dispose() {
rotateController.removeListener(_rotateControllerUpdateListener);
animationController.removeListener(_animationControllerUpdateListener);
rotateController.dispose();
animationController.dispose();
super.dispose();
}
@override
Widget 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(() {});
}
Итак, давайте разберемся, что тут происходит и почему так. Для начала создается два контроллера rotateController и animationController. Первый необходим для создания анимации вращения, второй будет отвечать за саму анимацию. Далее мы добавляет listener, которые будут реагировать на изменение и перерисовать виджет по ходу выполнения анимации. Когда закончиться первая анимация у code>rotateController, запуститься анимация у animationController. Сделано это для улучшения визуала. Обратная анимация будет выполняться параллельно и там и там одновременно. Для вращение мы используем Transform.rotate, вращение которого зависит от rotateController.value. Для нашего CustomIconPaint, в качестве параметра value, мы передаем animationController.value. Промежуточный результат от 0 до 1, в течение времени, которое мы укажем в параметре duration, будет передаваться в наш paint, от него уже будет зависеть наша анимация. На данный момент мы получили такой результат.
![]()
Что ж, теперь осталось немного. Осталось только создать зависимость между animationController и canvas. Для этого необходимо добавить зависимость от value в момент отрисовки точек.
void _drawPath(Canvas canvas, Paint paint, Paint paintStroke) {
canvas.save();
Path path = Path();
double space = sizeIcon / 8;
path.addPolygon(
[
Offset(0, 0),
Offset(sizeIcon, (sizeIcon / 2) * value),
Offset(sizeIcon, (sizeIcon / 2 - (space * (1 - value)))),
Offset(0, (sizeIcon / 2 - (space * (1 - value)))),
],
true,
);
path.addPolygon(
[
Offset(0, sizeIcon),
Offset(sizeIcon, sizeIcon - (sizeIcon / 2) * value),
Offset(sizeIcon, (sizeIcon / 2 + (space * (1 - value)))),
Offset(0, (sizeIcon / 2 + (space * (1 - value)))),
],
true,
);
canvas.drawPath(path, paintStroke);
canvas.drawPath(path, paint);
}
После добавление зависимости в некоторых точках от value, конечный результат выглядит таким.
![]()
Самое главное, что теперь для использования нам достаточно просто вызывать виджет CustomAnimatedIconWidget и передать несколько параметров.
CanvasAnimatedIconWidget( onTap: _onTap, strokeColor: Colors.green, size: 50.0, color: Colors.lightGreenAccent, duration: Duration(milliseconds: 400), ),
Как итог могу сказать, что отрисовка с помощью canvas имеет преимущество в том, что можно создать любую анимацию, однако это трудоемкий процесс, требующий познаний в математике, также это требует немалой практики.
Спасибо за внимание, оставляйте свои отзывы по поводу статьи, возможно я что-то мог сделать по другому. Пишите, оказалась ли эта статья для вас полезной и узнали ли вы что-то новое. С удовольствие буду ждать ваших отзывов.
Ссылка на git: bitbucket.org/...n_canvas_example/src/dev
2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівПомогло решить волнующий вопрос! Как хорошо что в Flutter реализовано большинство решений. Очень помогает. Спасибо Вам, Андрей, за такую статью!
Спасибо за коментарий!