애니메이션 심화 — AnimationController, Tween, Curve

명시적 애니메이션은 AnimationController를 직접 제어하여 반복, 역방향, 시퀀스 등 세밀한 애니메이션을 구현합니다.


AnimationController 기본

DART
class PulseAnimation extends StatefulWidget {
  const PulseAnimation({super.key});

  @override
  State<PulseAnimation> createState() => _PulseAnimationState();
}

class _PulseAnimationState extends State<PulseAnimation>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,  // TickerProvider (프레임 동기화)
    );

    // 애니메이션 시작
    _controller.repeat(reverse: true);  // 반복 + 역방향
  }

  @override
  void dispose() {
    _controller.dispose();  // 반드시 해제!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.scale(
          scale: 1.0 + (_controller.value * 0.2),  // 1.0 ~ 1.2
          child: child,
        );
      },
      child: Container(
        width: 100,
        height: 100,
        decoration: const BoxDecoration(
          color: Colors.blue,
          shape: BoxShape.circle,
        ),
      ),
    );
  }
}

SingleTickerProviderStateMixin vs TickerProviderStateMixin

Mixin용도
SingleTickerProviderStateMixinAnimationController 1개일 때
TickerProviderStateMixinAnimationController 여러 개일 때

Tween — 값 범위 매핑

AnimationController는 0.0~1.0 사이 값을 생성합니다. Tween으로 원하는 범위로 변환합니다.

DART
class TweenExample extends StatefulWidget {
  const TweenExample({super.key});

  @override
  State<TweenExample> createState() => _TweenExampleState();
}

class _TweenExampleState extends State<TweenExample>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _sizeAnimation;
  late final Animation<Color?> _colorAnimation;
  late final Animation<double> _rotationAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    // 크기: 50 → 200
    _sizeAnimation = Tween<double>(
      begin: 50,
      end: 200,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.elasticOut,
    ));

    // 색상: 빨강 → 파랑
    _colorAnimation = ColorTween(
      begin: Colors.red,
      end: Colors.blue,
    ).animate(_controller);

    // 회전: 0 → 2π (360도)
    _rotationAnimation = Tween<double>(
      begin: 0,
      end: 2 * 3.14159,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Transform.rotate(
              angle: _rotationAnimation.value,
              child: Container(
                width: _sizeAnimation.value,
                height: _sizeAnimation.value,
                color: _colorAnimation.value,
              ),
            );
          },
        ),
        const SizedBox(height: 20),
        ElevatedButton(
          onPressed: () {
            if (_controller.isCompleted) {
              _controller.reverse();
            } else {
              _controller.forward();
            }
          },
          child: const Text('애니메이션 토글'),
        ),
      ],
    );
  }
}

AnimationController 제어 메서드

DART
_controller.forward();         // 정방향 재생
_controller.reverse();         // 역방향 재생
_controller.repeat();          // 반복
_controller.repeat(reverse: true); // 왕복 반복
_controller.stop();            // 정지
_controller.reset();           // 초기값으로 리셋
_controller.animateTo(0.5);    // 특정 값까지 애니메이션

// 상태 확인
_controller.isAnimating;       // 실행 중인지
_controller.isCompleted;       // 완료 상태인지
_controller.isDismissed;       // 초기 상태인지
_controller.value;             // 현재 값 (0.0 ~ 1.0)

상태 리스너

DART
_controller.addStatusListener((status) {
  switch (status) {
    case AnimationStatus.forward:
      print('정방향 재생 중');
    case AnimationStatus.completed:
      print('완료');
    case AnimationStatus.reverse:
      print('역방향 재생 중');
    case AnimationStatus.dismissed:
      print('초기 상태');
  }
});

시퀀스 애니메이션 (Staggered)

여러 애니메이션을 순차적으로 실행합니다.

DART
class StaggeredAnimation extends StatefulWidget {
  const StaggeredAnimation({super.key});

  @override
  State<StaggeredAnimation> createState() => _StaggeredAnimationState();
}

class _StaggeredAnimationState extends State<StaggeredAnimation>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _opacity;
  late final Animation<double> _width;
  late final Animation<double> _height;
  late final Animation<EdgeInsets> _padding;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    // 0% ~ 30%: 투명도
    _opacity = Tween(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.0, 0.3, curve: Curves.ease),
      ),
    );

    // 30% ~ 60%: 너비
    _width = Tween(begin: 50.0, end: 200.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.3, 0.6, curve: Curves.ease),
      ),
    );

    // 60% ~ 100%: 높이
    _height = Tween(begin: 50.0, end: 200.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.6, 1.0, curve: Curves.ease),
      ),
    );

    // 0% ~ 100%: 패딩
    _padding = EdgeInsetsTween(
      begin: const EdgeInsets.only(bottom: 16),
      end: const EdgeInsets.only(bottom: 75),
    ).animate(
      CurvedAnimation(parent: _controller, curve: Curves.ease),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Opacity(
          opacity: _opacity.value,
          child: Container(
            width: _width.value,
            height: _height.value,
            padding: _padding.value,
            color: Colors.blue,
          ),
        );
      },
    );
  }
}

TweenAnimationBuilder — 암시적 + 명시적 혼합

AnimationController 없이 Tween 애니메이션을 사용할 수 있습니다.

DART
TweenAnimationBuilder<double>(
  tween: Tween(begin: 0, end: _targetValue),
  duration: const Duration(milliseconds: 500),
  curve: Curves.easeOut,
  builder: (context, value, child) {
    return Transform.scale(
      scale: value,
      child: child,
    );
  },
  child: const Icon(Icons.star, size: 50, color: Colors.yellow),
)

실전: 로딩 스피너

DART
class CustomSpinner extends StatefulWidget {
  const CustomSpinner({super.key});

  @override
  State<CustomSpinner> createState() => _CustomSpinnerState();
}

class _CustomSpinnerState extends State<CustomSpinner>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    )..repeat();  // 계속 반복
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.rotate(
          angle: _controller.value * 2 * 3.14159,
          child: child,
        );
      },
      child: const Icon(Icons.refresh, size: 48),
    );
  }
}

정리

  • AnimationController로 애니메이션 시작, 정지, 반복, 역방향을 제어합니다
  • Tween으로 0.0~1.0 값을 원하는 범위로 변환합니다
  • CurvedAnimation으로 Curve(이징)를 적용합니다
  • Interval을 사용해 시퀀스 애니메이션을 구현할 수 있습니다
  • AnimatedBuilderaddListener + setState보다 성능이 좋습니다
  • dispose()에서 반드시 controller를 해제하세요
댓글 로딩 중...