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

질문

Flutter에서 Rive 애니메이션을 어떻게 통합하고 사용하나요?

답변

Rive(이전의 Flare)는 실시간 인터랙티브 벡터 애니메이션을 위한 강력한 도구로, Flutter 애플리케이션에 복잡하고 멋진 애니메이션을 쉽게 통합할 수 있게 해줍니다. Rive는 애니메이션 디자이너와 개발자 간의 협업을 단순화하여, 디자이너가 만든 고품질 애니메이션을 코드 수정 없이 Flutter 앱에 바로 적용할 수 있습니다.

Rive의 주요 특징

  1. 벡터 기반: 모든 화면 크기에서 선명한 애니메이션
  2. 작은 파일 크기: 최적화된 파일 형식으로 앱 크기 최소화
  3. 런타임 제어: 코드로 애니메이션 제어 가능
  4. 인터랙티브: 사용자 입력에 반응하는 애니메이션 구현 가능
  5. 상태 머신: 복잡한 애니메이션 흐름 제어

Flutter에서 Rive 통합하기

1. 종속성 추가

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

dependencies:
  flutter:
    sdk: flutter
  rive: ^0.11.0 # 최신 버전 확인

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

import 'package:rive/rive.dart';

2. Rive 파일 추가

Rive 웹사이트(https://rive.app)에서 만든 .riv 파일을 프로젝트의 assets 폴더에 추가하고, pubspec.yaml에 등록합니다:

flutter:
  assets:
    - assets/animations/

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

가장 간단한 형태로 Rive 애니메이션을 표시하는 방법:

class SimpleRiveAnimation extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          width: 300,
          height: 300,
          child: RiveAnimation.asset(
            'assets/animations/my_animation.riv',
            fit: BoxFit.contain,
          ),
        ),
      ),
    );
  }
}

4. 애니메이션 상태 제어하기

특정 애니메이션을 재생하거나 제어하려면 StateMachineController 또는 SimpleAnimation을 사용합니다:

class ControlledRiveAnimation extends StatefulWidget {
  @override
  _ControlledRiveAnimationState createState() => _ControlledRiveAnimationState();
}

class _ControlledRiveAnimationState extends State<ControlledRiveAnimation> {
  Artboard? _riveArtboard;
  RiveAnimationController? _controller;

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

    // Rive 파일 로드
    rootBundle.load('assets/animations/my_animation.riv').then(
      (data) async {
        final file = RiveFile.import(data);

        // 파일에서 Artboard 가져오기
        final artboard = file.mainArtboard;

        // 애니메이션 컨트롤러 생성 및 등록
        _controller = SimpleAnimation('idle'); // 'idle'은 Rive에서 정의한 애니메이션 이름
        artboard.addController(_controller!);

        setState(() => _riveArtboard = artboard);
      },
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: _riveArtboard == null
            ? CircularProgressIndicator()
            : Container(
                width: 300,
                height: 300,
                child: Rive(artboard: _riveArtboard!),
              ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 다른 애니메이션으로 전환
          if (_riveArtboard != null) {
            _riveArtboard!.removeController(_controller!);
            _controller?.dispose();
            _controller = SimpleAnimation('walk'); // 'walk'는 다른 애니메이션 이름
            _riveArtboard!.addController(_controller!);
          }
        },
        child: Icon(Icons.refresh),
      ),
    );
  }
}

5. 상태 머신 사용하기

Rive의 상태 머신(State Machine)은 더 복잡한, 상호작용이 가능한 애니메이션을 위한 강력한 도구입니다:

class StateMachineRiveAnimation extends StatefulWidget {
  @override
  _StateMachineRiveAnimationState createState() => _StateMachineRiveAnimationState();
}

class _StateMachineRiveAnimationState extends State<StateMachineRiveAnimation> {
  Artboard? _riveArtboard;
  StateMachineController? _controller;
  SMIInput<bool>? _triggerInput;

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

