Flutter에서 Rive 애니메이션을 어떻게 통합하나요?
질문
Flutter에서 Rive 애니메이션을 어떻게 통합하고 사용하나요?
답변
Rive(이전의 Flare)는 실시간 인터랙티브 벡터 애니메이션을 위한 강력한 도구로, Flutter 애플리케이션에 복잡하고 멋진 애니메이션을 쉽게 통합할 수 있게 해줍니다. Rive는 애니메이션 디자이너와 개발자 간의 협업을 단순화하여, 디자이너가 만든 고품질 애니메이션을 코드 수정 없이 Flutter 앱에 바로 적용할 수 있습니다.
Rive의 주요 특징
- 벡터 기반: 모든 화면 크기에서 선명한 애니메이션
- 작은 파일 크기: 최적화된 파일 형식으로 앱 크기 최소화
- 런타임 제어: 코드로 애니메이션 제어 가능
- 인터랙티브: 사용자 입력에 반응하는 애니메이션 구현 가능
- 상태 머신: 복잡한 애니메이션 흐름 제어
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!),
),
);
}
}
애니메이션 성능 최적화 팁
- 적절한 크기 사용: Rive 애니메이션의 표시 크기를 필요 이상으로 크게 설정하지 마세요.
- 복잡성 관리: 너무 복잡한 애니메이션은 성능에 영향을 줄 수 있습니다.
- 필요할 때만 컨트롤러 활성화: 화면에 보이지 않을 때는 컨트롤러를 일시 중지하거나 제거하세요.
- 캐싱 활용: 자주 사용하는 애니메이션은 한 번 로드하여 재사용하세요.
- 불필요한 상태 변경 최소화: 과도한 setState 호출을 피하세요.
Rive 애니메이션 디자인 팁
- 적절한 네이밍: 애니메이션, 상태 머신, 입력에 명확한 이름을 사용하세요.
- 모듈식 설계: 애니메이션을 재사용 가능한 컴포넌트로 구성하세요.
- 상태 머신 활용: 복잡한 상호작용은 상태 머신을 사용해 관리하세요.
- 경량화: 불필요한 복잡성을 제거하여 파일 크기를 최소화하세요.
- 반응형 설계: 다양한 화면 크기에 적응할 수 있도록 애니메이션을 디자인하세요.
결론
Rive는 Flutter 애플리케이션에 고품질 애니메이션을 쉽게 통합할 수 있는 강력한 도구입니다. 기본 애니메이션부터 복잡한 인터랙티브 경험까지, Rive는 앱의 시각적 매력과 사용자 경험을 향상시키는 다양한 가능성을 제공합니다.
디자이너와 개발자 간의 효율적인 협업을 지원하고, 작은 파일 크기로 복잡한 애니메이션을 구현할 수 있어 앱의 성능에 큰 부담을 주지 않으면서도 멋진 시각적 효과를 만들 수 있습니다.
Rive와 Flutter의 조합은 애니메이션 구현의 복잡성을 크게 줄이면서, 앱에 감각적인 움직임과 상호작용을 추가할 수 있는 이상적인 방법입니다.