Animazioni implicite in Flutter con AnimatedContainer
GuidePrincipiante35 min Flutter 3.x

Animazioni implicite in Flutter con AnimatedContainer

Le animazioni rendono un'app più viva e professionale, migliorando l'esperienza utente. Flutter offre due approcci principali: animazioni esplicite (con AnimationController) e animazioni implicite. Queste ultime sono il modo più semplice per aggiungere movimento alla UI: basta cambiare un valore e Flutter si occupa automaticamente della transizione.

In questo tutorial vedremo come usare i principali widget impliciti — AnimatedContainer, AnimatedOpacity e TweenAnimationBuilder — per costruire interfacce dinamiche con poche righe di codice. Non serve alcuna conoscenza pregressa di animazioni: partiremo da zero.

  1. 1

    Preparare il progetto e lo stato di base

    Creiamo un nuovo progetto e impostiamo uno StatefulWidget che conterrà le variabili che faremo animare. Le animazioni implicite reagiscono ai cambiamenti di stato: ogni volta che chiamiamo setState, i widget animati interpoleranno automaticamente tra il vecchio e il nuovo valore.

    Partiamo da una struttura minima con una variabile booleana _expanded che useremo per attivare/disattivare l'animazione.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Animazioni implicite',
          theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
          home: const AnimationDemo(),
        );
      }
    }
    
    class AnimationDemo extends StatefulWidget {
      const AnimationDemo({super.key});
    
      @override
      State<AnimationDemo> createState() => _AnimationDemoState();
    }
    
    class _AnimationDemoState extends State<AnimationDemo> {
      bool _expanded = false;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Animazioni implicite')),
          body: const Center(child: Text('Iniziamo!')),
        );
      }
    }

    Risultato atteso

    L'app si avvia mostrando una AppBar e il testo 'Iniziamo!' al centro dello schermo.

  2. 2

    Animare dimensioni e colore con AnimatedContainer

    AnimatedContainer è il widget implicito più versatile: anima automaticamente le modifiche a proprietà come width, height, color, padding, decoration e alignment.

    Le due proprietà fondamentali sono:

    • duration: quanto dura l'animazione.
    • curve: la curva di accelerazione (es. Curves.easeInOut).

    Quando cambiamo _expanded con setState, il container interpolerà tra i due set di valori.

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(title: const Text('Animazioni implicite')),
        body: Center(
          child: GestureDetector(
            onTap: () => setState(() => _expanded = !_expanded),
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 500),
              curve: Curves.easeInOut,
              width: _expanded ? 250 : 120,
              height: _expanded ? 250 : 120,
              decoration: BoxDecoration(
                color: _expanded ? Colors.indigo : Colors.orange,
                borderRadius: BorderRadius.circular(_expanded ? 32 : 8),
              ),
              alignment: Alignment.center,
              child: const Text(
                'Tocca!',
                style: TextStyle(color: Colors.white, fontSize: 18),
              ),
            ),
          ),
        ),
      );
    }

    Risultato atteso

    Toccando il riquadro, questo cambia dimensione, colore e raggio degli angoli con una transizione fluida di mezzo secondo.

  3. 3

    Aggiungere dissolvenze con AnimatedOpacity

    AnimatedOpacity anima la trasparenza di un widget, ideale per far apparire o scomparire elementi gradualmente. Il valore opacity va da 0.0 (invisibile) a 1.0 (completamente visibile).

    Aggiungiamo un testo che appare in dissolvenza solo quando il container è espanso. Avvolgiamo il tutto in una Column per ospitare più elementi.

    body: Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          GestureDetector(
            onTap: () => setState(() => _expanded = !_expanded),
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 500),
              curve: Curves.easeInOut,
              width: _expanded ? 250 : 120,
              height: _expanded ? 250 : 120,
              decoration: BoxDecoration(
                color: _expanded ? Colors.indigo : Colors.orange,
                borderRadius: BorderRadius.circular(_expanded ? 32 : 8),
              ),
              alignment: Alignment.center,
              child: const Text(
                'Tocca!',
                style: TextStyle(color: Colors.white, fontSize: 18),
              ),
            ),
          ),
          const SizedBox(height: 24),
          AnimatedOpacity(
            opacity: _expanded ? 1.0 : 0.0,
            duration: const Duration(milliseconds: 500),
            child: const Text(
              'Riquadro espanso! 🎉',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
          ),
        ],
      ),
    ),

    Risultato atteso

    Quando il riquadro si espande appare in dissolvenza il testo 'Riquadro espanso! 🎉', che svanisce quando il riquadro si rimpicciolisce.

  4. 4

    Animare la posizione con AnimatedAlign

    AnimatedAlign anima la proprietà alignment di un figlio all'interno di uno spazio definito. È perfetto per spostare un elemento da un angolo all'altro in modo fluido.

    Inseriamo un piccolo cerchio che si sposta tra l'alto e il basso a seconda dello stato _expanded. Lo collochiamo dentro un Container con altezza fissa per dare spazio all'animazione.

    Container(
      height: 150,
      width: double.infinity,
      margin: const EdgeInsets.symmetric(horizontal: 24),
      decoration: BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: BorderRadius.circular(12),
      ),
      child: AnimatedAlign(
        alignment: _expanded ? Alignment.bottomRight : Alignment.topLeft,
        duration: const Duration(milliseconds: 600),
        curve: Curves.elasticOut,
        child: Container(
          margin: const EdgeInsets.all(12),
          width: 40,
          height: 40,
          decoration: const BoxDecoration(
            color: Colors.redAccent,
            shape: BoxShape.circle,
          ),
        ),
      ),
    ),

    Risultato atteso

    Il cerchio rosso si sposta dall'angolo in alto a sinistra a quello in basso a destra con un rimbalzo elastico al cambio di stato.

  5. 5

    Animazioni personalizzate con TweenAnimationBuilder

    Quando nessun widget Animated* predefinito copre il caso d'uso, TweenAnimationBuilder permette di animare qualsiasi valore numerico. Si definisce un Tween (intervallo di valori) e una funzione builder che ricostruisce la UI a ogni frame.

    A differenza degli altri widget, TweenAnimationBuilder parte automaticamente l'animazione al primo build. Qui lo usiamo per animare un contatore che sale da 0 al valore finale e una rotazione.

    TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: _expanded ? 100 : 0),
      duration: const Duration(milliseconds: 800),
      curve: Curves.easeOut,
      builder: (context, value, child) {
        return Column(
          children: [
            Transform.rotate(
              angle: value / 100 * 6.28, // un giro completo
              child: const Icon(Icons.star, size: 48, color: Colors.amber),
            ),
            const SizedBox(height: 8),
            Text(
              'Progresso: ${value.toInt()}%',
              style: const TextStyle(fontSize: 16),
            ),
          ],
        );
      },
    ),

    Risultato atteso

    La stella ruota e il testo del progresso conta da 0 a 100% (o torna a 0) in modo animato quando si cambia lo stato.

  6. 6

    Buone pratiche e ottimizzazione

    Alcuni accorgimenti per usare al meglio le animazioni implicite:

    • Usa child nel builder: in TweenAnimationBuilder e widget simili, passa i sotto-widget statici tramite il parametro child per evitare di ricostruirli a ogni frame.
    • Scegli durate ragionevoli: tra 200 e 600 ms per le interazioni UI; durate troppo lunghe risultano lente.
    • Sperimenta con le Curves: easeInOut, elasticOut, bounceOut cambiano molto la percezione.
    • Animazioni implicite vs esplicite: se devi controllare manualmente play/pause, ripetizione o sincronizzare più animazioni, passa a AnimationController. Per semplici transizioni di stato, le implicite sono più pulite.
    • Evita di animare widget pesanti: ricostruzioni frequenti di liste o immagini grandi possono causare cali di frame.

    Di seguito un esempio di child riutilizzato per migliorare le prestazioni.

    TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: _expanded ? 1 : 0),
      duration: const Duration(milliseconds: 400),
      // Il widget statico viene costruito una sola volta
      child: const Icon(Icons.favorite, size: 48, color: Colors.pink),
      builder: (context, value, child) {
        return Opacity(
          opacity: value,
          child: Transform.scale(
            scale: 0.5 + value * 0.5,
            child: child, // riutilizziamo l'icona senza ricostruirla
          ),
        );
      },
    ),

    Risultato atteso

    L'icona del cuore appare scalando e dissolvendosi, mantenendo prestazioni ottimali grazie al riutilizzo del widget tramite il parametro child.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!