В этой статье мы рассмотрим простой способ создания анимаций в Flutter, начнем с примера ниже. Как бы вы реализовали такой эффект анимации?
На самом деле создание подобного анимационного эффекта в Flutter довольно просто, даже без необходимости создания собственного компонента. Для этого можно использовать встроенные контроллеры.
Для начала нам потребуются AnimatedPositioned
и AnimatedContainer
:
AnimatedPositioned
используется для создания анимации перемещения внутри Stack
AnimatedContainer
используется для создания анимации изменения размераЗатем мы определяем класс PositionItem
, который объединяет AnimatedPositioned
и AnimatedContainer
. Мы также используем класс PositionedItemData
для управления положением и размерами этих элементов.
class PositionItem extends StatelessWidget {
final PositionedItemData data;
final Widget child;
const PositionItem(this.data, {required this.child});
@override
Widget build(BuildContext context) {
return new AnimatedPositioned(
duration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: new AnimatedContainer(
duration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
width: data.width,
height: data.height,
child: child,
),
left: data.left,
top: data.top,
);
}
}
class PositionedItemData {
final double left;
final double top;
final double width;
final double height;
PositionedItemData({
required this.left,
required this.top,
required this.width,
required this.height,
});
}
```После этого достаточно поместить `PositionItem` внутрь `Stack`, а затем использовать `LayoutBuilder`, чтобы получить размер родительского контейнера и корректировать положение и размер `PositionItem` через `PositionedItemData`. Таким образом, можно легко воспроизвести начальный анимационный эффект.
```dart
child: LayoutBuilder(
builder: (_, constraints) {
var first = getIndexPosition(currentIndex % 3, constraints.biggest);
var second = getIndexPosition((currentIndex + 1) % 3, constraints.biggest);
var third = getIndexPosition((currentIndex + 2) % 3, constraints.biggest);
return Stack(
fit: StackFit.expand,
children: [
PositionItem(first,
child: InkWell(
onTap: () {
print("красный");
},
child: Container(color: Colors.redAccent),
)
),
PositionItem(second,
child: InkWell(
onTap: () {
print("зелёный");
},
child: Container(color: Colors.greenAccent),
)
),
PositionItem(third,
child: InkWell(
onTap: () {
print("жёлтый");
},
child: Container(color: Colors.yellowAccent),
)
),
],
);
},
),
Как показано ниже, достаточно каждый раз менять соответствующий индекс, чтобы изменить размер и положение соответствующего элемента, что приведёт к запуску анимации AnimatedPositioned
и AnimatedContainer
, создающей эффект, аналогичный начальной анимации.
Вычисление размера | Эффект |
---|---|
![]() |
![]() |
Если вас не интересуют принципы реализации, то вы можете закончить чтение здесь. По вышеописанному примеру вы уже знаете один небольшой трюк:
Изменение любого параметра в
AnimatedPositioned
иAnimatedContainer
позволяет им запустить анимацию, так как их параметры полностью совпадают с параметрамиPositioned
иContainer
. Поэтому они могут использоваться взаимозаменяемо, при этом требуется лишь простое конфигурирование дополнительных параметров типаduration
.
А теперь вопрос, как AnimatedPositioned
и AnimatedContainer
запускают анимацию? Здесь мы рассмотрим абстрактный родительский класс ImplicitlyAnimatedWidget
.
Почти все компоненты, начинающиеся со слова "Animated", наследуются от него. Так как он используется для анимации, то
ImplicitlyAnimatedWidget
обязательно являетсяStatefulWidget
. Соответственно, основная логика реализуется вImplicitlyAnimatedWidgetState
, которую мы будем использовать далее.
Сначала давайте вспомним, какие компоненты обычно используются в Flutter для создания анимации:- AnimationController
: используется для управления запуском и остановкой анимации
TickerProvider
: используется для создания параметра vsync
для AnimationController
, чаще всего это SingleTickerProviderStateMixin
Animation
: используется для обработки значений анимации, например, часто используемый CurvedAnimation
FadeTransition
Кратко говоря, анимация в Flutter начинается с Ticker
. Когда мы добавляем with TickerProviderStateMixin
в State
, это означает, что наш компонент способен выполнять анимацию. Каждый раз, когда Flutter рисует кадр, объект Ticker
синхронизируется вызовом метода _tick
в контроллере анимации AnimationController
, после чего выполняется метод notifyListeners
. Это приводит к изменению значения анимации Animation
, что в свою очередь запускает метод setState
состояния или метод markNeedsPaint
объекта отрисовки, чтобы обновить интерфейс.Например, рассмотрим следующий код:
class _AnimatedOpacityState extends State<AnimatedOpacity>
with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
late final Animation<double> _animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: FadeTransition(
opacity: _animation,
child: const Padding(padding: EdgeInsets.all(8), child: FlutterLogo()),
),
);
}
}
Для простой анимации требуется немало кода, который часто повторяется. Поэтому официальное API предоставляет шаблон ImplicitlyAnimatedWidget
.
Если мы используем ImplicitlyAnimatedWidgetState
, нам нужно реализовать только два метода — forEachTween
и didUpdateTweens
. Мы больше не должны беспокоиться о создании AnimationController
и CurvedAnimation
.
Пример использования ImplicitlyAnimatedWidgetState
:```dart
class _AnimatedOpacityState extends ImplicitlyAnimatedWidgetState {
Tween? _opacity;
late Animation _opacityAnimation;
@override void forEachTween(TweenVisitor visitor) { _opacity = visitor(_opacity, widget.opacity, (dynamic value) => Tween(begin: value as double)) as Tween?; }
@override void didUpdateTweens() { _opacityAnimation = animation.drive(_opacity!); }
@override Widget build(BuildContext context) { return FadeTransition( opacity: _opacityAnimation, alwaysIncludeSemantics: widget.alwaysIncludeSemantics, child: widget.child, ); } }
Как же `ImplicitlyAnimatedWidgetState` позволяет менять значение `opacity` и запускать анимацию? Ключевой момент заключается в реализации метода `forEachTween`: когда обновляется значение `opacity`, вызывается метод `forEachTween`. Внутри этого метода через `_shouldAnimateTween` проверяется, было ли значение изменено. Если целевое значение действительно изменилось, выполняется метод `AnimationController.forward` базового класса для запуска анимации. 
> В этом разделе добавлено следующее содержание: внутренне в `FadeTransition` добавляется совместимость с `_opacityAnimation`. Когда `AnimationController` начинает выполнение анимации, это приводит к вызову слушателя `_opacityAnimation`, который затем запускает метод `markNeedsPaint`. **Как показано на нижеприведённой图为所指示**, `markNeedsPaint` в конечном итоге вызывает перерисовку объекта `RenderObject`.

Итак, мы узнали, что: **путём наследования от `ImplicitlyAnimatedWidget` и `ImplicitlyAnimatedWidgetState` можно более удобно реализовать некоторые эффекты анимации; многие стандартные анимационные эффекты в Flutter создаются именно таким образом**.> Кроме того, в шаблоне `ImplicitlyAnimatedWidget` помимо `ImplicitlyAnimatedWidgetState`, официальная документация также предоставляет другой подкласс `AnimatedWidgetBaseState`.
На самом деле большинство часто используемых нами анимационных компонентов в Flutter созданы путём использования шаблона `ImplicitlyAnimatedWidget`, как показано на нижеприведённом рисунке:
| `ImplicitlyAnimatedWidgetState` | `AnimatedWidgetBaseState` |
| ------------------------------------------------------------- | ------------------------------------------------------------ |
|  |  |
Отличия между этими двумя состояниями можно понять следующим образом:
- В `ImplicitlyAnimatedWidgetState` используется для работы с различными контроллерами типа `*Transition`, такими как использование `FadeTransition` в `AnimatedOpacity` и `ScaleTransition` в `AnimatedScale`. **Потому что `ImplicitlyAnimatedWidgetState` не использует `setState`, а вместо этого обновляет интерфейс через вызов метода `markNeedsPaint` у объекта `RenderObject`.**
- **`AnimatedWidgetBaseState` добавляет автоматическое слежение за `setState` на основе `ImplicitlyAnimatedWidgetState`**, поэтому можно создавать более гибкие анимации, такие как `AnimatedPositioned` и `AnimatedContainer`, которые мы использовали ранее.
На самом деле `AnimatedContainer` является очень характерным примером такой реализации. Если вы посмотрите его исходный код, то заметите, что его реализация довольно проста, **необходимо просто реализовать соответствующие `Tween` для каждого параметра в методе `forEachTween`**.Например, изменения, которые мы внесли ранее, такие как `width` и `height`, фактически меняют `BoxConstraints` контейнера, поэтому соответствующее решение — это `BoxConstraintsTween`. **А `BoxConstraintsTween` наследует `Tween`, главным образом реализуя метод `lerp` этого класса.**

В Flutter метод `lerp` используется для реализации интерполяции: например, во время анимации между двумя значениями `BoxConstraint` происходит линейная интерполяция, где t представляет собой значение времени анимации. Например:
> Вычисление промежуточных размеров при переходе от 100x100 до 200x200.
Как показано ниже с помощью наследования от `AnimatedWidgetBaseState` и использования метода `lerp` класса `ColorTween`, можно быстро создать следующий эффект плавной смены цвета текста.
| Код | Эффект |
| ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|  |  |
# Подведение итогов
В заключение стоит отметить, что в данной статье были рассмотрены:- Использование `AnimatedPositioned` и `AnimatedContainer` для быстрого создания эффекта смены анимации;
- Введение в `ImplicitlyAnimatedWidget` и описание того, как использовать `ImplicitlyAnimatedWidgetState` / `AnimatedWidgetBaseState` для упрощения процесса создания анимаций и быстрой реализации пользовательских анимаций.Имеете ли вы ещё какие-либо советы по использованию анимации в Flutter?
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )