Flutter Animacje: Kompleksowy poradnik

5/10/2025 Mobilne

Mateusz Kędziora

image

Witaj programisto! W tym artykule zajmiemy się animacjami we Flutterze. Animacje są kluczowym elementem tworzenia angażujących i intuicyjnych interfejsów użytkownika. Dzięki nim aplikacja staje się bardziej responsywna i przyjemna w użyciu. Celem tego artykułu jest kompleksowe omówienie różnych technik animacji dostępnych w Flutterze, od prostych animacji domyślnych (implicit) po zaawansowane animacje wektorowe z wykorzystaniem Rive. Bez względu na Twój poziom zaawansowania, znajdziesz tutaj coś dla siebie. Przejdziemy przez przykłady kodu, wyjaśnienia i wskazówki, które pomogą Ci opanować sztukę animacji w Flutterze.

Co znajdziesz w tym artykule?

  • Wprowadzenie do animacji w Flutterze: Dlaczego animacje są ważne i co zamierzamy osiągnąć.
  • Animacje domyślne (Implicit Animations): Wykorzystanie AnimatedContainer, AnimatedOpacity i TweenAnimationBuilder.
  • Animacje jawne (Explicit Animations): Użycie AnimationController i Tween do precyzyjnej kontroli nad animacją.
  • Animacje z CustomPainter: Tworzenie własnych animacji za pomocą rysowania.
  • Zaawansowane techniki animacji: Krzywe animacji, animacje kaskadowe (staggered animations) i optymalizacja wydajności.
  • Animacje Rive: Wprowadzenie do Rive i integracja z Flutterem.
  • Podsumowanie: Kluczowe wnioski i zachęta do dalszej nauki.

Zaczynajmy!

1. Wprowadzenie do animacji w Flutterze

Animacje w aplikacjach mobilnych i webowych odgrywają kluczową rolę w poprawie doświadczenia użytkownika (UX). Poprawnie zaimplementowane animacje nie tylko sprawiają, że aplikacja wygląda bardziej atrakcyjnie, ale również zwiększają jej intuicyjność. Animacje mogą:

  • Wskazywać zmiany stanu: Np. animacja ładowania, przejście między ekranami.
  • Dawać feedback użytkownikowi: Np. animacja przycisku po kliknięciu.
  • Poprawiać percepcję: Np. płynne przejścia sprawiają, że aplikacja wydaje się szybsza.
  • Angażować użytkownika: Np. interaktywne elementy animowane.

Flutter oferuje bogaty zestaw narzędzi do tworzenia różnorodnych animacji, od prostych przejść po zaawansowane interaktywne efekty. W tym artykule zapoznasz się z kilkoma z nich, zaczynając od najprostszych (animacje domyślne), przez bardziej zaawansowane (animacje jawne i CustomPainter) aż po integrację z zewnętrzną biblioteką Rive.

2. Animacje domyślne (Implicit Animations)

Animacje domyślne, nazywane również animacjami niejawnymi, są najprostszym sposobem dodawania animacji w Flutterze. Wykorzystują one wbudowane widgety, które automatycznie animują zmiany swoich właściwości. Oznacza to, że zamiast ręcznie kontrolować proces animacji, po prostu zmieniasz wartość jakiejś właściwości widgetu, a Flutter zajmuje się resztą – animacją przejścia pomiędzy starą a nową wartością.

Najpopularniejsze widgety do animacji domyślnych:

  • AnimatedContainer: Animuje zmiany w kontenerze, takie jak rozmiar, kolor, margines, padding, dekoracja (np. border radius).
  • AnimatedOpacity: Animuje zmiany przezroczystości (opacity).
  • AnimatedPadding: Animuje zmiany paddingu.
  • AnimatedPositioned: Animuje zmiany pozycji widgetu w Stack.
  • AnimatedDefaultTextStyle: Animuje zmiany stylu tekstu.
  • AnimatedCrossFade: Umożliwia płynne przechodzenie między dwoma widgetami.
  • TweenAnimationBuilder: Pozwala na tworzenie własnych animacji domyślnych dla dowolnych właściwości.

Przykład: AnimatedContainer

Poniższy przykład pokazuje, jak użyć AnimatedContainer do zmiany koloru i rozmiaru kontenera po naciśnięciu przycisku.

import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Implicit Animation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double _width = 100;
  double _height = 100;
  Color _color = Colors.blue;
  BorderRadiusGeometry _borderRadius = BorderRadius.circular(8);

  void _animateContainer() {
    setState(() {
      // Generuj losowe wartości dla animacji
      Random random = Random();
      _width = random.nextInt(200).toDouble() + 50;
      _height = random.nextInt(200).toDouble() + 50;
      _color = Color.fromRGBO(
        random.nextInt(256),
        random.nextInt(256),
        random.nextInt(256),
        1,
      );
      _borderRadius = BorderRadius.circular(random.nextInt(50).toDouble());
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Implicit Animation Demo'),
      ),
      body: Center(
        child: AnimatedContainer(
          width: _width,
          height: _height,
          decoration: BoxDecoration(
            color: _color,
            borderRadius: _borderRadius,
          ),
          duration: Duration(milliseconds: 500), // Czas trwania animacji
          curve: Curves.fastOutSlowIn, // Krzywa animacji
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _animateContainer,
        tooltip: 'Animate',
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

Wyjaśnienie kodu:

  • AnimatedContainer przyjmuje parametry width, height, decoration, duration i curve.
  • duration określa czas trwania animacji.
  • curve określa krzywą animacji (np. Curves.easeInOut, Curves.bounceOut). Dostępnych jest wiele predefiniowanych krzywych, które nadają animacji charakterystyczny wygląd.
  • Po naciśnięciu przycisku (FloatingActionButton), funkcja _animateContainer zmienia wartości _width, _height, _color i _borderRadius.
  • setState() informuje Fluttera, że stan widgetu się zmienił, co powoduje przebudowanie widgetu AnimatedContainer z nowymi wartościami.
  • AnimatedContainer automatycznie animuje przejście pomiędzy starymi a nowymi wartościami właściwości width, height, color, borderRadius w czasie określonym przez duration i zgodnie z krzywą określoną przez curve.

Przykład: AnimatedOpacity

Poniższy przykład demonstruje jak zmieniać przezroczystość widgetu z użyciem AnimatedOpacity.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter AnimatedOpacity Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double _opacity = 1.0;

  void _toggleOpacity() {
    setState(() {
      _opacity = _opacity == 1.0 ? 0.0 : 1.0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedOpacity Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedOpacity(
              opacity: _opacity,
              duration: Duration(seconds: 1),
              child: Container(
                width: 200,
                height: 200,
                color: Colors.red,
              ),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _toggleOpacity,
              child: Text('Toggle Opacity'),
            ),
          ],
        ),
      ),
    );
  }
}

Wyjaśnienie kodu:

  • AnimatedOpacity przyjmuje parametry opacity i duration.
  • opacity określa stopień przezroczystości widgetu (0.0 - całkowicie przezroczysty, 1.0 - całkowicie widoczny).
  • Po naciśnięciu przycisku (ElevatedButton), funkcja _toggleOpacity zmienia wartość _opacity na przeciwną (1.0 na 0.0 lub 0.0 na 1.0).
  • AnimatedOpacity automatycznie animuje zmianę przezroczystości w czasie określonym przez duration.

TweenAnimationBuilder - Tworzenie własnych animacji domyślnych

TweenAnimationBuilder jest bardziej zaawansowanym widgetem, który pozwala na tworzenie własnych animacji domyślnych dla dowolnych właściwości, które nie są bezpośrednio obsługiwane przez inne widgety Animated.... Wykorzystuje on Tween, który definiuje zakres wartości animacji.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter TweenAnimationBuilder Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double _targetValue = 0.0;

  void _animateValue() {
    setState(() {
      _targetValue = _targetValue == 0.0 ? 1.0 : 0.0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TweenAnimationBuilder Demo'),
      ),
      body: Center(
        child: TweenAnimationBuilder<double>(
          tween: Tween<double>(begin: 0.0, end: _targetValue),
          duration: Duration(seconds: 1),
          builder: (BuildContext context, double value, Widget? child) {
            return Transform.scale(
              scale: 1 + value,
              child: Container(
                width: 100,
                height: 100,
                color: Colors.green,
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _animateValue,
        tooltip: 'Animate',
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

Wyjaśnienie kodu:

  • TweenAnimationBuilder przyjmuje parametry tween, duration i builder.
  • tween definiuje zakres wartości animacji (od begin do end). W tym przykładzie animujemy wartość typu double od 0.0 do _targetValue.
  • builder jest funkcją, która buduje widget na podstawie aktualnej wartości animacji. Przyjmuje trzy argumenty: context, value (aktualna wartość animacji) i child (opcjonalny widget, który można ponownie wykorzystać).
  • W tym przykładzie używamy Transform.scale do skalowania kontenera w oparciu o wartość animacji.
  • Po naciśnięciu przycisku (FloatingActionButton), funkcja _animateValue zmienia wartość _targetValue.
  • TweenAnimationBuilder automatycznie animuje wartość od 0.0 do _targetValue (lub z powrotem) w czasie określonym przez duration.

Podsumowanie Animacji Domyślnych:

Animacje domyślne są proste w użyciu i idealne do tworzenia podstawowych animacji, takich jak zmiany koloru, rozmiaru, przezroczystości czy paddingu. Są łatwe do implementacji i nie wymagają zaawansowanej wiedzy na temat animacji. Jednakże, mają pewne ograniczenia – nie dają pełnej kontroli nad procesem animacji i nie pozwalają na tworzenie złożonych sekwencji animacji. Do bardziej zaawansowanych animacji potrzebne są animacje jawne.

3. Animacje jawne (Explicit Animations)

Animacje jawne, w przeciwieństwie do animacji domyślnych, dają pełną kontrolę nad procesem animacji. Wykorzystują one AnimationController i Tween, aby precyzyjnie kontrolować każdą klatkę animacji. Dzięki temu można tworzyć złożone sekwencje animacji, zmieniać prędkość animacji w czasie, dodawać efekty specjalne i interakcje.

Kluczowe elementy animacji jawnych:

  • AnimationController: Zarządza czasem trwania animacji, kierunkiem (do przodu, do tyłu) i stanem (rozpoczęta, zatrzymana, zakończona).
  • Tween: Definiuje zakres wartości animacji (od begin do end).
  • Animation: Reprezentuje aktualną wartość animacji w danym momencie czasu. Animation jest tworzona na podstawie AnimationController i Tween.
  • Listener: Funkcja wywoływana przy każdej zmianie wartości animacji. W Listener aktualizujemy stan widgetu (np. używając setState()), aby wyświetlić aktualną klatkę animacji.

Przykład: Prosta animacja z AnimationController i Tween

Poniższy przykład pokazuje, jak użyć AnimationController i Tween do animowania przesunięcia (offset) kontenera.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Explicit Animation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Offset> _offsetAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this, // Potrzebne do synchronizacji animacji z odświeżaniem ekranu
    );

    _offsetAnimation = Tween<Offset>(
      begin: Offset.zero,
      end: const Offset(1, 0), // Przesunięcie o 1 w prawo
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));

    _controller.repeat(reverse: true); // Powtarzaj animację w przód i w tył
  }

  @override
  void dispose() {
    _controller.dispose(); // Zwolnij zasoby
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Explicit Animation Demo'),
      ),
      body: Center(
        child: SlideTransition(
          position: _offsetAnimation,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.red,
          ),
        ),
      ),
    );
  }
}

Wyjaśnienie kodu:

  • SingleTickerProviderStateMixin jest potrzebny do synchronizacji animacji z odświeżaniem ekranu.
  • AnimationController jest inicjalizowany w initState() z duration (czas trwania animacji) i vsync (obiekt, który dostarcza sygnały synchronizacji ekranu).
  • Tween<Offset> definiuje zakres wartości animacji (od Offset.zero do Offset(1, 0)).
  • _offsetAnimation jest tworzona na podstawie AnimationController i Tween. Dodatkowo używamy CurvedAnimation aby dodać krzywą animacji (Curves.easeInOut).
  • _controller.repeat(reverse: true) uruchamia animację i powtarza ją w nieskończoność, odwracając kierunek po osiągnięciu końca.
  • SlideTransition jest widgetem, który przesuwa swój child w oparciu o wartość _offsetAnimation.
  • _controller.dispose() zwalnia zasoby, gdy widget jest usuwany z drzewa widgetów.

Sterowanie cyklem życia animacji

AnimationController oferuje metody do sterowania cyklem życia animacji:

  • forward(): Rozpoczyna animację w kierunku do przodu.
  • reverse(): Rozpoczyna animację w kierunku do tyłu.
  • stop(): Zatrzymuje animację.
  • reset(): Resetuje animację do stanu początkowego.
  • dispose(): Zwalnia zasoby.

Można również monitorować stan animacji za pomocą AnimationStatusListener:

_controller.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    // Animacja zakończyła się w kierunku do przodu
    _controller.reverse();
  } else if (status == AnimationStatus.dismissed) {
    // Animacja zakończyła się w kierunku do tyłu
    _controller.forward();
  }
});

Łączenie animacji (Chaining Animations)

Można łączyć animacje, aby tworzyć złożone sekwencje. Można to zrobić na kilka sposobów:

  1. Użycie Future.delayed: Opóźnia rozpoczęcie kolejnej animacji.
_controller.forward().then((_) {
  Future.delayed(Duration(seconds: 1), () {
    _controller2.forward(); // Uruchom drugą animację po 1 sekundzie
  });
});
  1. Użycie AnimationStatusListener: Uruchamia kolejną animację po zakończeniu poprzedniej.
_controller.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    _controller2.forward(); // Uruchom drugą animację po zakończeniu pierwszej
  }
});
_controller.forward();
  1. Użycie SequentialAnimation (z pakietu animations): Pozwala na definiowanie sekwencji animacji. Niestety pakiet ten nie jest już aktywnie rozwijany (stan na 8 maja 2025), ale może stanowić inspirację. Zalecane jest napisanie własnego rozwiązania bazującego na powyższych przykładach lub poszukanie alternatywnych pakietów.

Krzywe animacji

Krzywe animacji (Curves) definiują, jak wartość animacji zmienia się w czasie. Domyślnie animacja ma liniowy przebieg (wartość zmienia się równomiernie). Krzywe animacji pozwalają na tworzenie bardziej naturalnych i interesujących efektów.

Flutter oferuje wiele predefiniowanych krzywych:

  • Curves.linear: Liniowy przebieg animacji.
  • Curves.easeInOut: Animacja rozpoczyna się i kończy wolno.
  • Curves.easeIn: Animacja rozpoczyna się wolno i przyspiesza.
  • Curves.easeOut: Animacja rozpoczyna się szybko i zwalnia.
  • Curves.bounceIn: Efekt odbicia na początku animacji.
  • Curves.bounceOut: Efekt odbicia na końcu animacji.
  • Curves.elasticIn: Efekt elastycznego rozciągania na początku animacji.
  • Curves.elasticOut: Efekt elastycznego rozciągania na końcu animacji.

Można również tworzyć własne krzywe animacji, implementując interfejs Curve.

class MyCustomCurve extends Curve {
  @override
  double transformInternal(double t) {
    // Implementacja własnej krzywej animacji
    return t * t; // Prosty przykład - kwadratowa krzywa
  }
}

Podsumowanie Animacji Jawnych:

Animacje jawne dają pełną kontrolę nad procesem animacji, pozwalając na tworzenie złożonych sekwencji animacji, zmiany prędkości animacji w czasie, dodawanie efektów specjalnych i interakcji. Wymagają one jednak więcej kodu i zrozumienia, niż animacje domyślne. Są idealne do tworzenia zaawansowanych interfejsów użytkownika i interaktywnych elementów.

4. Animacje z CustomPainter

CustomPainter pozwala na rysowanie własnych kształtów i elementów graficznych na ekranie. Można go użyć do tworzenia niestandardowych animacji, które nie są możliwe do osiągnięcia za pomocą standardowych widgetów. Dzięki CustomPainter możemy animować ścieżki (paths), gradienty, tekstury i inne elementy wizualne.

Kluczowe elementy animacji z CustomPainter:

  • CustomPainter: Klasa, którą rozszerzamy, aby zdefiniować, co ma być narysowane na ekranie.
  • paint(Canvas canvas, Size size): Metoda, w której rysujemy elementy graficzne.
  • shouldRepaint(CustomPainter oldDelegate): Metoda, która określa, czy widget powinien być przerysowany.
  • Animation: Wykorzystywana do animowania właściwości rysowanych elementów.
  • RepaintBoundary: Widget, który izoluje obszar rysowania, poprawiając wydajność.

Przykład: Animacja okręgu

Poniższy przykład pokazuje, jak użyć CustomPainter do animowania promienia okręgu.

import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter CustomPainter Animation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _radiusAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    _radiusAnimation = Tween<double>(
      begin: 0,
      end: 100,
    ).animate(_controller);

    _controller.repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('CustomPainter Animation Demo'),
      ),
      body: Center(
        child: RepaintBoundary( // Izolacja obszaru rysowania
          child: AnimatedBuilder(
            animation: _radiusAnimation,
            builder: (context, child) {
              return CustomPaint(
                size: Size(200, 200),
                painter: CirclePainter(radius: _radiusAnimation.value),
              );
            },
          ),
        ),
      ),
    );
  }
}

class CirclePainter extends CustomPainter {
  final double radius;

  CirclePainter({required this.radius});

  @override
  void paint(Canvas canvas, Size size) {
    final center = size.center(Offset.zero);
    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.fill;

    canvas.drawCircle(center, radius, paint);
  }

  @override
  bool shouldRepaint(covariant CirclePainter oldDelegate) {
    return oldDelegate.radius != radius; // Przerysuj tylko jeśli promień się zmienił
  }
}

Wyjaśnienie kodu:

  • CirclePainter rozszerza klasę CustomPainter.
  • W metodzie paint rysujemy okrąg za pomocą canvas.drawCircle. Promień okręgu jest określony przez radius.
  • Metoda shouldRepaint zwraca true tylko wtedy, gdy promień się zmienił. Dzięki temu unikamy niepotrzebnego przerysowywania widgetu.
  • AnimatedBuilder odbudowuje widget za każdym razem, gdy wartość _radiusAnimation się zmienia.
  • RepaintBoundary izoluje obszar rysowania, co poprawia wydajność animacji.

Animowanie ścieżek (Paths)

Można animować ścieżki (paths) za pomocą CustomPainter, tworząc złożone i dynamiczne efekty. Aby to zrobić, należy zdefiniować ścieżkę w metodzie paint i animować jej punkty kontrolne lub inne właściwości.

Animowanie gradientów

Można również animować gradienty, zmieniając ich kolory, położenie lub orientację. Aby to zrobić, należy zdefiniować Gradient w metodzie paint i animować jego właściwości.

Podsumowanie Animacji z CustomPainter:

CustomPainter daje ogromną elastyczność w tworzeniu niestandardowych animacji. Pozwala na rysowanie dowolnych kształtów i animowanie ich właściwości. Wymaga jednak dobrego zrozumienia rysowania w Flutterze i optymalizacji wydajności. Jest idealny do tworzenia unikalnych i wizualnie atrakcyjnych efektów.

5. Zaawansowane techniki animacji

Po opanowaniu podstawowych technik animacji w Flutterze, można zacząć eksperymentować z bardziej zaawansowanymi technikami, które pozwolą na tworzenie bardziej naturalnych, płynnych i angażujących animacji.

Krzywe animacji dla naturalnych animacji

Używanie odpowiednich krzywych animacji ma kluczowe znaczenie dla tworzenia naturalnych i realistycznych efektów. Eksperymentuj z różnymi krzywymi, aby znaleźć te, które najlepiej pasują do Twojej animacji.

  • Curves.easeInOut: Idealna do większości animacji, tworzy płynne przejście z przyspieszeniem i zwalnianiem.
  • Curves.elasticOut: Daje efekt sprężystego odbicia na końcu animacji, świetny do animacji, które mają imitować fizyczne ruchy.
  • Curves.bounceOut: Podobnie jak elasticOut, ale z efektem odbicia.
  • Curves.fastOutSlowIn: Szybki początek i powolne zakończenie, dobra do animacji, które mają być dynamiczne, ale eleganckie.