    rootBundle.load('assets/animations/state_machine.riv').then(
      (data) async {
        final file = RiveFile.import(data);
        final artboard = file.mainArtboard;

        // 상태 머신 컨트롤러 찾기 및 등록
        _controller = StateMachineController.fromArtboard(
          artboard,
          'state_machine_name', // Rive에서 정의한 상태 머신 이름
        );

        if (_controller != null) {
          artboard.addController(_controller!);

          // 입력 찾기 (Rive에서 정의한 입력)
          _triggerInput = _controller!.findInput<bool>('trigger_name');
        }

        setState(() => _riveArtboard = artboard);
      },
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: _riveArtboard == null
            ? CircularProgressIndicator()
            : Container(
                width: 300,
                height: 300,
                child: Rive(artboard: _riveArtboard!),
              ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 상태 머신 트리거 활성화
          if (_triggerInput != null) {
            _triggerInput!.value = true;
          }
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

다양한 입력 유형 다루기

Rive는 여러 유형의 입력을 지원합니다:

// 불리언 입력 (트리거)
SMIInput<bool>? _triggerInput;
_triggerInput = _controller!.findInput<bool>('trigger_name');
_triggerInput?.value = true;

// 숫자 입력
SMIInput<double>? _numberInput;
_numberInput = _controller!.findInput<double>('number_name');
_numberInput?.value = 0.5;

// 트리거 입력 (한 번만 활성화되고 자동으로 재설정)
SMITrigger? _trigger;
_trigger = _controller!.findSMI<SMITrigger>('trigger_name');
_trigger?.fire();

사용자 상호작용 처리하기

사용자 제스처에 반응하는 애니메이션을 만들 수 있습니다:

GestureDetector(
  onTap: () {
    if (_triggerInput != null) {
      _triggerInput!.value = true;
    }
  },
  onPanUpdate: (details) {
    if (_numberInput != null) {
      // 드래그 거리를 입력 값으로 변환
      _numberInput!.value = details.delta.dx / 100;
    }
  },
  child: Container(
    width: 300,
    height: 300,
    child: Rive(artboard: _riveArtboard!),
  ),
)

네트워크에서 Rive 파일 로드하기

원격 서버에서 Rive 파일을 로드할 수도 있습니다:

RiveAnimation.network(
  'https://example.com/animations/my_animation.riv',
  fit: BoxFit.cover,
  placeHolder: CircularProgressIndicator(),
)

애니메이션 이벤트 감지하기

Rive 애니메이션의 특정 이벤트를 감지하고 반응할 수 있습니다:

void _setupStateChangeListener() {
  if (_controller == null) return;

  _controller!.onStateChange = (stateMachineName, stateName) {
    print('State Changed in $stateMachineName to $stateName');

    if (stateName == 'success') {
      // 성공 상태에 도달했을 때 처리
      showSuccessDialog();
    }
  };
}

실제 애플리케이션 예시: 로딩 화면

Rive를 사용한 맞춤형 로딩 화면을 만들어 보겠습니다:

class RiveLoadingScreen extends StatefulWidget {
  @override
  _RiveLoadingScreenState createState() => _RiveLoadingScreenState();
}

class _RiveLoadingScreenState extends State<RiveLoadingScreen> {
  Artboard? _riveArtboard;
  RiveAnimationController? _controller;
  bool _isLoading = true;

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

    // 데이터 로딩 시뮬레이션
    Future.delayed(Duration(seconds: 5), () {
      if (mounted) {
        setState(() {
          _isLoading = false;
          _changeAnimationState('success');
        });
      }
    });
  }

  void _loadRiveFile() async {
    final data = await rootBundle.load('assets/animations/loading.riv');
    final file = RiveFile.import(data);
    final artboard = file.mainArtboard;

    _controller = SimpleAnimation('loading');
    artboard.addController(_controller!);

    setState(() => _riveArtboard = artboard);
  }

  void _changeAnimationState(String animationName) {
    if (_riveArtboard == null) return;

    _riveArtboard!.removeController(_controller!);
    _controller?.dispose();
    _controller = SimpleAnimation(animationName);
    _riveArtboard!.addController(_controller!);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _riveArtboard == null
                ? CircularProgressIndicator()
                : Container(
                    width: 200,
                    height: 200,
                    child: Rive(artboard: _riveArtboard!),
                  ),
            SizedBox(height: 20),
            Text(
              _isLoading ? '데이터 로딩 중...' : '로딩 완료!',
              style: TextStyle(fontSize: 18),
            ),
            if (!_isLoading)
              Padding(
                padding: const EdgeInsets.only(top: 20),
                child: ElevatedButton(
                  onPressed: () {
                    Navigator.of(context).pushReplacement(
                      MaterialPageRoute(builder: (_) => HomeScreen()),
                    );
                  },
                  child: Text('계속하기'),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

로그인 버튼 애니메이션 예시

Rive를 사용하여 인터랙티브한 로그인 버튼을 만들어 보겠습니다:

class AnimatedLoginButton extends StatefulWidget {
  final VoidCallback onPressed;

  AnimatedLoginButton({required this.onPressed});

  @override
  _AnimatedLoginButtonState createState() => _AnimatedLoginButtonState();
}

class _AnimatedLoginButtonState extends State<AnimatedLoginButton> {
  Artboard? _riveArtboard;
  StateMachineController? _controller;
  SMIInput<bool>? _pressInput;
  SMIInput<bool>? _loadingInput;
  SMIInput<bool>? _successInput;
  bool _isLoading = false;

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

  void _loadRiveFile() async {
    final data = await rootBundle.load('assets/animations/login_button.riv');
    final file = RiveFile.import(data);
    final artboard = file.mainArtboard;

    _controller = StateMachineController.fromArtboard(
      artboard,
      'login_state_machine',
    );

    if (_controller != null) {
      artboard.addController(_controller!);

      _pressInput = _controller!.findInput<bool>('press');
      _loadingInput = _controller!.findInput<bool>('loading');
      _successInput = _controller!.findInput<bool>('success');
    }

    setState(() => _riveArtboard = artboard);
  }

  void _performLogin() async {
    if (_isLoading) return;

    // 버튼 누름 애니메이션
    _pressInput?.value = true;

    // 지연 후 로딩 시작
    await Future.delayed(Duration(milliseconds: 300));

    setState(() => _isLoading = true);
    _loadingInput?.value = true;

    // 로그인 로직 시뮬레이션
    await Future.delayed(Duration(seconds: 2));

    // 성공 애니메이션
    _loadingInput?.value = false;
    _successInput?.value = true;

    await Future.delayed(Duration(seconds: 1));

    widget.onPressed();

    // 상태 리셋
    setState(() => _isLoading = false);
    _successInput?.value = false;
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _isLoading ? null : _performLogin,
      child: Container(
        width: 200,
        height: 60,
        child: _riveArtboard == null
            ? Container(
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(30),
                ),
                child: Center(child: Text('로그인', style: TextStyle(color: Colors.white))),
              )
            : Rive(artboard: _riveArtboard!),
      ),
    );
  }
}

애니메이션 성능 최적화 팁

  1. 적절한 크기 사용: Rive 애니메이션의 표시 크기를 필요 이상으로 크게 설정하지 마세요.
  2. 복잡성 관리: 너무 복잡한 애니메이션은 성능에 영향을 줄 수 있습니다.
  3. 필요할 때만 컨트롤러 활성화: 화면에 보이지 않을 때는 컨트롤러를 일시 중지하거나 제거하세요.
  4. 캐싱 활용: 자주 사용하는 애니메이션은 한 번 로드하여 재사용하세요.
  5. 불필요한 상태 변경 최소화: 과도한 setState 호출을 피하세요.

Rive 애니메이션 디자인 팁

  1. 적절한 네이밍: 애니메이션, 상태 머신, 입력에 명확한 이름을 사용하세요.
  2. 모듈식 설계: 애니메이션을 재사용 가능한 컴포넌트로 구성하세요.
  3. 상태 머신 활용: 복잡한 상호작용은 상태 머신을 사용해 관리하세요.
  4. 경량화: 불필요한 복잡성을 제거하여 파일 크기를 최소화하세요.
  5. 반응형 설계: 다양한 화면 크기에 적응할 수 있도록 애니메이션을 디자인하세요.

결론

Rive는 Flutter 애플리케이션에 고품질 애니메이션을 쉽게 통합할 수 있는 강력한 도구입니다. 기본 애니메이션부터 복잡한 인터랙티브 경험까지, Rive는 앱의 시각적 매력과 사용자 경험을 향상시키는 다양한 가능성을 제공합니다.

디자이너와 개발자 간의 효율적인 협업을 지원하고, 작은 파일 크기로 복잡한 애니메이션을 구현할 수 있어 앱의 성능에 큰 부담을 주지 않으면서도 멋진 시각적 효과를 만들 수 있습니다.

Rive와 Flutter의 조합은 애니메이션 구현의 복잡성을 크게 줄이면서, 앱에 감각적인 움직임과 상호작용을 추가할 수 있는 이상적인 방법입니다.

results matching ""

    No results matching ""