Mobile

Flutter: Animasi & Transisi

TOKEN

Panduan lengkap animasi dan transisi di Flutter β€” dari implicit animations, explicit animations, Hero transitions, AnimatedContainer, hingga custom animation controllers dan curves

1. Pengenalan Animasi di Flutter

Animasi adalah elemen kunci untuk menciptakan pengalaman pengguna yang menarik dan responsif. Flutter menyediakan sistem animasi yang sangat powerful dan fleksibel β€” mulai dari implicit animations yang sederhana (cukup ubah nilai, animasi berjalan otomatis) hingga explicit animations yang memberikan kontrol penuh atas setiap frame.

Jenis Animasi di Flutter

JenisKontrolKompleksitasContoh Widget
Implicit (Implisit)Otomatis saat nilai berubah🟒 MudahAnimatedContainer, AnimatedOpacity
Explicit (Eksplisit)Full kontrol via AnimationController🟑 SedangAnimatedBuilder, AnimatedWidget
HeroOtomatis antar halaman🟒 MudahHero widget
Custom TransitionFull kontrol page routeπŸ”΄ LanjutPageRouteBuilder
Physics-basedSpring/damping simulation🟑 SedangSpringSimulation, GestureDetector
Diagram: Pipeline Animasi Flutter
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  ANIMATION PIPELINE                     β”‚
β”‚                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  Animation    │───▢│  Tween       │───▢│ Curved    β”‚ β”‚
β”‚  β”‚  Controller   β”‚    β”‚  (range)     β”‚    β”‚ Animation β”‚ β”‚
β”‚  β”‚  0.0 β†’ 1.0   β”‚    β”‚  0.0 β†’ 200.0 β”‚    β”‚ (easing)  β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚
β”‚         β”‚                                       β”‚       β”‚
β”‚         β–Ό                                       β–Ό       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  AnimatedBuilder / AnimatedWidget                β”‚   β”‚
β”‚  β”‚  β†’ Rebuild UI setiap frame (~60fps)              β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                          β–Ό                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Widget Tree (UI di-render ulang)                β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Implicit Animations (Animasi Implisit)

Implicit animations adalah jenis animasi termudah di Flutter. Cukup ubah nilai properti di dalam widget animasi, dan Flutter akan secara otomatis menganimasikan perubahan tersebut. Tidak perlu AnimationController atau setup khusus β€” Flutter menangani semuanya.

Dart β€” AnimatedContainer
import 'package:flutter/material.dart';

class AnimatedContainerDemo extends StatefulWidget {
  @override
  _AnimatedContainerDemoState createState() => _AnimatedContainerDemoState();
}

class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
  bool _expanded = false;
  bool _isCircle = false;
  Color _color = Colors.blue;
  double _width = 100;
  double _height = 100;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('AnimatedContainer')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // AnimatedContainer β€” Animasi otomatis saat properti berubah
            AnimatedContainer(
              duration: Duration(milliseconds: 500),
              curve: Curves.easeInOut,
              width: _width,
              height: _height,
              decoration: BoxDecoration(
                color: _color,
                borderRadius: BorderRadius.circular(
                  _isCircle ? 100 : 16,
                ),
                boxShadow: [
                  BoxShadow(
                    color: _color.withOpacity(0.4),
                    blurRadius: _expanded ? 20 : 8,
                    offset: Offset(0, _expanded ? 8 : 4),
                  ),
                ],
              ),
              child: Center(
                child: Icon(
                  _isCircle ? Icons.star : Icons.favorite,
                  color: Colors.white,
                  size: _expanded ? 48 : 32,
                ),
              ),
            ),
            SizedBox(height: 32),

            // Toggle buttons
            Wrap(
              spacing: 8,
              children: [
                ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _expanded = !_expanded;
                      _width = _expanded ? 200 : 100;
                      _height = _expanded ? 200 : 100;
                      _color = _expanded ? Colors.purple : Colors.blue;
                    });
                  },
                  child: Text(_expanded ? 'Kecilkan' : 'Perbesar'),
                ),
                ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _isCircle = !_isCircle;
                    });
                  },
                  child: Text(_isCircle ? 'Kotak' : 'Lingkaran'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
Dart β€” AnimatedOpacity & AnimatedPositioned
// ===== ANIMATED OPACITY =====
class AnimatedOpacityDemo extends StatefulWidget {
  @override
  _AnimatedOpacityDemoState createState() => _AnimatedOpacityDemoState();
}

class _AnimatedOpacityDemoState extends State<AnimatedOpacityDemo> {
  double _opacity = 1.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedOpacity(
              opacity: _opacity,
              duration: Duration(milliseconds: 800),
              curve: Curves.easeOut,
              child: Container(
                width: 200,
                height: 200,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    colors: [Colors.blue, Colors.purple],
                  ),
                  borderRadius: BorderRadius.circular(16),
                ),
                child: Center(
                  child: Text(
                    'Flutter',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 28,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
            ),
            SizedBox(height: 20),
            Slider(
              value: _opacity,
              onChanged: (value) => setState(() => _opacity = value),
              min: 0.0,
              max: 1.0,
            ),
          ],
        ),
      ),
    );
  }
}

// ===== ANIMATED POSITIONED (dalam Stack) =====
class AnimatedPositionedDemo extends StatefulWidget {
  @override
  _AnimatedPositionedDemoState createState() => _AnimatedPositionedDemoState();
}

class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
  bool _moved = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          AnimatedPositioned(
            duration: Duration(milliseconds: 600),
            curve: Curves.elasticOut,
            left: _moved ? 250 : 20,
            top: _moved ? 400 : 100,
            child: GestureDetector(
              onTap: () => setState(() => _moved = !_moved),
              child: Container(
                width: 80,
                height: 80,
                decoration: BoxDecoration(
                  color: Colors.orange,
                  shape: BoxShape.circle,
                ),
                child: Icon(Icons.touch_app, color: Colors.white),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// ===== ANIMATED CROSSFADE =====
class AnimatedCrossFadeDemo extends StatefulWidget {
  @override
  _AnimatedCrossFadeDemoState createState() => _AnimatedCrossFadeDemoState();
}

class _AnimatedCrossFadeDemoState extends State<AnimatedCrossFadeDemo> {
  bool _showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedCrossFade(
          firstChild: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
            child: Center(child: Text('Widget 1', style: TextStyle(color: Colors.white, fontSize: 20))),
          ),
          secondChild: Container(
            width: 200,
            height: 200,
            color: Colors.red,
            child: Center(child: Text('Widget 2', style: TextStyle(color: Colors.white, fontSize: 20))),
          ),
          crossFadeState: _showFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond,
          duration: Duration(milliseconds: 500),
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: () => setState(() => _showFirst = !_showFirst),
          child: Text('Toggle'),
        ),
      ],
    );
  }
}

3. Animated Widget Lengkap

Daftar Implicit Animation Widgets

WidgetYang DianimasikanPenggunaan Umum
AnimatedContainerSize, color, padding, margin, decorationKartu yang berubah ukuran/warna
AnimatedOpacityTransparansi (0.0-1.0)Fade in/out konten
AnimatedPositionedPosisi dalam StackMenggeser elemen
AnimatedPaddingPaddingSpacing berubah
AnimatedAlignAlignmentMemindahkan elemen ke posisi baru
AnimatedDefaultTextStyleStyle teksPerubahan font/warna teks
AnimatedPhysicalModelElevation, shadow, shapeCard elevation animation
AnimatedRotationRotasi (derajat)Icon yang berputar
AnimatedScaleSkalaZoom in/out
AnimatedSlideOffset (posisi relatif)Slide in/out
AnimatedSwitcherSwitch antar child widgetGanti konten dengan animasi
AnimatedCrossFadeCross-fade antar dua childToggle antar widget
TweenAnimationBuilderProperti apapun via TweenAnimasi custom tanpa controller
Dart β€” TweenAnimationBuilder
// TweenAnimationBuilder β€” Implicit animation untuk properti custom
class TweenAnimationDemo extends StatefulWidget {
  @override
  _TweenAnimationDemoState createState() => _TweenAnimationDemoState();
}

class _TweenAnimationDemoState extends State<TweenAnimationDemo> {
  double _targetValue = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Animate number counter
            TweenAnimationBuilder<double>(
              tween: Tween(begin: 0, end: _targetValue),
              duration: Duration(seconds: 2),
              curve: Curves.easeOutCubic,
              builder: (context, value, child) {
                return Text(
                  '${value.toStringAsFixed(0)}%',
                  style: TextStyle(
                    fontSize: 72,
                    fontWeight: FontWeight.bold,
                    color: Colors.blue,
                  ),
                );
              },
            ),
            SizedBox(height: 20),

            // Animate gradient rotation
            TweenAnimationBuilder<double>(
              tween: Tween(begin: 0, end: _targetValue / 100),
              duration: Duration(milliseconds: 800),
              builder: (context, value, child) {
                return Container(
                  width: 150,
                  height: 150,
                  decoration: BoxDecoration(
                    gradient: SweepGradient(
                      colors: [
                        Colors.blue,
                        Colors.purple,
                        Colors.blue,
                      ],
                      stops: [0, value, 1],
                    ),
                    shape: BoxShape.circle,
                  ),
                  child: Center(
                    child: Text(
                      '${(value * 100).toInt()}%',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 24,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                );
              },
            ),
            SizedBox(height: 30),

            // Slider
            Slider(
              value: _targetValue,
              onChanged: (v) => setState(() => _targetValue = v),
              min: 0,
              max: 100,
              divisions: 10,
            ),
          ],
        ),
      ),
    );
  }
}

// AnimatedSwitcher β€” Animasi pergantian widget
class AnimatedSwitcherDemo extends StatefulWidget {
  @override
  _AnimatedSwitcherDemoState createState() => _AnimatedSwitcherDemoState();
}

class _AnimatedSwitcherDemoState extends State<AnimatedSwitcherDemo> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedSwitcher(
          duration: Duration(milliseconds: 400),
          transitionBuilder: (child, animation) {
            return ScaleTransition(
              scale: animation,
              child: child,
            );
          },
          child: Text(
            '$_count',
            key: ValueKey(_count),  // Key penting untuk AnimatedSwitcher!
            style: TextStyle(fontSize: 80, fontWeight: FontWeight.bold),
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IconButton(
              icon: Icon(Icons.remove, size: 32),
              onPressed: () => setState(() => _count--),
            ),
            SizedBox(width: 32),
            IconButton(
              icon: Icon(Icons.add, size: 32),
              onPressed: () => setState(() => _count++),
            ),
          ],
        ),
      ],
    );
  }
}

4. Explicit Animations (Animasi Eksplisit)

Explicit animations memberikan kontrol penuh atas animasi menggunakan AnimationController. Anda bisa mengontrol durasi, kecepatan, arah (forward/reverse), dan bahkan menghentikan animasi di tengah jalan. Cocok untuk animasi yang memerlukan interaksi pengguna atau timing yang presisi.

Dart β€” Explicit Animation Basics
import 'package:flutter/material.dart';

class ExplicitAnimationDemo extends StatefulWidget {
  @override
  _ExplicitAnimationDemoState createState() => _ExplicitAnimationDemoState();
}

// Perlu SingleTickerProviderStateMixin untuk AnimationController
class _ExplicitAnimationDemoState extends State<ExplicitAnimationDemo>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _rotationAnimation;

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

    // AnimationController β€” engine dari animasi
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,  // Menggunakan TickerProvider dari mixin
    );

    // Tween β€” menentukan range nilai
    _scaleAnimation = Tween<double>(begin: 0.5, end: 1.5)
        .animate(CurvedAnimation(
          parent: _controller,
          curve: Curves.elasticOut,
        ));

    _colorAnimation = ColorTween(begin: Colors.blue, end: Colors.purple)
        .animate(CurvedAnimation(
          parent: _controller,
          curve: Interval(0.0, 0.5, curve: Curves.easeIn),
        ));

    _rotationAnimation = Tween<double>(begin: 0, end: 2 * 3.14159)
        .animate(CurvedAnimation(
          parent: _controller,
          curve: Curves.easeInOut,
        ));
  }

  @override
  void dispose() {
    _controller.dispose();  // SELALU dispose controller!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Explicit Animation')),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Transform.rotate(
              angle: _rotationAnimation.value,
              child: Transform.scale(
                scale: _scaleAnimation.value,
                child: Container(
                  width: 150,
                  height: 150,
                  decoration: BoxDecoration(
                    color: _colorAnimation.value,
                    borderRadius: BorderRadius.circular(16),
                    boxShadow: [
                      BoxShadow(
                        color: (_colorAnimation.value ?? Colors.blue)
                            .withOpacity(0.5),
                        blurRadius: 20 * _scaleAnimation.value,
                      ),
                    ],
                  ),
                  child: Center(
                    child: Icon(Icons.flutter_dash, color: Colors.white, size: 60),
                  ),
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'forward',
            onPressed: () => _controller.forward(),
            child: Icon(Icons.play_arrow),
          ),
          SizedBox(width: 12),
          FloatingActionButton(
            heroTag: 'reverse',
            onPressed: () => _controller.reverse(),
            child: Icon(Icons.replay),
          ),
          SizedBox(width: 12),
          FloatingActionButton(
            heroTag: 'reset',
            onPressed: () => _controller.reset(),
            child: Icon(Icons.stop),
          ),
        ],
      ),
    );
  }
}

