🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Flutter Animation 중급 실전 - 슬라이드 1/11
A

AI Generated

2025. 10. 30. · 31 Views

Flutter Animation 실전 완벽 가이드

Flutter에서 애니메이션을 구현하는 실전 기법을 배워봅니다. 기본 암시적 애니메이션부터 복잡한 명시적 애니메이션, 그리고 Hero 애니메이션까지 실무에서 바로 활용할 수 있는 다양한 기법을 다룹니다.


목차

  1. 암시적 애니메이션 - AnimatedContainer로 시작하기
  2. 명시적 애니메이션 - AnimationController 마스터하기
  3. Tween 애니메이션 - 값의 범위 지정하기
  4. Hero 애니메이션 - 화면 전환의 마법
  5. 스태거드 애니메이션 - 순차적 효과의 예술
  6. 커스텀 애니메이션 - CustomPainter로 그리기
  7. 물리 기반 애니메이션 - SpringSimulation 활용
  8. 암시적 애니메이션 위젯들 - 빠른 적용을 위한 도구들

1. 암시적 애니메이션 - AnimatedContainer로 시작하기

시작하며

여러분이 Flutter로 앱을 만들 때 버튼을 눌렀을 때 부드럽게 크기가 변하거나 색상이 전환되는 효과를 구현하고 싶었던 적 있나요? 많은 초보 개발자들이 setState()만으로 값을 바꾸면서 왜 애니메이션이 안 생기는지 고민합니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 사용자 경험을 높이기 위해서는 단순한 상태 변화가 아니라 부드러운 전환 효과가 필요한데, 복잡한 애니메이션 코드를 작성하는 것은 부담스럽죠.

바로 이럴 때 필요한 것이 AnimatedContainer입니다. 이 위젯을 사용하면 별도의 애니메이션 컨트롤러 없이도 프로퍼티 변경만으로 자동으로 부드러운 애니메이션 효과를 만들 수 있습니다.

개요

간단히 말해서, AnimatedContainer는 일반 Container의 프로퍼티가 변경될 때 자동으로 애니메이션을 적용해주는 위젯입니다. width, height, color, padding, margin 등 거의 모든 Container 속성이 변경될 때 지정한 duration 동안 부드럽게 전환됩니다.

예를 들어, 사용자가 카드를 선택했을 때 강조 효과를 주거나, 버튼 상태에 따라 모양을 변경하는 경우에 매우 유용합니다. 기존에는 AnimationController를 생성하고 Tween을 설정하고 addListener를 통해 setState를 호출하는 복잡한 과정이 필요했다면, 이제는 단순히 AnimatedContainer의 속성 값만 바꿔주면 됩니다.

이 위젯의 핵심 특징은 첫째, 암시적(Implicit) 애니메이션이라 별도의 컨트롤러가 필요 없고, 둘째, 커스터마이징 가능한 curve와 duration을 제공하며, 셋째, onEnd 콜백으로 애니메이션 완료 시점을 감지할 수 있다는 점입니다. 이러한 특징들이 개발 생산성을 크게 높여주고 코드를 간결하게 만들어줍니다.

코드 예제

class AnimatedBox extends StatefulWidget {
  @override
  _AnimatedBoxState createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _isExpanded = !_isExpanded),
      // AnimatedContainer가 프로퍼티 변경을 자동으로 애니메이션화
      child: AnimatedContainer(
        duration: Duration(milliseconds: 500),
        curve: Curves.easeInOut,
        width: _isExpanded ? 200 : 100,
        height: _isExpanded ? 200 : 100,
        color: _isExpanded ? Colors.blue : Colors.red,
        child: Center(child: Text('탭하세요')),
      ),
    );
  }
}

설명

이것이 하는 일: 사용자가 위젯을 탭할 때마다 크기와 색상이 부드럽게 변경되는 인터랙티브한 박스를 만듭니다. 첫 번째로, StatefulWidget을 사용하여 _isExpanded라는 상태를 관리합니다.

이 bool 값이 위젯의 확장/축소 상태를 결정하죠. GestureDetector로 탭 이벤트를 감지하고 setState를 호출하여 상태를 토글합니다.

이렇게 하는 이유는 Flutter가 상태 변경을 감지하고 다시 빌드할 수 있도록 하기 위함입니다. 그 다음으로, AnimatedContainer가 실행되면서 _isExpanded 값에 따라 width, height, color 속성이 결정됩니다.

내부적으로 AnimatedContainer는 이전 값과 새로운 값 사이의 차이를 계산하고, duration(500ms) 동안 Curves.easeInOut 곡선을 따라 중간 값들을 생성합니다. 이 과정이 초당 60프레임으로 진행되면서 부드러운 애니메이션이 만들어집니다.

curve 속성은 애니메이션의 느낌을 결정하는 매우 중요한 요소입니다. Curves.easeInOut은 시작과 끝이 부드럽고 중간이 빠른 자연스러운 움직임을 만들어냅니다.

Curves.bounceOut을 사용하면 튕기는 효과를, Curves.elasticInOut을 사용하면 탄성 효과를 낼 수 있죠. 마지막으로, 모든 프레임이 렌더링되면서 최종적으로 목표 값에 도달하게 됩니다.

만약 애니메이션 중간에 다시 탭하면 AnimatedContainer는 현재 위치에서 새로운 목표 값으로 다시 애니메이션을 시작합니다. 여러분이 이 코드를 사용하면 복잡한 애니메이션 로직 없이도 전문적인 UI/UX를 구현할 수 있습니다.

특히 카드 선택, 버튼 상태 변경, 드롭다운 확장 등 다양한 인터랙션에 즉시 적용할 수 있고, 코드 가독성도 뛰어나며, 유지보수도 쉽습니다.

실전 팁

💡 duration은 300-500ms가 가장 자연스럽습니다. 너무 짧으면 급작스럽고, 너무 길면 답답하게 느껴집니다.

💡 여러 속성을 동시에 변경할 때는 모두 같은 duration을 사용하세요. 서로 다른 duration은 시각적으로 불안정해 보입니다.

💡 Curves.linear는 기계적으로 보이므로 피하고, easeInOut, fastOutSlowIn 같은 자연스러운 곡선을 사용하세요.

💡 onEnd 콜백을 활용하면 애니메이션 완료 후 다음 동작을 연결할 수 있습니다. 예: AnimatedContainer(..., onEnd: () => _showNextStep())

💡 성능을 위해 const 생성자를 사용할 수 없지만, 자식 위젯은 const로 만들어 불필요한 재빌드를 방지하세요.


2. 명시적 애니메이션 - AnimationController 마스터하기

시작하며

여러분이 좀 더 복잡한 애니메이션을 만들려고 할 때, 예를 들어 여러 위젯이 순차적으로 나타나거나, 무한 반복되는 로딩 스피너를 만들어야 할 때 AnimatedContainer만으로는 한계를 느끼셨나요? 이런 상황은 실무에서 매우 자주 발생합니다.

사용자 인증 플로우에서 단계별로 페이드인되는 폼, 무한 회전하는 새로고침 아이콘, 파도치듯 움직이는 배경 등은 모두 정밀한 제어가 필요한 애니메이션입니다. 암시적 애니메이션은 이런 복잡한 시나리오를 처리할 수 없습니다.

바로 이럴 때 필요한 것이 AnimationController입니다. 이 컨트롤러를 사용하면 애니메이션의 시작, 정지, 반복, 역재생 등을 완벽하게 제어할 수 있고, 여러 애니메이션을 조합하여 복잡한 효과를 만들 수 있습니다.

개요

간단히 말해서, AnimationController는 0.0에서 1.0 사이의 값을 시간에 따라 변화시키는 엔진입니다. 이 컨트롤러는 vsync와 duration을 필수로 요구하며, forward(), reverse(), repeat() 같은 메서드로 애니메이션을 직접 제어할 수 있습니다.

예를 들어, 스플래시 스크린에서 로고가 회전하면서 페이드인하는 효과나, 게임에서 캐릭터가 점프하는 애니메이션 같은 경우에 필수적입니다. 기존 암시적 애니메이션이 "무엇"이 변할지만 지정했다면, 명시적 애니메이션은 "언제", "어떻게", "얼마나 오래", "몇 번" 변할지까지 모두 제어할 수 있습니다.

핵심 특징은 첫째, TickerProviderStateMixin과 함께 사용하여 화면이 보이지 않을 때 애니메이션을 자동으로 일시정지해 배터리를 절약하고, 둘째, addListener로 매 프레임마다 값의 변화를 감지할 수 있으며, 셋째, addStatusListener로 애니메이션의 시작/완료/취소 상태를 추적할 수 있다는 점입니다. 이러한 특징들이 정교한 애니메이션 구현을 가능하게 합니다.

코드 예제

class RotatingLogo extends StatefulWidget {
  @override
  _RotatingLogoState createState() => _RotatingLogoState();
}

// vsync를 제공하기 위해 SingleTickerProviderStateMixin 추가
class _RotatingLogoState extends State<RotatingLogo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    // 2초 동안 0.0에서 1.0으로 변하는 컨트롤러 생성
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    )..repeat(); // 무한 반복
  }

  @override
  void dispose() {
    _controller.dispose(); // 메모리 누수 방지
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      // 회전 변환 적용: 0.0~1.0 값을 0~2π 라디안으로 변환
      builder: (context, child) => Transform.rotate(
        angle: _controller.value * 2 * 3.14159,
        child: child,
      ),
      child: FlutterLogo(size: 100),
    );
  }
}

설명

이것이 하는 일: Flutter 로고가 무한히 회전하는 애니메이션을 만듭니다. 로딩 인디케이터나 대기 화면에 사용하기 좋습니다.

첫 번째로, SingleTickerProviderStateMixin을 State 클래스에 믹스인합니다. 이것은 vsync 파라미터에 this를 전달하기 위해 필수입니다.

vsync는 화면 주사율(보통 60Hz)과 동기화하여 애니메이션을 부드럽게 만들고, 앱이 백그라운드에 있을 때 애니메이션을 자동으로 멈춰 배터리를 절약합니다. 여러 컨트롤러가 필요하면 SingleTickerProviderStateMixin 대신 TickerProviderStateMixin을 사용하면 됩니다.

그 다음으로, initState에서 AnimationController를 생성합니다. duration을 2초로 설정했으므로 _controller.value는 2초에 걸쳐 0.0에서 1.0으로 증가합니다.

..repeat() 캐스케이드 연산자를 사용하여 생성 즉시 무한 반복 애니메이션을 시작합니다. 만약 한 번만 실행하려면 ..forward()를, 왕복 운동을 하려면 ..repeat(reverse: true)를 사용하면 됩니다.

AnimatedBuilder 위젯은 매우 중요한 최적화 도구입니다. _controller가 변경될 때마다 builder 함수만 다시 실행하고, child로 전달된 FlutterLogo는 재빌드하지 않습니다.

이렇게 하면 초당 60번 재빌드가 일어나더라도 성능에 미치는 영향을 최소화할 수 있습니다. Transform.rotate는 _controller.value (0.0~1.0)에 2π를 곱하여 0도에서 360도까지 회전합니다.

이 계산이 매 프레임마다 일어나므로 부드러운 회전 애니메이션이 만들어집니다. 마지막으로, dispose에서 반드시 _controller.dispose()를 호출해야 합니다.

이를 빼먹으면 위젯이 화면에서 사라진 후에도 애니메이션이 계속 실행되어 메모리 누수가 발생합니다. 여러분이 이 코드를 사용하면 로딩 스피너, 새로고침 아이콘, 주목을 끄는 배지 등 다양한 회전 애니메이션을 만들 수 있습니다.

AnimationController의 개념을 이해하면 더 복잡한 애니메이션도 쉽게 구현할 수 있게 됩니다.

실전 팁

💡 항상 dispose에서 컨트롤러를 해제하세요. 이를 잊으면 심각한 메모리 누수와 성능 저하가 발생합니다.

💡 SingleTickerProviderStateMixin은 컨트롤러가 1개일 때, TickerProviderStateMixin은 여러 개일 때 사용합니다.

💡 AnimatedBuilder 대신 일반 builder를 사용하면 child도 매번 재빌드되어 성능이 크게 저하됩니다. 꼭 AnimatedBuilder를 사용하세요.

💡 lowerBound와 upperBound 파라미터로 0.0~1.0이 아닌 다른 범위도 사용할 수 있습니다. 예: AnimationController(lowerBound: 0, upperBound: 360)

💡 개발 중에는 _controller.duration을 짧게 설정하여 빠르게 테스트하고, 완성 단계에서 실제 duration으로 조정하세요.


3. Tween 애니메이션 - 값의 범위 지정하기

시작하며

여러분이 AnimationController를 사용하면서 0.0~1.0 값을 매번 원하는 범위로 수동 변환하는 게 번거롭다고 느끼셨나요? 예를 들어 투명도를 0.3에서 1.0으로 변경하거나, 위치를 100픽셀에서 300픽셀로 이동시킬 때 계산식을 직접 작성하는 것은 비효율적입니다.

이런 문제는 복잡한 애니메이션에서 더욱 심각해집니다. 여러 속성을 동시에 애니메이션화할 때 각각의 변환 로직을 관리하다 보면 코드가 지저분해지고 버그가 발생하기 쉽습니다.

바로 이럴 때 필요한 것이 Tween입니다. Tween은 "between"의 줄임말로, 시작 값과 끝 값을 지정하면 AnimationController의 0.0~1.0을 자동으로 원하는 범위의 값으로 매핑해줍니다.

개요

