Создаем анимации во 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 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів