Flutter에서 Lottie 애니메이션을 어떻게 통합하나요?

질문

Flutter에서 Lottie 애니메이션을 어떻게 통합하고 제어하나요?

답변

Lottie는 Airbnb에서 개발한 오픈 소스 애니메이션 라이브러리로, Adobe After Effects에서 만든 애니메이션을 JSON 형식으로 변환하여 다양한 플랫폼에서 렌더링할 수 있게 해줍니다. Flutter에서는 lottie 패키지를 사용하여 이러한 애니메이션을 쉽게 통합하고 제어할 수 있습니다.

Lottie 애니메이션의 장점

  1. 고품질 애니메이션: After Effects의 강력한 애니메이션 도구를 활용한 결과물
  2. 작은 파일 크기: 벡터 기반이므로 이미지보다 파일 크기가 작음
  3. 다양한 제어 기능: 재생, 일시 정지, 속도 조절 등 프로그래밍 방식 제어 가능
  4. 디자이너-개발자 협업: 디자이너의 작업물을 코드 수정 없이 바로 적용 가능
  5. 크로스 플랫폼: 동일한 애니메이션 파일을 iOS, Android, 웹 등에서 사용 가능

Flutter에서 Lottie 통합하기

1. 패키지 추가

pubspec.yaml 파일에 lottie 패키지를 추가합니다:

dependencies:
  flutter:
    sdk: flutter
  lottie: ^2.6.0 # 최신 버전 확인하세요

그리고 패키지를 가져옵니다:

import 'package:lottie/lottie.dart';

2. Lottie 파일 추가

Lottie 애니메이션 파일(.json)을 프로젝트의 assets 폴더에 추가하고, pubspec.yaml에 등록합니다:

flutter:
  assets:
    - assets/animations/

3. 기본 Lottie 애니메이션 표시하기

가장 간단한 방법으로 Lottie 애니메이션을 표시하는 방법:

class SimpleLottieAnimation extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Lottie.asset(
          'assets/animations/animation.json',
          width: 200,
          height: 200,
          fit: BoxFit.fill,
        ),
      ),
    );
  }
}

4. 다양한 소스에서 로드하기

Lottie는 여러 소스에서 애니메이션을 로드할 수 있습니다:

// 앱 애셋에서 로드
Lottie.asset('assets/animations/animation.json')

// 네트워크에서 로드
Lottie.network('https://assets.example.com/animation.json')

// 파일에서 로드
Lottie.file(File('/path/to/animation.json'))

// 메모리에서 로드
Lottie.memory(Uint8List(...))

Lottie 애니메이션 제어하기

1. 재생 제어

LottieControllerAnimationController를 사용하여 애니메이션을 제어할 수 있습니다:

class ControlledLottieAnimation extends StatefulWidget {
  @override
  _ControlledLottieAnimationState createState() => _ControlledLottieAnimationState();
}

class _ControlledLottieAnimationState extends State<ControlledLottieAnimation>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2), // 애니메이션 기본 재생 시간
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Lottie.asset(
              'assets/animations/animation.json',
              controller: _controller,
              width: 200,
              height: 200,
              onLoaded: (composition) {
                // composition을 사용하여 애니메이션 정보에 접근
                // 애니메이션 파일의 실제 재생 시간으로 컨트롤러 업데이트
                _controller.duration = composition.duration;
              },
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    _controller.forward(); // 애니메이션 재생
                  },
                  child: Text('재생'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    _controller.stop(); // 애니메이션 정지
                  },
                  child: Text('정지'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    _controller.reset(); // 애니메이션 초기화
                  },
                  child: Text('리셋'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

2. 반복 애니메이션

애니메이션을 반복 재생하고 싶다면:

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

  // 무한 반복
  _controller.repeat();

  // 또는 특정 횟수만큼 반복
  // _controller.repeat(min: 0, max: 1, period: _controller.duration, reverse: false, count: 3);
}

3. 특정 프레임 범위 재생

애니메이션의 특정 부분만 재생하기:

// 0%에서 50%까지만 재생
_controller.animateTo(0.5);

// 30%에서 70%까지 재생
_controller.animateFrom(0.3);
_controller.animateTo(0.7);

4. 재생 속도 제어

애니메이션 속도를 조절할 수 있습니다:

// 2배 빠른 재생
_controller.duration = Duration(milliseconds: composition.duration.inMilliseconds ~/ 2);

// 0.5배 느린 재생
_controller.duration = Duration(milliseconds: composition.duration.inMilliseconds * 2);

인터랙티브 Lottie 애니메이션

사용자 상호작용에 따라 Lottie 애니메이션을 제어하는 예제:

class InteractiveLottie extends StatefulWidget {
  @override
  _InteractiveLottieState createState() => _InteractiveLottieState();
}

class _InteractiveLottieState extends State<InteractiveLottie>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;
  double _dragValue = 0;
  bool _isDragging = false;

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

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

  void _handleDragUpdate(DragUpdateDetails details) {
    setState(() {
      _isDragging = true;
      // 드래그 위치를 0-1 범위의 값으로 변환
      _dragValue += details.primaryDelta! / 300;
      _dragValue = _dragValue.clamp(0.0, 1.0);

      // 컨트롤러 값 업데이트
      _controller.value = _dragValue;
    });
  }

  void _handleDragEnd(DragEndDetails details) {
    setState(() {
      _isDragging = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            GestureDetector(
              onVerticalDragUpdate: _handleDragUpdate,
              onVerticalDragEnd: _handleDragEnd,
              child: Container(
                width: 300,
                height: 300,
                color: Colors.transparent,
                child: Lottie.asset(
                  'assets/animations/interactive.json',
                  controller: _controller,
                ),
              ),
            ),
            SizedBox(height: 20),
            Text(
              '위/아래로 드래그하여 애니메이션 제어',
              style: TextStyle(fontSize: 16),
            ),
            SizedBox(height: 10),
            Text(
              '진행률: ${(_dragValue * 100).toInt()}%',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
          ],
        ),
      ),
    );
  }
}

상태 기반 애니메이션

앱 상태에 따라 다른 Lottie 애니메이션을 표시하는 예제:

enum LoadingState { initial, loading, success, error }

class StatefulLottieExample extends StatefulWidget {
  @override
  _StatefulLottieExampleState createState() => _StatefulLottieExampleState();
}

class _StatefulLottieExampleState extends State<StatefulLottieExample>
    with SingleTickerProviderStateMixin {

  LoadingState _state = LoadingState.initial;
  late AnimationController _controller;

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

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

  void _simulateLoading() async {
    setState(() {
      _state = LoadingState.loading;
      _controller.repeat();
    });

    // 로딩 시뮬레이션
    await Future.delayed(Duration(seconds: 3));

    // 랜덤으로 성공/실패 결정
    final random = Random();
    final isSuccess = random.nextBool();

    setState(() {
      _state = isSuccess ? LoadingState.success : LoadingState.error;
      _controller.reset();
      _controller.forward();
    });
  }

  String _getLottieAsset() {
    switch (_state) {
      case LoadingState.initial:
        return 'assets/animations/initial.json';
      case LoadingState.loading:
        return 'assets/animations/loading.json';
      case LoadingState.success:
        return 'assets/animations/success.json';
      case LoadingState.error:
        return 'assets/animations/error.json';
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Lottie.asset(
              _getLottieAsset(),
              controller: _state == LoadingState.loading ? _controller : null,
              width: 200,
              height: 200,
              onLoaded: (composition) {
                if (_state != LoadingState.loading) {
                  _controller.duration = composition.duration;
                }
              },
            ),
            SizedBox(height: 20),
            Text(
              _getStateText(),
              style: TextStyle(fontSize: 18),
            ),
            SizedBox(height: 20),
            if (_state == LoadingState.initial || _state == LoadingState.success || _state == LoadingState.error)
              ElevatedButton(
                onPressed: _simulateLoading,
                child: Text('데이터 로드'),
              ),
          ],
        ),
      ),
    );
  }

  String _getStateText() {
    switch (_state) {
      case LoadingState.initial:
        return '시작하려면 버튼을 누르세요';
      case LoadingState.loading:
        return '로딩 중...';
      case LoadingState.success:
        return '성공!';
      case LoadingState.error:
        return '오류가 발생했습니다';
    }
  }
}

실제 앱 예시: Lottie 애니메이션으로 온보딩 화면 만들기

class LottieOnboarding extends StatefulWidget {
  @override
  _LottieOnboardingState createState() => _LottieOnboardingState();
}

class _LottieOnboardingState extends State<LottieOnboarding>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;
  late PageController _pageController;
  int _currentPage = 0;

  final List<OnboardingItem> _items = [
    OnboardingItem(
      title: '환영합니다',
      description: '우리 앱을 사용해 주셔서 감사합니다!',
      lottieAsset: 'assets/animations/welcome.json',
    ),
    OnboardingItem(
      title: '쉬운 사용법',
      description: '단 몇 번의 탭으로 원하는 작업을 수행하세요.',
      lottieAsset: 'assets/animations/easy_use.json',
    ),
    OnboardingItem(
      title: '안전한 관리',
      description: '당신의 데이터는 안전하게 보호됩니다.',
      lottieAsset: 'assets/animations/secure.json',
    ),
  ];

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    _pageController = PageController();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: PageView.builder(
                controller: _pageController,
                itemCount: _items.length,
                onPageChanged: (index) {
                  setState(() {
                    _currentPage = index;
                  });
                },
                itemBuilder: (context, index) {
                  return _buildPage(_items[index]);
                },
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(20.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  // 페이지 인디케이터
                  Row(
                    children: List.generate(
                      _items.length,
                      (index) => Container(
                        margin: EdgeInsets.only(right: 8),
                        width: 12,
                        height: 12,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: _currentPage == index
                              ? Colors.blue
                              : Colors.grey.shade300,
                        ),
                      ),
                    ),
                  ),

                  // 다음/완료 버튼
                  ElevatedButton(
                    onPressed: () {
                      if (_currentPage < _items.length - 1) {
                        _pageController.nextPage(
                          duration: Duration(milliseconds: 300),
                          curve: Curves.easeInOut,
                        );
                      } else {
                        Navigator.of(context).pushReplacement(
                          MaterialPageRoute(builder: (_) => HomeScreen()),
                        );
                      }
                    },
                    child: Text(
                      _currentPage < _items.length - 1 ? '다음' : '시작하기',
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPage(OnboardingItem item) {
    return Padding(
      padding: const EdgeInsets.all(20.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Lottie.asset(
            item.lottieAsset,
            width: 300,
            height: 300,
            controller: _controller,
            onLoaded: (composition) {
              _controller.duration = composition.duration;
              _controller.forward();
            },
          ),
          SizedBox(height: 40),
          Text(
            item.title,
            style: TextStyle(
              fontSize: 28,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 20),
          Text(
            item.description,
            textAlign: TextAlign.center,
            style: TextStyle(
              fontSize: 18,
              color: Colors.grey.shade600,
            ),
          ),
        ],
      ),
    );
  }
}

class OnboardingItem {
  final String title;
  final String description;
  final String lottieAsset;

  OnboardingItem({
    required this.title,
    required this.description,
    required this.lottieAsset,
  });
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('홈')),
      body: Center(child: Text('메인 화면')),
    );
  }
}

다양한 애니메이션 리소스 찾기

Lottie 애니메이션은 다양한 소스에서 찾을 수 있습니다:

  1. LottieFiles: LottieFiles는 무료 및 유료 Lottie 애니메이션을 제공하는 가장 인기 있는 사이트입니다.
  2. Icons8: Icons8에서 애니메이션 아이콘을 찾을 수 있습니다.
  3. 2dimensions: 2dimensions에서 Flare/Rive 애니메이션을 구할 수 있습니다.
  4. After Effects에서 직접 제작: Adobe After Effects와 Bodymovin 플러그인을 사용하여 직접 애니메이션을 만들 수 있습니다.

Lottie 애니메이션 최적화 팁

  1. 파일 크기 최적화: 애니메이션 복잡성과 레이어 수를 최소화하세요.
  2. 캐싱 사용: 반복적으로 사용되는 애니메이션은 캐싱하세요.
  3. 적절한 크기 사용: 필요 이상으로 큰 애니메이션을 사용하지 마세요.
  4. 재생 제어: 필요할 때만 애니메이션을 재생하고, 사용하지 않을 때는 일시 중지하세요.
  5. 로딩 시간 고려: 대용량 애니메이션은 비동기적으로 로드하여 UI 블로킹을 방지하세요.

문제 해결 및 알려진 이슈

  1. 성능 이슈: 매우 복잡한 Lottie 애니메이션은 성능 저하를 일으킬 수 있습니다. 애니메이션을 단순화하거나 RepaintBoundary로 감싸보세요.
  2. 지원되지 않는 기능: Lottie는 After Effects의 모든 기능을 지원하지 않습니다. 지원되는 기능 목록을 확인하세요.
  3. 버전 호환성: Flutter와 Lottie 패키지 버전 간의 호환성 문제가 있을 수 있습니다. 최신 버전을 사용하세요.
  4. 텍스트 렌더링: Lottie는 After Effects의 텍스트 레이어를 완벽하게 지원하지 않습니다. 텍스트는 별도의 Flutter 위젯으로 구현하는 것이 좋습니다.

결론

Lottie는 Flutter 앱에 고품질 애니메이션을 쉽게 통합할 수 있는 강력한 도구입니다. 기본 사용부터 복잡한 인터랙티브 애니메이션까지, Lottie는 다양한 사용자 경험을 향상시키는 데 활용할 수 있습니다.

디자이너와 개발자 간의 협업을 원활하게 하며, 애니메이션 구현에 필요한 코드를 대폭 줄여주어 개발 시간과 노력을 절약할 수 있습니다. 또한 작은 파일 크기로 고품질의 애니메이션을 제공하므로 앱 크기와 성능에 미치는 영향이 적습니다.

Lottie를 통해 로딩 화면, 온보딩 경험, 피드백 애니메이션, 인터랙티브 UI 요소 등 다양한 애니메이션을 구현하여 Flutter 앱의 사용자 경험을 한 차원 높일 수 있습니다.

results matching ""

    No results matching ""