간단히 말해서, Tween은 애니메이션의 시작점과 끝점을 정의하고, 그 사이의 중간 값들을 자동으로 계산해주는 보간(interpolation) 도구입니다. Tween<double>, Tween<Color>, Tween<Offset> 등 다양한 타입을 지원하며, animate() 메서드로 AnimationController와 연결하여 사용합니다.

예를 들어, 로그인 버튼이 화면 아래에서 위로 슬라이드되면서 나타나는 효과나, 알림 배지의 색상이 서서히 변하는 효과를 구현할 때 필수적입니다. 기존에는 _controller.value * (end - start) + start 같은 수식을 직접 계산했다면, 이제는 Tween(begin: start, end: end).animate(_controller)로 간단하게 처리할 수 있습니다.

핵심 특징은 첫째, 숫자뿐 아니라 Color, Size, Rect, TextStyle 등 거의 모든 타입을 보간할 수 있고, 둘째, CurvedAnimation과 조합하여 비선형 곡선을 적용할 수 있으며, 셋째, chain()으로 여러 Tween을 연결할 수 있다는 점입니다. 이러한 특징들이 복잡한 애니메이션을 간결하고 읽기 쉬운 코드로 만들어줍니다.

코드 예제

class SlidingButton extends StatefulWidget {
  @override
  _SlidingButtonState createState() => _SlidingButtonState();
}

class _SlidingButtonState extends State<SlidingButton>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Offset> _slideAnimation;
  late Animation<double> _fadeAnimation;

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

    // Tween으로 화면 아래(0, 1)에서 원위치(0, 0)로 슬라이드
    _slideAnimation = Tween<Offset>(
      begin: Offset(0, 1),
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOut,
    ));

    // 동시에 투명도도 0에서 1로 변경
    _fadeAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(_controller);

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: _slideAnimation,
      child: FadeTransition(
        opacity: _fadeAnimation,
        child: ElevatedButton(
          onPressed: () {},
          child: Text('로그인'),
        ),
      ),
    );
  }
}

설명

이것이 하는 일: 버튼이 화면 아래에서 위로 슬라이드되면서 동시에 페이드인되는 세련된 입장 애니메이션을 만듭니다. 첫 번째로, 두 개의 Animation 변수를 선언합니다.

_slideAnimation은 위치를, _fadeAnimation은 투명도를 담당합니다. 이 둘은 같은 _controller를 공유하므로 완벽하게 동기화되어 실행됩니다.

이렇게 여러 속성을 동시에 애니메이션화하는 것이 Tween의 가장 큰 장점입니다. 그 다음으로, Offset Tween을 설정합니다.

Offset(0, 1)은 위젯의 높이만큼 아래를 의미하고, Offset.zero는 원래 위치입니다. CurvedAnimation으로 감싸면 Curves.easeOut이 적용되어 빠르게 시작했다가 부드럽게 멈추는 자연스러운 움직임이 만들어집니다.

CurvedAnimation 없이 직접 animate()를 호출하면 선형(linear) 움직임이 되어 기계적으로 보입니다. 투명도 Tween은 더 간단합니다.

0.0(완전 투명)에서 1.0(완전 불투명)으로 변하며, 별도의 Curve를 지정하지 않았으므로 컨트롤러의 진행에 따라 선형으로 증가합니다. 만약 투명도도 곡선을 적용하고 싶다면 _slideAnimation처럼 CurvedAnimation으로 감싸면 됩니다.

SlideTransition과 FadeTransition은 Flutter가 제공하는 최적화된 전환 위젯입니다. 직접 Transform과 Opacity를 사용할 수도 있지만, 이 위젯들은 내부적으로 렌더링 최적화가 되어 있어 성능이 더 좋습니다.

특히 FadeTransition은 자식 위젯을 캐시하여 불필요한 재빌드를 방지합니다. 마지막으로, _controller.forward()를 호출하여 애니메이션을 시작합니다.

이것은 initState에서 한 번만 실행되므로 위젯이 처음 화면에 나타날 때만 애니메이션이 재생됩니다. 여러분이 이 코드를 사용하면 온보딩 화면, 모달 다이얼로그, 알림 배너 등 다양한 UI 요소에 전문적인 등장 효과를 적용할 수 있습니다.

Tween을 마스터하면 디자이너가 요구하는 거의 모든 애니메이션을 구현할 수 있게 됩니다.

실전 팁

💡 ColorTween을 사용하면 색상 간 부드러운 전환이 가능합니다. 직접 RGB 값을 계산할 필요가 없습니다.

💡 CurvedAnimation은 별도의 Animation 객체를 생성하므로 dispose가 필요 없습니다. 부모 컨트롤러만 해제하면 됩니다.

💡 여러 단계의 애니메이션이 필요하면 TweenSequence를 사용하세요. 예: 튀어 올랐다가 내려오고 다시 튀는 효과

💡 슬라이드 방향은 Offset의 x, y 값으로 조절합니다. Offset(1, 0)은 오른쪽에서, Offset(-1, 0)은 왼쪽에서 등장합니다.

💡 성능을 위해 Transform과 Opacity 대신 SlideTransition, FadeTransition 같은 전용 위젯을 사용하세요.


4. Hero 애니메이션 - 화면 전환의 마법

시작하며

여러분이 인스타그램이나 쇼핑몰 앱에서 썸네일 이미지를 탭하면 그 이미지가 확대되면서 상세 페이지로 부드럽게 전환되는 효과를 본 적 있나요? 많은 개발자들이 이런 효과를 구현하려고 복잡한 애니메이션 로직을 작성하다가 좌절합니다.

이런 화면 간 공유 요소 전환(Shared Element Transition)은 사용자 경험을 극적으로 향상시키지만, 두 화면의 좌표를 계산하고 동기화하는 것은 매우 어렵습니다. 위치, 크기, 회전까지 고려하면 코드가 금방 복잡해집니다.

바로 이럴 때 필요한 것이 Hero 위젯입니다. 이름처럼 영웅이 무대를 이동하듯, 같은 tag를 가진 위젯이 화면 간 이동할 때 자동으로 부드러운 전환 애니메이션을 만들어줍니다.

개요

간단히 말해서, Hero는 두 화면에서 같은 tag를 가진 위젯을 찾아 자동으로 비행(flight) 애니메이션을 생성하는 마법 같은 위젯입니다. 출발 화면과 도착 화면에 각각 Hero 위젯을 배치하고 동일한 tag를 부여하면, Navigator로 화면을 전환할 때 Flutter가 자동으로 위치, 크기, 모양의 변화를 계산하여 매끄럽게 애니메이션화합니다.

예를 들어, 갤러리 앱에서 그리드의 작은 사진이 풀스크린으로 확대되거나, 제품 목록의 카드가 상세 페이지로 전환되는 경우에 완벽합니다. 기존에는 CustomPageRoute를 만들고 Tween으로 위치와 크기를 직접 계산해야 했다면, 이제는 Hero로 감싸고 tag만 동일하게 주면 끝입니다.

핵심 특징은 첫째, 완전히 자동으로 작동하여 별도의 컨트롤러나 계산이 필요 없고, 둘째, 화면 전환 애니메이션과 완벽하게 동기화되며, 셋째, createRectTween으로 비행 경로를 커스터마이징할 수 있다는 점입니다. 이러한 특징들이 앱을 프리미엄 퀄리티로 만들어주는 결정적인 요소가 됩니다.

코드 예제

// 첫 번째 화면: 썸네일 목록
class ImageGrid extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
      ),
      itemBuilder: (context, index) => GestureDetector(
        onTap: () => Navigator.push(
          context,
          MaterialPageRoute(
            builder: (_) => ImageDetail(index: index),
          ),
        ),
        // Hero로 감싸고 고유한 tag 부여
        child: Hero(
          tag: 'image-$index',
          child: Image.network(
            'https://picsum.photos/200?random=$index',
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

// 두 번째 화면: 이미지 상세
class ImageDetail extends StatelessWidget {
  final int index;
  ImageDetail({required this.index});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onTap: () => Navigator.pop(context),
        // 같은 tag를 사용하여 자동 연결
        child: Hero(
          tag: 'image-$index',
          child: Image.network(
            'https://picsum.photos/800?random=$index',
            fit: BoxFit.contain,
          ),
        ),
      ),
    );
  }
}

설명

이것이 하는 일: 그리드에서 작은 이미지를 탭하면 풀스크린으로 확대되면서 자연스럽게 전환되는 갤러리 앱을 만듭니다. 첫 번째로, 출발 화면에서 각 이미지를 Hero로 감싸고 'image-$index'라는 고유한 tag를 부여합니다.

이 tag는 두 화면에서 같은 위젯임을 식별하는 핵심입니다. tag는 String, int 등 어떤 타입이든 가능하지만, 같은 화면에서는 절대 중복되어서는 안 됩니다.

중복되면 런타임 에러가 발생합니다. 그 다음으로, Navigator.push로 화면을 전환할 때 Flutter의 Hero 시스템이 작동합니다.

내부적으로 OverlayEntry를 생성하여 Hero 위젯을 두 화면 위에 오버레이로 띄우고, 출발 위치에서 도착 위치까지 부드럽게 이동시킵니다. 이 과정에서 위치뿐 아니라 크기, BorderRadius, BoxShadow 등의 변화도 자동으로 보간됩니다.

도착 화면에서도 동일한 tag 'image-$index'를 사용하여 Hero를 만듭니다. fit 속성이 cover에서 contain으로 바뀌었지만, Hero는 이 차이도 자동으로 처리하여 자연스럽게 전환합니다.

만약 이미지 자체가 다르면 크로스페이드 효과가 적용됩니다. Navigator.pop()으로 돌아갈 때도 같은 애니메이션이 역으로 재생됩니다.

큰 이미지가 다시 작은 썸네일 위치로 축소되면서 돌아가죠. 이 양방향 애니메이션이 사용자에게 공간적 일관성을 제공하여 내비게이션을 직관적으로 만듭니다.

Hero 애니메이션의 기본 duration은 300ms이고, curve는 fastOutSlowIn입니다. 이것을 변경하려면 출발 화면의 Hero에 flightShuttleBuilder를 제공하여 커스텀 애니메이션을 만들 수 있습니다.

여러분이 이 코드를 사용하면 갤러리, 프로필 사진 확대, 제품 상세 페이지 전환 등 다양한 시나리오에서 iOS와 Android의 네이티브 앱처럼 세련된 전환 효과를 구현할 수 있습니다. 사용자들은 이런 디테일에서 앱의 퀄리티를 판단합니다.

실전 팁

💡 tag는 반드시 고유해야 합니다. ListView에서는 ID를, GridView에서는 인덱스를 사용하세요.

💡 Material 또는 Card로 감싸진 Hero는 elevation 변화도 애니메이션화됩니다. 입체감 있는 전환을 만들 수 있습니다.

💡 flightShuttleBuilder를 사용하면 비행 중에 표시될 위젯을 커스터마이징할 수 있습니다. 예: 회전 효과 추가

💡 placeholderBuilder로 원래 위치에 남을 위젯을 지정할 수 있습니다. 예: 로딩 스켈레톤

💡 Hero 애니메이션이 끊기거나 이상하다면 BoxFit, BorderRadius, ClipRRect 설정이 양쪽에서 일치하는지 확인하세요.


5. 스태거드 애니메이션 - 순차적 효과의 예술

시작하며

여러분이 앱의 온보딩 화면에서 로고, 제목, 설명, 버튼이 순차적으로 페이드인되면서 나타나는 세련된 효과를 만들고 싶었던 적 있나요? 각 요소마다 별도의 AnimationController를 만들고 타이밍을 맞추려다 보면 코드가 복잡해지고 동기화가 어려워집니다.

이런 순차 애니메이션은 사용자의 시선을 자연스럽게 유도하고 정보의 우선순위를 시각적으로 표현하는 중요한 UX 기법입니다. 하지만 여러 애니메이션의 시작 시점과 지속 시간을 조율하는 것은 생각보다 복잡합니다.

바로 이럴 때 필요한 것이 스태거드(Staggered) 애니메이션입니다. 단일 AnimationController로 여러 애니메이션을 관리하되, 각각 다른 Interval을 적용하여 순차적으로 실행되도록 만드는 기법입니다.

개요

간단히 말해서, 스태거드 애니메이션은 하나의 컨트롤러로 여러 애니메이션을 시간차를 두고 실행하는 디자인 패턴입니다. Interval 클래스를 사용하여 전체 애니메이션 duration 중 특정 구간에만 각 애니메이션이 활성화되도록 설정합니다.

예를 들어, 3초 애니메이션에서 첫 번째는 0.00.3초, 두 번째는 0.20.6초, 세 번째는 0.4~1.0초에 실행되도록 오버랩시킬 수 있습니다. 이는 리스트 아이템이 차례로 나타나거나, 대시보드의 통계 카드가 연쇄적으로 등장하는 효과에 완벽합니다.

기존에는 여러 개의 AnimationController를 만들고 Future.delayed로 타이밍을 맞췄다면, 이제는 단일 컨트롤러와 여러 개의 Tween + Interval 조합으로 훨씬 간결하게 구현할 수 있습니다. 핵심 특징은 첫째, 하나의 컨트롤러만 관리하면 되므로 코드가 간결하고, 둘째, Interval의 begin과 end를 조절하여 순차/오버랩/동시 실행을 자유롭게 제어할 수 있으며, 셋째, 모든 애니메이션이 자동으로 동기화되어 타이밍 버그가 발생하지 않는다는 점입니다.

이러한 특징들이 복잡한 입장 애니메이션을 쉽게 만들어줍니다.

코드 예제

class StaggeredList extends StatefulWidget {
  @override
  _StaggeredListState createState() => _StaggeredListState();
}

class _StaggeredListState extends State<StaggeredList>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late List<Animation<double>> _fadeAnimations;
  late List<Animation<Offset>> _slideAnimations;

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

    // 3개 아이템에 대한 애니메이션 생성
    _fadeAnimations = List.generate(3, (index) {
      final start = index * 0.2; // 0.0, 0.2, 0.4
      final end = start + 0.4; // 0.4, 0.6, 0.8
      return Tween<double>(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(
          parent: _controller,
          curve: Interval(start, end, curve: Curves.easeOut),
        ),
      );
    });

    _slideAnimations = List.generate(3, (index) {
      final start = index * 0.2;
      final end = start + 0.4;
      return Tween<Offset>(begin: Offset(0, 0.5), end: Offset.zero).animate(
        CurvedAnimation(
          parent: _controller,
          curve: Interval(start, end, curve: Curves.easeOut),
        ),
      );
    });

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 3,
      itemBuilder: (context, index) => FadeTransition(
        opacity: _fadeAnimations[index],
        child: SlideTransition(
          position: _slideAnimations[index],
          child: Card(
            margin: EdgeInsets.all(8),
            child: ListTile(title: Text('아이템 ${index + 1}')),
          ),
        ),
      ),
    );
  }
}

설명

이것이 하는 일: 리스트의 3개 아이템이 0.2초 간격으로 차례대로 페이드인되면서 슬라이드 업하는 우아한 입장 효과를 만듭니다. 첫 번째로, 전체 duration을 1500ms로 설정한 단일 컨트롤러를 생성합니다.

이 컨트롤러의 0.0~1.0 진행 값을 모든 애니메이션이 공유합니다. List.generate로 3개의 애니메이션을 생성하는데, 각각 다른 Interval을 가집니다.

첫 번째는 0.00.4(0600ms), 두 번째는 0.20.6(300900ms), 세 번째는 0.40.8(6001200ms)에 활성화됩니다. Interval의 작동 원리는 이렇습니다.

컨트롤러 값이 start(0.0) 미만이면 0.0을 반환하고, end(0.4)를 넘으면 1.0을 반환하며, 그 사이에서는 비례적으로 보간합니다. 따라서 첫 번째 애니메이션은 컨트롤러가 0.0~0.4 구간에 있을 때만 0.0에서 1.0으로 증가하고, 0.4 이후로는 1.0에 고정됩니다.

각 아이템은 두 가지 애니메이션을 동시에 적용받습니다. _fadeAnimations로 투명도가 변하고, _slideAnimations로 아래에서 위로 이동합니다.

이 둘은 같은 Interval을 사용하므로 완벽하게 동기화되어 실행됩니다. 만약 페이드를 먼저 시작하고 슬라이드를 나중에 시작하고 싶다면 서로 다른 Interval을 사용하면 됩니다.

오버랩 정도는 start 값의 간격으로 조절합니다. index * 0.2는 20% 간격이므로 상당히 오버랩됩니다.

index * 0.33으로 바꾸면 거의 순차적으로 실행되고, index * 0.1로 줄이면 거의 동시에 실행되는 것처럼 보입니다. 마지막으로, 모든 애니메이션이 설정되면 _controller.forward() 한 번만 호출하면 됩니다.

이것만으로 3개의 아이템이 자동으로 순차 애니메이션됩니다. 컨트롤러가 하나이므로 pause, reverse, stop도 모든 애니메이션에 일괄 적용됩니다.

여러분이 이 코드를 사용하면 온보딩 화면, 대시보드 로딩, 검색 결과 표시 등 다양한 곳에서 프로페셔널한 연출을 만들 수 있습니다. 사용자는 정보가 순차적으로 등장하는 것을 보며 자연스럽게 시선을 이동하게 됩니다.

실전 팁

💡 Interval의 범위가 겹치면 오버랩 애니메이션, 붙어있으면 순차 애니메이션, 같으면 동시 애니메이션이 됩니다.

💡 긴 리스트에서는 List.generate 대신 빌더 패턴을 사용하여 필요한 애니메이션만 생성하세요.

💡 duration을 아이템 개수에 비례하여 동적으로 설정하면 항목 수가 달라져도 자연스럽습니다. 예: Duration(milliseconds: 300 + items.length * 100)

💡 reverse 애니메이션도 자동으로 스태거드됩니다. 퇴장 효과를 만들 때 유용합니다.

💡 복잡한 스태거드는 TweenSequence를 사용하여 각 단계를 명확하게 정의할 수 있습니다.


6. 커스텀 애니메이션 - CustomPainter로 그리기

시작하며

여러분이 로딩 인디케이터나 진행률 바를 만들 때 기본 위젯으로는 원하는 모양을 만들 수 없어서 답답했던 경험이 있나요? 특히 원형 진행률 바에 그라디언트를 적용하거나, 물결 모양으로 차오르는 효과 같은 커스텀 디자인은 일반 위젯으로는 불가능합니다.

이런 고급 시각 효과는 앱의 브랜드 아이덴티티를 구축하고 경쟁 제품과 차별화하는 핵심 요소입니다. 하지만 Canvas API를 직접 다루는 것은 진입 장벽이 높아 많은 개발자가 포기합니다.

바로 이럴 때 필요한 것이 CustomPainter입니다. Canvas와 Paint를 사용하여 픽셀 단위로 직접 그림을 그릴 수 있고, 이를 애니메이션과 결합하면 상상할 수 있는 거의 모든 효과를 만들 수 있습니다.

개요

간단히 말해서, CustomPainter는 Flutter의 저수준 그리기 API에 접근하여 원, 선, 경로, 텍스트 등을 직접 렌더링할 수 있게 해주는 클래스입니다. paint() 메서드에서 Canvas 객체를 받아 drawCircle, drawArc, drawPath 같은 메서드로 그래픽을 그리고, shouldRepaint()로 재렌더링 조건을 지정합니다.

예를 들어, 심박수 그래프, 사인파 애니메이션, 커스텀 차트, 게임 캐릭터 등 표준 위젯으로는 불가능한 시각화를 구현할 때 필수입니다. 기존에는 복잡한 모양을 만들기 위해 여러 Container와 Transform을 조합했다면, 이제는 CustomPainter로 정확히 원하는 모양을 직접 그릴 수 있습니다.

핵심 특징은 첫째, 픽셀 수준의 완벽한 제어가 가능하여 디자이너의 요구사항을 100% 구현할 수 있고, 둘째, 애니메이션 값을 생성자로 전달하여 실시간 업데이트가 가능하며, 셋째, 성능이 우수하여 복잡한 그래픽도 60fps로 애니메이션할 수 있다는 점입니다. 이러한 특징들이 프리미엄 시각 효과의 기반이 됩니다.

코드 예제

// 애니메이션되는 원형 진행률 바
class CircularProgressPainter extends CustomPainter {
  final double progress; // 0.0 ~ 1.0
  final Color color;

  CircularProgressPainter({required this.progress, required this.color});

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 2;

    // 배경 원 그리기
    final bgPaint = Paint()
      ..color = Colors.grey.shade200
      ..strokeWidth = 10
      ..style = PaintingStyle.stroke;
    canvas.drawCircle(center, radius, bgPaint);

    // 진행률 아크 그리기
    final progressPaint = Paint()
      ..color = color
      ..strokeWidth = 10
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round; // 둥근 끝

    // -90도(12시 방향)에서 시작하여 progress만큼 그리기
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -3.14159 / 2, // 시작 각도 (라디안)
      2 * 3.14159 * progress, // 진행 각도
      false,
      progressPaint,
    );
  }

  @override
  bool shouldRepaint(CircularProgressPainter oldDelegate) {
    // progress가 변경되었을 때만 다시 그리기
    return oldDelegate.progress != progress;
  }
}

// 사용 예시
class AnimatedProgress extends StatefulWidget {
  @override
  _AnimatedProgressState createState() => _AnimatedProgressState();
}

class _AnimatedProgressState extends State<AnimatedProgress>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

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

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) => CustomPaint(
        size: Size(150, 150),
        painter: CircularProgressPainter(
          progress: _controller.value,
          color: Colors.blue,
        ),
      ),
    );
  }
}

설명

이것이 하는 일: 원형 진행률 바가 12시 방향에서 시작하여 시계방향으로 2초에 걸쳐 완성되는 애니메이션을 만듭니다. 첫 번째로, CustomPainter를 상속하는 CircularProgressPainter 클래스를 만듭니다.

progress 파라미터로 0.0~1.0 값을 받아 진행률을 표현합니다. 이 값이 애니메이션에 의해 계속 변하면서 paint() 메서드가 반복 호출됩니다.

color도 파라미터로 받아 재사용 가능하게 만듭니다. paint() 메서드 내부에서 Canvas 객체를 사용하여 실제 그리기를 수행합니다.

먼저 배경 원을 그립니다. Paint 객체를 만들어 색상, 선 두께, 스타일을 설정하고 drawCircle로 회색 원을 그립니다.

PaintingStyle.stroke는 채우기가 아닌 테두리만 그린다는 의미입니다. 그 다음으로, 진행률 아크를 그립니다.

drawArc는 타원의 일부를 그리는 메서드로, Rect(사각형 영역), startAngle(시작 각도), sweepAngle(진행 각도), useCenter(중심 연결 여부), paint(스타일) 파라미터를 받습니다. -π/2는 12시 방향(위쪽)을 의미하고, 2π * progress는 진행률에 비례한 각도입니다.

strokeCap.round를 사용하여 끝부분을 둥글게 만들어 세련된 느낌을 줍니다. shouldRepaint() 메서드는 성능 최적화에 매우 중요합니다.

이 메서드가 true를 반환할 때만 paint()가 다시 호출됩니다. progress 값이 실제로 변경되었을 때만 true를 반환하여 불필요한 재렌더링을 방지합니다.

만약 항상 true를 반환하면 매 프레임마다 다시 그려져 성능이 저하됩니다. AnimatedBuilder에서 _controller.value를 progress로 전달하면, 컨트롤러가 0.0에서 1.0으로 변할 때마다 새로운 CircularProgressPainter가 생성되고 paint()가 호출되어 진행률 바가 애니메이션됩니다.

여러분이 이 코드를 사용하면 다운로드 진행률, 타이머, 건강 데이터 표시, 게임 쿨다운 등 다양한 곳에서 고유한 디자인의 진행률 표시를 만들 수 있습니다. 그라디언트, 그림자, 텍스트까지 추가하여 완전히 커스터마이징할 수 있습니다.

실전 팁

💡 Paint 객체는 재사용하세요. paint() 메서드 안에서 매번 생성하면 GC 압박이 커집니다. 클래스 필드로 만들어 재사용하세요.

💡 복잡한 Path는 Path.combine()으로 결합하거나, PathMetrics로 경로를 따라 애니메이션할 수 있습니다.

💡 shouldRepaint는 최대한 정밀하게 작성하세요. 불필요한 재렌더링은 배터리와 성능을 크게 해칩니다.

💡 디버깅 시 debugPaintSizeEnabled = true로 설정하면 위젯 경계를 시각화할 수 있습니다.

💡 shader를 사용하면 그라디언트, 이미지 패턴, 선형/방사형 효과를 적용할 수 있습니다. 예: Paint()..shader = LinearGradient(...).createShader(rect)


7. 물리 기반 애니메이션 - SpringSimulation 활용

시작하며

여러분이 앱에서 드래그해서 놓았을 때 튕기면서 제자리로 돌아오는 자연스러운 효과를 만들고 싶었던 적 있나요? Curves.easeOut 같은 기본 곡선은 수학적으로는 부드럽지만, 실제 물리적 움직임처럼 느껴지지 않습니다.

이런 현실감 있는 애니메이션은 사용자에게 앱이 살아있다는 느낌을 주고, 터치 인터랙션이 즉각적으로 반응한다는 느낌을 전달합니다. 하지만 스프링의 강성, 감쇠, 질량 같은 물리 파라미터를 직접 계산하는 것은 매우 어렵습니다.

바로 이럴 때 필요한 것이 SpringSimulation입니다. 실제 물리 법칙을 기반으로 스프링의 움직임을 시뮬레이션하여, iOS의 네이티브 애니메이션처럼 자연스럽고 반응적인 효과를 만들 수 있습니다.

개요

간단히 말해서, SpringSimulation은 후크의 법칙(Hooke's law)을 적용하여 스프링이 늘어났다가 제자리로 돌아오는 움직임을 시뮬레이션하는 물리 엔진입니다. SpringDescription으로 질량(mass), 강성(stiffness), 감쇠(damping)를 설정하고, animateWith()로 AnimationController에 적용합니다.

예를 들어, 바텀시트를 드래그해서 놓았을 때 관성을 고려하여 튕기면서 열리거나 닫히는 효과, 또는 당겨서 새로고침(pull-to-refresh)에서 손을 놓으면 부드럽게 되돌아가는 효과에 완벽합니다. 기존에는 Curves.bounceOut 같은 미리 정의된 곡선을 사용했다면, 이제는 물리 파라미터를 조절하여 정확히 원하는 느낌을 만들 수 있습니다.

핵심 특징은 첫째, 초기 속도(velocity)를 고려하여 드래그의 속도에 따라 다르게 반응하고, 둘째, critical damping(임계 감쇠)으로 튕김 없이 빠르게 안정화시키거나 under damping(과소 감쇠)으로 여러 번 튕기게 할 수 있으며, 셋째, 수학적으로 정확하여 예측 가능하고 일관된 결과를 보장한다는 점입니다. 이러한 특징들이 프리미엄 앱의 촉감(haptic feel)을 만들어냅니다.

코드 예제

class BouncingBox extends StatefulWidget {
  @override
  _BouncingBoxState createState() => _BouncingBoxState();
}

class _BouncingBoxState extends State<BouncingBox>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  double _dragPosition = 0.0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      // upperBound를 화면 높이로 설정
      lowerBound: 0,
      upperBound: 300,
    );
    _animation = _controller;
  }

  void _onPanUpdate(DragUpdateDetails details) {
    setState(() {
      // 드래그 거리만큼 위치 변경
      _dragPosition += details.delta.dy;
      _dragPosition = _dragPosition.clamp(0.0, 300.0);
      _controller.value = _dragPosition;
    });
  }

  void _onPanEnd(DragEndDetails details) {
    // 드래그 종료 시 스프링 시뮬레이션으로 원위치
    final spring = SpringDescription(
      mass: 1, // 질량
      stiffness: 100, // 강성 (높을수록 빠르게 복원)
      damping: 10, // 감쇠 (낮을수록 많이 튕김)
    );

    final simulation = SpringSimulation(
      spring,
      _dragPosition, // 시작 위치
      0, // 목표 위치
      details.velocity.pixelsPerSecond.dy / 1000, // 초기 속도
    );

    _controller.animateWith(simulation);
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: _onPanUpdate,
      onPanEnd: _onPanEnd,
      child: AnimatedBuilder(
        animation: _animation,
        builder: (context, child) => Transform.translate(
          offset: Offset(0, _animation.value),
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
            child: Center(child: Text('드래그하세요')),
          ),
        ),
      ),
    );
  }
}

설명

이것이 하는 일: 박스를 아래로 드래그하면 따라 움직이고, 손을 떼면 스프링처럼 튕기면서 원위치로 돌아오는 인터랙티브한 애니메이션을 만듭니다. 첫 번째로, GestureDetector로 패닝(드래그) 제스처를 감지합니다.

onPanUpdate에서 드래그 거리(details.delta.dy)만큼 _dragPosition을 업데이트하고, clamp로 0~300 범위로 제한합니다. _controller.value에 직접 할당하여 애니메이션 없이 즉시 위치를 변경합니다.

이렇게 하면 손가락을 따라 박스가 정확히 움직입니다. 그 다음으로, onPanEnd에서 손을 뗐을 때 SpringSimulation을 생성합니다.

SpringDescription의 파라미터가 애니메이션의 느낌을 결정합니다. stiffness를 높이면(예: 500) 강한 스프링처럼 빠르게 복원되고, 낮추면(예: 50) 부드럽게 천천히 돌아옵니다.

damping을 낮추면(예: 5) 여러 번 튕기고, 높이면(예: 20) 튕김 없이 부드럽게 멈춥니다. details.velocity.pixelsPerSecond.dy는 손가락의 속도를 픽셀/초 단위로 제공합니다.

이 값을 1000으로 나눠 적절한 스케일로 조정하여 SpringSimulation의 초기 속도로 전달합니다. 빠르게 드래그하면 빠르게 튕기고, 천천히 드래그하면 천천히 돌아가는 자연스러운 효과가 만들어집니다.

_controller.animateWith(simulation)는 기존의 forward()나 reverse() 대신 물리 시뮬레이션을 적용하는 메서드입니다. 내부적으로 SpringSimulation이 매 프레임마다 다음 위치를 계산하고, 스프링 방정식이 안정 상태에 도달하면 자동으로 멈춥니다.

critical damping을 계산하고 싶다면 damping = 2 * sqrt(mass * stiffness) 공식을 사용하세요. 이 값에서 정확히 튕김 없이 가장 빠르게 안정화됩니다.

여러분이 이 코드를 사용하면 드로어, 바텀시트, 모달, 카드 스와이프 등 거의 모든 드래그 인터랙션에 네이티브 앱 수준의 물리 기반 애니메이션을 적용할 수 있습니다. 사용자는 앱이 터치에 "살아있는 것처럼" 반응한다고 느끼게 됩니다.

실전 팁

💡 일반적인 스프링 값: mass=1, stiffness=100200, damping=1015가 iOS 네이티브와 비슷한 느낌을 줍니다.

💡 FrictionSimulation도 함께 사용하면 관성 스크롤 효과를 만들 수 있습니다. 드래그 방향으로 계속 미끄러지다가 멈추는 효과입니다.

💡 GravitySimulation은 낙하 효과를 시뮬레이션합니다. 카드를 아래로 던지면 가속되면서 떨어지는 효과를 만들 수 있습니다.

💡 velocity를 너무 크게 전달하면 튕김이 과도해집니다. 1000으로 나누는 스케일 팩터를 조절하여 적절한 강도를 찾으세요.

💡 Simulation 클래스를 직접 상속하면 완전히 커스텀한 물리 시뮬레이션도 만들 수 있습니다. 예: 자석에 끌리는 효과


8. 암시적 애니메이션 위젯들 - 빠른 적용을 위한 도구들

시작하며

여러분이 모든 애니메이션마다 AnimationController를 만들고 Tween을 설정하는 것이 반복적이고 피곤하다고 느끼셨나요? 단순한 페이드 효과나 크기 변경에도 많은 보일러플레이트 코드가 필요합니다.

이런 일상적인 애니메이션은 개발 시간의 상당 부분을 차지하지만, 실제로는 거의 비슷한 패턴이 반복됩니다. 투명도, 위치, 크기, 회전 등 기본적인 속성 변경이 대부분이죠.

바로 이럴 때 필요한 것이 Flutter의 암시적 애니메이션 위젯들입니다. AnimatedOpacity, AnimatedPositioned, AnimatedSize, AnimatedRotation 등 각 속성에 특화된 위젯들이 준비되어 있어, setState로 값만 바꿔주면 자동으로 애니메이션됩니다.

개요

간단히 말해서, Animated로 시작하는 위젯들은 특정 속성의 변경을 자동으로 애니메이션화하는 편의 위젯들입니다. AnimatedOpacity는 투명도, AnimatedPadding은 패딩, AnimatedAlign은 정렬, AnimatedDefaultTextStyle은 텍스트 스타일을 애니메이션합니다.

모두 duration과 curve 파라미터를 공통으로 가지며, 값이 변경되면 자동으로 이전 값에서 새 값으로 전환합니다. 예를 들어, 조건부로 나타나는 에러 메시지, 확장/축소되는 패널, 활성/비활성 상태에 따라 변하는 UI 요소 등에 즉시 적용할 수 있습니다.

기존에는 FadeTransition + AnimationController를 만들어야 했다면, 이제는 AnimatedOpacity에 opacity 값만 바꿔주면 됩니다. 핵심 특징은 첫째, 컨트롤러가 필요 없어 코드가 매우 간결하고, 둘째, StatefulWidget의 setState와 완벽하게 통합되며, 셋째, 다양한 속성별로 특화된 위젯이 제공되어 선택의 폭이 넓다는 점입니다.

이러한 특징들이 일상적인 애니메이션 작업을 5분의 1로 단축시켜줍니다.

코드 예제

class ToggleWidgets extends StatefulWidget {
  @override
  _ToggleWidgetsState createState() => _ToggleWidgetsState();
}

class _ToggleWidgetsState extends State<ToggleWidgets> {
  bool _isVisible = true;
  bool _isExpanded = false;
  AlignmentGeometry _alignment = Alignment.topLeft;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 1. AnimatedOpacity: 투명도 애니메이션
        AnimatedOpacity(
          opacity: _isVisible ? 1.0 : 0.0,
          duration: Duration(milliseconds: 500),
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          ),
        ),

        // 2. AnimatedContainer: 여러 속성 동시 애니메이션
        AnimatedContainer(
          duration: Duration(milliseconds: 400),
          width: _isExpanded ? 200 : 100,
          height: _isExpanded ? 200 : 100,
          color: _isExpanded ? Colors.green : Colors.red,
          curve: Curves.easeInOut,
        ),

        // 3. AnimatedAlign: 정렬 위치 애니메이션
        Container(
          height: 200,
          width: 200,
          color: Colors.grey[300],
          child: AnimatedAlign(
            alignment: _alignment,
            duration: Duration(milliseconds: 600),
            curve: Curves.bounceOut,
            child: Container(
              width: 50,
              height: 50,
              color: Colors.orange,
            ),
          ),
        ),

        // 제어 버튼들
        ElevatedButton(
          onPressed: () => setState(() => _isVisible = !_isVisible),
          child: Text('투명도 토글'),
        ),
        ElevatedButton(
          onPressed: () => setState(() => _isExpanded = !_isExpanded),
          child: Text('크기 토글'),
        ),
        ElevatedButton(
          onPressed: () => setState(() {
            _alignment = _alignment == Alignment.topLeft
                ? Alignment.bottomRight
                : Alignment.topLeft;
          }),
          child: Text('위치 토글'),
        ),
      ],
    );
  }
}

설명

이것이 하는 일: 버튼을 누를 때마다 세 가지 다른 위젯이 각각 페이드, 크기 변경, 위치 이동 애니메이션을 실행하는 인터랙티브한 데모를 만듭니다. 첫 번째로, AnimatedOpacity는 가장 자주 사용되는 암시적 애니메이션 위젯입니다.

_isVisible 상태가 변경되어 setState가 호출되면, 위젯이 재빌드되면서 opacity 값이 1.0에서 0.0으로(또는 반대로) 변합니다. AnimatedOpacity는 이 변화를 감지하고 500ms 동안 부드럽게 전환합니다.

이는 조건부 렌더링 + 페이드 효과에 완벽합니다. AnimatedContainer는 앞에서 배웠듯이 가장 만능인 암시적 애니메이션 위젯입니다.

이 예제에서는 width, height, color를 동시에 변경하여 확장/축소 효과를 만듭니다. 모든 속성이 같은 duration과 curve를 공유하므로 완벽하게 동기화되어 실행됩니다.

AnimatedAlign은 자식 위젯의 정렬 위치를 애니메이션합니다. Alignment.topLeft에서 Alignment.bottomRight로 변경하면 왼쪽 위에서 오른쪽 아래로 부드럽게 이동합니다.

Curves.bounceOut을 사용하여 도착 지점에서 튕기는 재미있는 효과를 추가했습니다. 이는 주목을 끄는 UI 요소나 튜토리얼 힌트에 유용합니다.

각 버튼의 onPressed에서 setState를 호출하여 상태를 토글합니다. setState는 build 메서드를 다시 실행하고, 새로운 속성 값으로 Animated 위젯들이 재생성되면서 애니메이션이 트리거됩니다.

이 패턴이 Flutter 상태 관리와 애니메이션의 기본 결합 방식입니다. 다른 유용한 암시적 애니메이션 위젯들도 있습니다.

AnimatedPadding은 패딩 변경, AnimatedPositioned는 Stack 내 위치 변경, AnimatedPhysicalModel은 그림자와 elevation 변경, AnimatedDefaultTextStyle은 텍스트 스타일 변경을 애니메이션합니다. 여러분이 이 위젯들을 활용하면 대부분의 일상적인 애니메이션을 복잡한 컨트롤러 없이 몇 줄로 해결할 수 있습니다.

조건부 렌더링, 상태 기반 UI 변경, 사용자 인터랙션 피드백 등 다양한 곳에서 즉시 적용할 수 있습니다.

실전 팁

💡 AnimatedSwitcher는 자식 위젯이 완전히 바뀔 때 크로스페이드 효과를 줍니다. 조건부 렌더링에 매우 유용합니다.

💡 모든 Animated 위젯은 onEnd 콜백을 지원합니다. 애니메이션 완료 후 다음 액션을 연결하세요.

💡 AnimatedCrossFade는 두 위젯 간 전환에 특화되어 있습니다. 탭 바의 아이콘 변경 등에 완벽합니다.

💡 성능을 위해 const 생성자를 사용할 수 없지만, 자식 위젯은 별도 변수로 추출하여 재사용하세요.

💡 duration을 너무 짧게(100ms 미만)하면 애니메이션을 인지하기 어렵고, 너무 길면(1초 이상) 답답합니다. 300-500ms가 적절합니다.


#Flutter#Animation#Intermediate#Transition#CustomWidget

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Riverpod 3.0 쇼핑 앱 종합 프로젝트 완벽 가이드

Flutter와 Riverpod 3.0을 활용한 실무 수준의 쇼핑 앱 개발 과정을 단계별로 학습합니다. 상품 목록, 장바구니, 주문, 인증, 검색 기능까지 모든 핵심 기능을 구현하며 상태 관리의 실전 노하우를 익힙니다.

Riverpod 3.0 Retry 자동 재시도 완벽 가이드

Riverpod 3.0에 새로 추가된 Retry 기능을 활용하여 네트워크 오류나 일시적인 실패 상황에서 자동으로 재시도하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.

Riverpod 3.0 requireValue로 Provider 결합하기

Riverpod 3.0에 새로 추가된 requireValue를 활용하여 여러 Provider의 데이터를 효율적으로 결합하는 방법을 배웁니다. 비동기 데이터를 마치 동기 데이터처럼 다루는 실전 패턴을 소개합니다.

Flutter 3.0 Offline 데이터 영속화 완벽 가이드

Flutter 3.0에서 새롭게 추가된 Offline 데이터 영속화 기능을 배웁니다. Storage 인터페이스부터 SharedPreferences 활용, 실전 예제까지 실무에서 바로 사용할 수 있는 패턴을 배워봅시다.

Riverpod 3.0 Mutation으로 폼 제출 완벽 가이드

Riverpod 3.0의 새로운 Mutation 기능으로 로그인과 회원가입 폼을 우아하게 처리하는 방법을 배웁니다. 로딩 상태, 에러 처리, 성공 처리까지 실무에서 바로 쓸 수 있는 패턴을 익혀보세요.

이전2/2
다음