Można również tworzyć własne krzywe animacji, implementując interfejs Curve, aby uzyskać jeszcze bardziej unikalne efekty.

Animacje kaskadowe (Staggered Animations)

Animacje kaskadowe to technika, w której animacje poszczególnych elementów uruchamiane są z opóźnieniem, tworząc efekt sekwencyjny. Dzięki temu animacja wydaje się bardziej dynamiczna i interesująca.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Staggered Animation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  late AnimationController _controller;
  late List<Animation<double>> _animations;
  int numberOfItems = 5;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    _animations = List.generate(numberOfItems, (index) {
      return Tween<double>(
        begin: 0.0,
        end: 1.0,
      ).animate(
        CurvedAnimation(
          parent: _controller,
          curve: Interval(
            index / numberOfItems, // Start animacji elementu
            1.0,               // Koniec animacji elementu
            curve: Curves.easeOut,
          ),
        ),
      );
    });

    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Staggered Animation Demo'),
      ),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: List.generate(numberOfItems, (index) {
            return FadeTransition(
              opacity: _animations[index],
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Container(
                  width: 50,
                  height: 50,
                  color: Colors.blue,
                ),
              ),
            );
          }),
        ),
      ),
    );
  }
}

Wyjaśnienie kodu:

  • Tworzymy listę animacji (_animations) dla każdego elementu.
  • Używamy Interval w CurvedAnimation, aby opóźnić rozpoczęcie animacji każdego elementu.
  • index / numberOfItems określa moment rozpoczęcia animacji dla danego elementu w relacji do czasu trwania całej animacji.
  • FadeTransition animuje przezroczystość każdego elementu w oparciu o jego animację.

Budowanie złożonych sekwencji animacji

Do budowania złożonych sekwencji animacji można wykorzystać kombinację technik, takich jak:

  • Future.delayed: Do opóźniania rozpoczęcia kolejnych animacji.
  • AnimationStatusListener: Do uruchamiania kolejnych animacji po zakończeniu poprzednich.
  • TweenSequence: Pozwala na zdefiniowanie sekwencji wartości Tween w jednej animacji. Uwaga: Dostępność i wsparcie dla tej klasy może być ograniczone w nowszych wersjach Fluttera. Zawsze sprawdź aktualną dokumentację.

Optymalizacja wydajności animacji

Animacje mogą być zasobożerne, zwłaszcza te bardziej złożone. Dlatego ważne jest, aby zoptymalizować wydajność animacji, aby uniknąć spadków płynności (tzw. “jank”).

Porady dotyczące optymalizacji wydajności animacji:

  • Używaj RepaintBoundary: Izoluj obszary, które się zmieniają, aby Flutter nie musiał przerysowywać całego ekranu.
  • Unikaj przebudowywania widgetów w build: Używaj AnimatedBuilder i ValueListenableBuilder do odbudowywania tylko tych widgetów, które rzeczywiście się zmieniły.
  • Używaj Transform zamiast zmiany właściwości: Zmiana właściwości widgetu często powoduje jego przebudowanie, podczas gdy Transform pozwala na zmianę wyglądu widgetu bez jego przebudowywania.
  • Używaj Opacity zamiast Visibility: Visibility powoduje usunięcie widgetu z drzewa widgetów i ponowne jego dodanie, co jest kosztowne. Opacity tylko zmienia przezroczystość, co jest znacznie szybsze.
  • Ogranicz liczbę animowanych elementów: Zbyt duża liczba animowanych elementów może obciążyć procesor.
  • Monitoruj wydajność: Używaj narzędzi do profilowania wydajności (np. Flutter DevTools), aby zidentyfikować wąskie gardła.

6. Animacje Rive

Rive (dawniej Flare) to potężne narzędzie do tworzenia interaktywnych animacji wektorowych w czasie rzeczywistym. Pozwala na tworzenie skomplikowanych animacji, które można łatwo zintegrować z aplikacjami Flutter. Rive oferuje edytor online, w którym można tworzyć animacje, a następnie eksportować je do formatu .riv, który można zaimportować do projektu Flutter.

Zalety Rive:

  • Wektorowe animacje: Skalowalne bez utraty jakości.
  • Interaktywność: Możliwość sterowania animacjami za pomocą kodu.
  • Edytor wizualny: Łatwe tworzenie skomplikowanych animacji bez konieczności pisania kodu.
  • Wydajność: Zoptymalizowane do renderowania w czasie rzeczywistym.
  • Bogata biblioteka: Dostęp do wielu gotowych animacji i zasobów.

Integracja Rive z Flutterem:

Aby zintegrować animacje Rive z aplikacją Flutter, należy dodać pakiet rive do pliku pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  rive: ^0.12.0 # Sprawdź najnowszą wersję na pub.dev

Następnie można użyć widgetu RiveAnimation.asset lub RiveAnimation.network do wyświetlania animacji z pliku lokalnego lub z adresu URL.

import 'package:flutter/material.dart';
import 'package:rive/rive.dart';

class RiveAnimationScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Rive Animation Demo'),
      ),
      body: Center(
        child: SizedBox(
          width: 200,
          height: 200,
          child: RiveAnimation.asset(
            'assets/my_animation.riv', // Ścieżka do pliku .riv w assets
          ),
        ),
      ),
    );
  }
}

Sterowanie animacjami Rive:

Pakiet rive udostępnia kontrolery animacji, które pozwalają na sterowanie stanami animacji, odtwarzaniem, pauzowaniem i przechodzeniem między różnymi animacjami z poziomu kodu Flutter. Można reagować na interakcje użytkownika, takie jak naciśnięcia przycisków, aby zmieniać stany animacji Rive.

Rive jest doskonałym wyborem do tworzenia złożonych, interaktywnych elementów wizualnych w aplikacjach Flutter, które wykraczają poza możliwości standardowych animacji opartych na widgetach.

7. Animacje Hero

Animacje Hero to rodzaj animacji przejścia między ekranami lub widgetami w aplikacji Flutter, w których wspólny element (“bohater”) płynnie przemieszcza się między widokami. Jest to popularna technika do tworzenia wizualnie atrakcyjnych i spójnych nawigacji.

Podstawowe użycie animacji Hero

Aby zaimplementować animację Hero, należy owinąć wspólny widget w obu ekranach (lub w różnych miejscach tego samego ekranu) widgetem Hero i przypisać mu unikalny tag. Flutter automatycznie zajmie się animacją przejścia tego widgetu.

Przykład

Ekran 1:

import 'package:flutter/material.dart';

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: InkWell(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondScreen()),
            );
          },
          child: Hero(
            tag: 'my-image',
            child: Image.asset(
              'assets/image.jpg',
              width: 150,
              height: 150,
            ),
          ),
        ),
      ),
    );
  }
}

Ekran 2:

import 'package:flutter/material.dart';

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Screen'),
      ),
      body: Center(
        child: Hero(
          tag: 'my-image',
          child: Image.asset(
            'assets/image.jpg',
            width: 300,
            height: 300,
          ),
        ),
      ),
    );
  }
}

W tym przykładzie, po naciśnięciu na obraz na pierwszym ekranie, obraz “przeleci” płynnie na drugi ekran, powiększając się. Widgety Hero na obu ekranach mają ten sam tag, co informuje Fluttera, który element ma zostać animowany.

Konfiguracja animacji Hero

Można dostosować wygląd animacji Hero za pomocą właściwości widgetu Hero, takich jak createRectTween do definiowania niestandardowej animacji prostokąta granicznego. Domyślnie Flutter używa MaterialRectArcTween, który tworzy łukową animację.

Animacje Hero są kluczowym elementem w tworzeniu płynnych i intuicyjnych interfejsów użytkownika w aplikacjach Flutter, szczególnie podczas nawigacji między różnymi częściami aplikacji.

Podsumowanie

Podsumowując, ten artykuł to prawdziwa skarbnica wiedzy o animacjach we Flutterze, od prostych efektów po zaawansowane techniki. Mamy nadzieję, że zainspirował Was do eksperymentowania i dodania trochę życia do Waszych projektów!

Polecane artykuły