5. Animation Controller & Tween

AnimationController adalah kelas utama yang menggerakkan semua explicit animations. Controller menghasilkan nilai linear dari lowerBound (default 0.0) ke upperBound (default 1.0) dalam durasi yang ditentukan.

Dart β€” Controller Methods
// AnimationController β€” Semua method yang tersedia
_controller.forward()        // Mulai dari 0 β†’ 1
_controller.forward(from: 0.5) // Mulai dari 0.5
_controller.reverse()        // Mundur dari posisi sekarang β†’ 0
_controller.reverse(from: 0.8) // Mundur dari 0.8
_controller.reset()          // Reset ke 0
_controller.stop()           // Hentikan di posisi sekarang
_controller.repeat()         // Ulangi terus (0β†’1β†’0β†’1...)
_controller.repeat(reverse: true)  // Ulangi dengan reverse (0β†’1β†’0β†’1...)
_controller.animateTo(0.7)   // Animasi ke 0.7
_controller.animateBack(0.3) // Animasi mundur ke 0.3
_controller.value            // Nilai saat ini (0.0-1.0)
_controller.isAnimating      // Sedang animasi?
_controller.isCompleted      // Sudah selesai?
_controller.isDismissed      // Sudah di 0?

// Mendengarkan perubahan
_controller.addListener(() {
  print('Value: ${_controller.value}');
});

_controller.addStatusListener((status) {
  switch (status) {
    case AnimationStatus.forward:
      print('Animasi berjalan maju');
      break;
    case AnimationStatus.reverse:
      print('Animasi berjalan mundur');
      break;
    case AnimationStatus.completed:
      print('Animasi selesai');
      // Bisa reverse otomatis untuk loop:
      // _controller.reverse();
      break;
    case AnimationStatus.dismissed:
      print('Animasi di posisi awal');
      break;
  }
});

// ===== BERBAGAI JENIS TWEEN =====

// Tween<double> β€” untuk angka
Tween<double>(begin: 0, end: 100)

// ColorTween β€” untuk warna
ColorTween(begin: Colors.red, end: Colors.blue)

// IntTween β€” untuk integer
IntTween(begin: 0, end: 255)

// AlignmentTween β€” untuk alignment
AlignmentTween(begin: Alignment.topLeft, end: Alignment.bottomRight)

// EdgeInsetsTween β€” untuk padding/margin
EdgeInsetsTween(
  begin: EdgeInsets.all(8),
  end: EdgeInsets.all(32),
)

// BorderRadiusTween β€” untuk border radius
BorderRadiusTween(
  begin: BorderRadius.circular(0),
  end: BorderRadius.circular(50),
)

// SizeTween β€” untuk ukuran
SizeTween(
  begin: Size(100, 100),
  end: Size(200, 200),
)

// RectTween β€” untuk rect
RectTween(
  begin: Rect.fromLTWH(0, 0, 100, 100),
  end: Rect.fromLTWH(50, 50, 200, 200),
)

// DecorationTween β€” untuk dekorasi
DecorationTween(
  begin: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(0)),
  end: BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(50)),
)

// Matrix4Tween β€” untuk transform
Matrix4Tween(
  begin: Matrix4.identity(),
  end: Matrix4.identity()..rotateZ(3.14159),
)

// Chained Tween (berantai)
TweenSequence<double>([
  TweenSequenceItem(tween: Tween(begin: 0, end: 100), weight: 40),
  TweenSequenceItem(tween: Tween(begin: 100, end: 50), weight: 30),
  TweenSequenceItem(tween: Tween(begin: 50, end: 200), weight: 30),
])
πŸ’‘ AnimatedBuilder vs AnimatedWidget

AnimatedBuilder digunakan ketika Anda ingin membangun widget tree baru setiap frame. AnimatedWidget digunakan ketika Anda ingin membuat class widget yang bisa di-animasi β€” lebih reusable. Untuk kebanyakan kasus, AnimatedBuilder sudah cukup dan lebih fleksibel.

6. Curves & Easing

Curves menentukan bagaimana nilai animasi berubah seiring waktu (easing function). Tanpa curve, animasi berjalan linear (konstan). Dengan curve, animasi bisa dipercepat, diperlambat, bahkan memantul.

CurveKarakteristikCocok Untuk
Curves.linearKonstan, tanpa easingProgress bar
Curves.easeInLambat di awal, cepat di akhirObjek jatuh
Curves.easeOutCepat di awal, lambat di akhirMenu muncul
Curves.easeInOutLambat-cepat-lambatTransisi halaman
Curves.bounceOutMemantul di akhirObjek jatuh & memantul
Curves.bounceInMemantul di awalEfek khusus
Curves.elasticOutElastis/memantulPop-up, tooltip
Curves.elasticInOutElastis di awal & akhirInteraksi playful
Curves.decelerateMelambat tajamScroll physics
Curves.fastOutSlowInMaterial Design standardAnimasi Material
Dart β€” Custom Curves & Interval
// Custom curve β€” Cubic Bezier
const customCurve = Cubic(0.68, -0.55, 0.265, 1.55);  // Overshoot

// Interval β€” Membagi durasi untuk sequenced animation
// Interval(begin, end, curve) β†’ animasi hanya berjalan di range begin-end
final opacityAnimation = Tween<double>(begin: 0, end: 1).animate(
  CurvedAnimation(
    parent: _controller,
    curve: Interval(0.0, 0.5, curve: Curves.easeIn),
    // Fade in selama 50% pertama durasi
  ),
);

final slideAnimation = Tween<Offset>(
  begin: Offset(0, 0.5),
  end: Offset.zero,
).animate(
  CurvedAnimation(
    parent: _controller,
    curve: Interval(0.3, 0.8, curve: Curves.easeOut),
    // Slide masuk selama 30%-80% durasi
  ),
);

final scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
  CurvedAnimation(
    parent: _controller,
    curve: Interval(0.5, 1.0, curve: Curves.elasticOut),
    // Scale di 50%-100% durasi
  ),
);

// Menggunakan di AnimatedBuilder
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return FadeTransition(
      opacity: opacityAnimation,
      child: SlideTransition(
        position: slideAnimation,
        child: ScaleTransition(
          scale: scaleAnimation,
          child: child,
        ),
      ),
    );
  },
  child: MyCard(),  // Child tidak rebuild β€” optimisasi!
)

7. Hero Animations

Hero animation (shared element transition) adalah animasi di mana widget berpindah dari satu halaman ke halaman lain dengan animasi yang mulus. Ini adalah pola UI yang sangat umum di mobile apps β€” ketika user menekan item di list, item tersebut "terbang" ke halaman detail.

Dart β€” Hero Animation
import 'package:flutter/material.dart';

// ===== HERO ANIMATION DASAR =====

// Halaman 1: List Produk
class ProductListPage extends StatelessWidget {
  final List<Map<String, dynamic>> products = [
    {'id': '1', 'name': 'iPhone 16 Pro', 'price': 'Rp 18.999.000', 'icon': Icons.phone_iphone, 'color': Colors.blue},
    {'id': '2', 'name': 'MacBook Air M3', 'price': 'Rp 16.999.000', 'icon': Icons.laptop_mac, 'color': Colors.grey},
    {'id': '3', 'name': 'iPad Pro M4', 'price': 'Rp 14.999.000', 'icon': Icons.tablet_mac, 'color': Colors.purple},
    {'id': '4', 'name': 'AirPods Pro 3', 'price': 'Rp 3.999.000', 'icon': Icons.headphones, 'color': Colors.green},
    {'id': '5', 'name': 'Apple Watch Ultra', 'price': 'Rp 12.999.000', 'icon': Icons.watch, 'color': Colors.orange},
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Produk')),
      body: ListView.builder(
        padding: EdgeInsets.all(16),
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => ProductDetailPage(product: product),
                ),
              );
            },
            child: Card(
              margin: EdgeInsets.only(bottom: 12),
              child: Padding(
                padding: EdgeInsets.all(16),
                child: Row(
                  children: [
                    // HERO WIDGET β€” bagian yang di-animasi
                    Hero(
                      tag: 'product-${product['id']}',  // Tag harus unik & sama di kedua halaman
                      child: Container(
                        width: 60,
                        height: 60,
                        decoration: BoxDecoration(
                          color: product['color'].withOpacity(0.2),
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Icon(product['icon'], color: product['color'], size: 32),
                      ),
                    ),
                    SizedBox(width: 16),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(product['name'], style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
                          SizedBox(height: 4),
                          Text(product['price'], style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold)),
                        ],
                      ),
                    ),
                    Icon(Icons.arrow_forward_ios, color: Colors.grey),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

// Halaman 2: Detail Produk
class ProductDetailPage extends StatelessWidget {
  final Map<String, dynamic> product;

  ProductDetailPage({required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(product['name'])),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Center(
              // HERO WIDGET β€” tag sama dengan di list
              Hero(
                tag: 'product-${product['id']}',
                child: Container(
                  width: 200,  // Lebih besar di halaman detail
                  height: 200,
                  decoration: BoxDecoration(
                    color: product['color'].withOpacity(0.2),
                    borderRadius: BorderRadius.circular(24),
                  ),
                  child: Icon(product['icon'], color: product['color'], size: 80),
                ),
              ),
            ),
            SizedBox(height: 32),
            Text(product['name'], style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
            SizedBox(height: 8),
            Text(product['price'], style: TextStyle(fontSize: 24, color: Colors.blue, fontWeight: FontWeight.w600)),
            SizedBox(height: 24),
            Text('Deskripsi produk akan ditampilkan di sini...', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
            Spacer(),
            SizedBox(
              width: double.infinity,
              height: 56,
              child: ElevatedButton(
                onPressed: () {},
                style: ElevatedButton.styleFrom(
                  backgroundColor: product['color'],
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
                ),
                child: Text('Beli Sekarang', style: TextStyle(fontSize: 18, color: Colors.white)),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ===== HERO DENGAN CUSTOM FLIGHT SHUTTLE =====
// Untuk mengontrol bagaimana Hero berpindah
Hero(
  tag: 'custom-hero',
  flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
    return ScaleTransition(
      scale: animation.drive(Tween(begin: 0.5, end: 1.0)),
      child: RotationTransition(
        turns: animation,
        child: toContext.widget,
      ),
    );
  },
  child: MyWidget(),
)

8. Custom Page Transitions

Dart β€” Custom Page Transitions
import 'package:flutter/material.dart';

// ===== CUSTOM PAGE TRANSITIONS =====

// Slide dari kanan
class SlideRightRoute extends PageRouteBuilder {
  final Widget page;

  SlideRightRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            var tween = Tween(begin: Offset(1.0, 0.0), end: Offset.zero)
                .chain(CurveTween(curve: Curves.easeInOut));
            return SlideTransition(
              position: animation.drive(tween),
              child: child,
            );
          },
          transitionDuration: Duration(milliseconds: 400),
        );
}

// Fade transition
class FadeRoute extends PageRouteBuilder {
  final Widget page;

  FadeRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return FadeTransition(
              opacity: animation,
              child: child,
            );
          },
        );
}

// Scale + Fade transition (zoom in)
class ScaleFadeRoute extends PageRouteBuilder {
  final Widget page;

  ScaleFadeRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            var fadeAnimation = Tween(begin: 0.0, end: 1.0).animate(
              CurvedAnimation(parent: animation, curve: Interval(0.0, 0.5)),
            );
            var scaleAnimation = Tween(begin: 0.8, end: 1.0).animate(
              CurvedAnimation(parent: animation, curve: Curves.easeOutBack),
            );
            return FadeTransition(
              opacity: fadeAnimation,
              child: ScaleTransition(
                scale: scaleAnimation,
                child: child,
              ),
            );
          },
        );
}

// Rotation + Scale transition
class RotationRoute extends PageRouteBuilder {
  final Widget page;

  RotationRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return RotationTransition(
              turns: Tween(begin: 0.5, end: 1.0).animate(
                CurvedAnimation(parent: animation, curve: Curves.easeOut),
              ),
              child: ScaleTransition(
                scale: Tween(begin: 0.0, end: 1.0).animate(
                  CurvedAnimation(parent: animation, curve: Curves.easeOut),
                ),
                child: child,
              ),
            );
          },
        );
}

// Menggunakan custom transitions
Navigator.push(context, SlideRightRoute(page: DetailPage()));
Navigator.push(context, FadeRoute(page: ProfilePage()));
Navigator.push(context, ScaleFadeRoute(page: SettingsPage()));

9. Staggered Animations

Staggered animation adalah serangkaian animasi yang berjalan secara berurutan dengan timing yang berbeda-beda β€” misalnya elemen pertama muncul, lalu elemen kedua, lalu ketiga. Ini menciptakan efek yang lebih natural dan menarik dibanding semua elemen animasi bersamaan.

Dart β€” Staggered Animation
import 'package:flutter/material.dart';

class StaggeredAnimationDemo extends StatefulWidget {
  @override
  _StaggeredAnimationDemoState createState() => _StaggeredAnimationDemoState();
}

class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo>
    with TickerProviderStateMixin {

  late AnimationController _controller;
  late Animation<double> _headerAnimation;
  late Animation<double> _card1Animation;
  late Animation<double> _card2Animation;
  late Animation<double> _card3Animation;
  late Animation<double> _buttonAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 2000),
      vsync: this,
    );

    // Header: 0% - 30% durasi
    _headerAnimation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.0, 0.3, curve: Curves.easeOut),
      ),
    );

    // Card 1: 15% - 45% durasi
    _card1Animation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.15, 0.45, curve: Curves.easeOut),
      ),
    );

    // Card 2: 30% - 60% durasi
    _card2Animation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.30, 0.60, curve: Curves.easeOut),
      ),
    );

    // Card 3: 45% - 75% durasi
    _card3Animation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.45, 0.75, curve: Curves.easeOut),
      ),
    );

    // Button: 60% - 100% durasi
    _buttonAnimation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.60, 1.0, curve: Curves.elasticOut),
      ),
    );
  }

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

  Widget _buildAnimatedCard({
    required Animation<double> animation,
    required String title,
    required String subtitle,
    required IconData icon,
    required Color color,
  }) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        return Transform.translate(
          offset: Offset(0, 50 * (1 - animation.value)),
          child: Opacity(
            opacity: animation.value,
            child: child,
          ),
        );
      },
      child: Card(
        margin: EdgeInsets.only(bottom: 12),
        child: ListTile(
          leading: CircleAvatar(
            backgroundColor: color.withOpacity(0.2),
            child: Icon(icon, color: color),
          ),
          title: Text(title),
          subtitle: Text(subtitle),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Staggered Animation')),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Header β€” muncul duluan
            AnimatedBuilder(
              animation: _headerAnimation,
              builder: (context, child) {
                return Transform.translate(
                  offset: Offset(-100 * (1 - _headerAnimation.value), 0),
                  child: Opacity(
                    opacity: _headerAnimation.value,
                    child: child,
                  ),
                );
              },
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Dashboard', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
                  Text('Selamat datang kembali, Budi!', style: TextStyle(color: Colors.grey)),
                ],
              ),
            ),
            SizedBox(height: 24),

            // Cards β€” muncul berurutan
            _buildAnimatedCard(
              animation: _card1Animation,
              title: 'Total Pesanan',
              subtitle: '156 pesanan',
              icon: Icons.shopping_bag,
              color: Colors.blue,
            ),
            _buildAnimatedCard(
              animation: _card2Animation,
              title: 'Pendapatan',
              subtitle: 'Rp 12.500.000',
              icon: Icons.attach_money,
              color: Colors.green,
            ),
            _buildAnimatedCard(
              animation: _card3Animation,
              title: 'Pelanggan Baru',
              subtitle: '48 pelanggan',
              icon: Icons.people,
              color: Colors.purple,
            ),
            SizedBox(height: 24),

            // Button β€” muncul terakhir dengan efek elastic
            AnimatedBuilder(
              animation: _buttonAnimation,
              builder: (context, child) {
                return Transform.scale(
                  scale: _buttonAnimation.value,
                  child: child,
                );
              },
              child: SizedBox(
                width: double.infinity,
                height: 50,
                child: ElevatedButton(
                  onPressed: () {},
                  child: Text('Lihat Detail'),
                ),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.isCompleted) {
            _controller.reverse();
          } else {
            _controller.forward();
          }
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

10. Best Practices & Performa

Best PracticePenjelasan
Selalu dispose()AnimationController harus di-dispose di method dispose() untuk menghindari memory leak
Gunakan AnimatedBuilderPisahkan widget yang animasi dari yang statis β€” gunakan child parameter
Pilih tepatImplicit untuk animasi sederhana, explicit untuk kontrol penuh
Durasi yang tepat200-500ms untuk micro-interactions, 500-1000ms untuk transisi halaman
CurvesHindari linear untuk kebanyakan animasi β€” gunakan easeInOut atau curves lainnya
60fpsPastikan animasi berjalan 60fps β€” hindari operasi berat di builder
Reduce rebuildGunakan child parameter di AnimatedBuilder untuk widget yang tidak berubah
AccessibilityBerikan opsi untuk menonaktifkan animasi bagi user yang sensitif
Dart β€” Performance Optimization
// ❌ BURUK: Semua widget rebuild setiap frame
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return Container(
      decoration: BoxDecoration(color: Colors.blue),
      child: Column(
        children: [
          Text('Title'),  // Ini ikut rebuild padahal statis!
          ExpensiveWidget(),  // Ini juga!
          Transform.scale(
            scale: _animation.value,
            child: Icon(Icons.star),
          ),
        ],
      ),
    );
  },
)

// βœ… BAIK: Hanya bagian yang berubah yang rebuild
Container(
  decoration: BoxDecoration(color: Colors.blue),
  child: Column(
    children: [
      Text('Title'),  // Tidak rebuild β€” di luar AnimatedBuilder
      ExpensiveWidget(),  // Tidak rebuild
      AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return Transform.scale(
            scale: _animation.value,
            child: child,  // Menggunakan child β€” tidak rebuild!
          );
        },
        child: Icon(Icons.star),  // Child statis β€” dibuat sekali saja
      ),
    ],
  ),
)

// βœ… BAHKAN LEBIH BAIK: Gunakan built-in transition widget
FadeTransition(
  opacity: _fadeAnimation,
  child: ScaleTransition(
    scale: _scaleAnimation,
    child: Icon(Icons.star),  // Dibuat sekali, di-reuse setiap frame
  ),
)
πŸ’‘ Reduce Motion / Accessibility

Beberapa pengguna memiliki sensitivitas terhadap gerakan (vestibular disorders). Flutter mendeteksi MediaQuery.disableAnimations β€” periksa ini dan kurangi atau hilangkan animasi untuk pengguna tersebut. Gunakan MediaQuery.of(context).disableAnimations untuk memeriksa preferensi ini.

11. Quiz Pemahaman

1. Apa perbedaan implicit dan explicit animation?

2. Mengapa AnimationController harus di-dispose?

3. Apa fungsi Hero widget di Flutter?

4. Apa perbedaan Curves.easeIn dan Curves.easeOut?

5. Apa itu staggered animation?

πŸŽ‰ Selamat!

Anda telah mempelajari sistem animasi Flutter secara mendalam β€” dari implicit animations yang sederhana, explicit animations yang powerful, Hero transitions, custom page transitions, hingga staggered animations. Animasi yang baik membuat aplikasi terasa hidup dan profesional!

πŸ” Zoom
100%
🎨 Tema