Flutter에서 애니메이션을 구현하는 방법은 무엇인가요?
질문
Flutter에서 애니메이션을 효과적으로 구현하는 다양한 방법과 적절한 사용 사례에 대해 설명해주세요.
답변
Flutter는 애니메이션을 구현하기 위한 풍부한 API를 제공합니다. 간단한 애니메이션부터 복잡한 애니메이션까지 다양한 수준의 제어와 유연성을 제공하는 여러 가지 방법을 살펴보겠습니다.
1. 암시적(Implicit) 애니메이션
1.1 AnimatedFoo 위젯
Flutter는 Animated
접두사가 붙은 내장 위젯들을 제공하여 속성 변경 시 자동으로 애니메이션 효과를 적용합니다.
class AnimatedWidgetExample extends StatefulWidget {
@override
_AnimatedWidgetExampleState createState() => _AnimatedWidgetExampleState();
}
class _AnimatedWidgetExampleState extends State<AnimatedWidgetExample> {
double _width = 100.0;
double _height = 100.0;
Color _color = Colors.blue;
BorderRadius _borderRadius = BorderRadius.circular(8.0);
void _changeProperties() {
setState(() {
_width = _width == 100.0 ? 200.0 : 100.0;
_height = _height == 100.0 ? 150.0 : 100.0;
_color = _color == Colors.blue ? Colors.red : Colors.blue;
_borderRadius = _borderRadius == BorderRadius.circular(8.0)
? BorderRadius.circular(50.0)
: BorderRadius.circular(8.0);
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
width: _width,
height: _height,
decoration: BoxDecoration(
color: _color,
borderRadius: _borderRadius,
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _changeProperties,
child: Text('애니메이션 토글'),
),
],
);
}
}
1.2 자주 사용되는 암시적 애니메이션 위젯
위젯 | 설명 |
---|---|
AnimatedContainer |
컨테이너의 속성(크기, 색상, 테두리 등) 변경을 애니메이션화 |
AnimatedOpacity |
투명도 변경을 애니메이션화 |
AnimatedPositioned |
Stack 내에서 위치 변경을 애니메이션화 |
AnimatedPadding |
패딩 변경을 애니메이션화 |
AnimatedSwitcher |
자식 위젯 전환을 애니메이션화 |
AnimatedCrossFade |
두 위젯 간 크로스페이드 애니메이션 |
AnimatedSize |
위젯 크기 변경 애니메이션 |
AnimatedDefaultTextStyle |
텍스트 스타일 변경 애니메이션 |
1.3 TweenAnimationBuilder
커스텀 암시적 애니메이션을 만들기 위해 사용:
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
duration: Duration(seconds: 1),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Transform.rotate(
angle: value * 2 * 3.14, // 완전한 한 바퀴 회전
child: child,
);
},
child: FlutterLogo(size: 100),
)
2. 명시적(Explicit) 애니메이션
2.1 AnimationController와 Animation 객체
명시적 애니메이션은 더 정밀한 제어가 필요할 때 사용합니다:
class ExplicitAnimationExample extends StatefulWidget {
@override
_ExplicitAnimationExampleState createState() => _ExplicitAnimationExampleState();
}
class _ExplicitAnimationExampleState extends State<ExplicitAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
),
);
_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
// 애니메이션 반복
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
color: _colorAnimation.value,
);
},
);
}
}
2.2 자주 사용되는 명시적 애니메이션 위젯
위젯 | 설명 |
---|---|
AnimatedBuilder |
애니메이션 객체를 기반으로 위젯 트리를 재구축 |
FadeTransition |
투명도 애니메이션 |
SlideTransition |
슬라이딩 애니메이션 |
ScaleTransition |
크기 조절 애니메이션 |
RotationTransition |
회전 애니메이션 |
PositionedTransition |
Stack 내 위치 애니메이션 |
DecoratedBoxTransition |
장식 속성 애니메이션 |
DefaultTextStyleTransition |
텍스트 스타일 애니메이션 |
2.3 애니메이션 상태 관리
// 애니메이션 시작
_controller.forward();
// 애니메이션 역방향 실행
_controller.reverse();
// 애니메이션 중지
_controller.stop();
// 특정 값으로 애니메이션 이동
_controller.animateTo(0.5);
// 애니메이션 반복
_controller.repeat(reverse: true);
// 애니메이션 상태 리스너
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print('애니메이션 완료');
} else if (status == AnimationStatus.dismissed) {
print('애니메이션 초기 상태');
}
});
// 애니메이션 값 리스너
_controller.addListener(() {
print('현재 값: ${_controller.value}');
});
3. Hero 애니메이션
화면 간 전환 시 공유 요소에 연속성을 부여하는 애니메이션:
// 첫 번째 화면
Hero(
tag: 'logo', // 고유 태그가 필요함
child: FlutterLogo(size: 50),
),
// 두 번째 화면
Hero(
tag: 'logo', // 첫 번째 화면과 동일한 태그
child: FlutterLogo(size: 200),
),
4. 물리 기반 애니메이션
4.1 SpringSimulation
자연스러운 스프링 효과를 위한 물리 기반 애니메이션:
import 'package:flutter/physics.dart';
class SpringExample extends StatefulWidget {
@override
_SpringExampleState createState() => _SpringExampleState();
}
class _SpringExampleState extends State<SpringExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Simulation _simulation;
double _dragStartX = 0.0;
double _offsetX = 0.0;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this)
..addListener(() {
setState(() {
_offsetX = _controller.value;
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _startSpringAnimation(double startVelocity) {
_simulation = SpringSimulation(
SpringDescription(
mass: 1.0,
stiffness: 500.0,
damping: 10.0,
),
_offsetX, // 시작 위치
0.0, // 목표 위치 (중앙)
startVelocity, // 시작 속도
);
_controller.animateWith(_simulation);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return GestureDetector(
onHorizontalDragStart: (details) {
_controller.stop();
_dragStartX = details.localPosition.dx - _offsetX;
},
onHorizontalDragUpdate: (details) {
setState(() {
_offsetX = details.localPosition.dx - _dragStartX;
});
},
onHorizontalDragEnd: (details) {
// 드래그 종료 시 스프링 애니메이션 시작
_startSpringAnimation(details.velocity.pixelsPerSecond.dx / screenWidth);
},
child: Center(
child: Transform.translate(
offset: Offset(_offsetX, 0.0),
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50),
),
),
),
),
);
}
}
4.2 FrictionSimulation
마찰을 적용한 물리 기반 애니메이션:
// 마찰 시뮬레이션 생성
final simulation = FrictionSimulation(
0.5, // 마찰 계수
_offsetX, // 시작 위치
velocity, // 시작 속도
);
// 마찰 시뮬레이션 적용
_controller.animateWith(simulation);
5. 커스텀 페인트 애니메이션
CustomPainter
를 사용한 복잡한 애니메이션:
class WavePainter extends CustomPainter {
final Animation<double> animation;
WavePainter({required this.animation}) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
final path = Path();
final waveHeight = 20.0;
final animValue = animation.value;
path.moveTo(0, size.height / 2);
for (double i = 0; i < size.width; i++) {
path.lineTo(
i,
size.height / 2 + sin((i / size.width * 4 * pi) + (animValue * 2 * pi)) * waveHeight
);
}
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(WavePainter oldDelegate) => true;
}
// 사용
class WaveAnimation extends StatefulWidget {
@override
_WaveAnimationState createState() => _WaveAnimationState();
}
class _WaveAnimationState extends State<WaveAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: WavePainter(animation: _controller),
child: Container(
height: 200,
width: double.infinity,
),
);
}
}
6. 애니메이션 시퀀스 및 복합 애니메이션
6.1 연속 애니메이션
여러 애니메이션을 순차적으로 실행:
Future<void> _playSequentially() async {
await _controller1.forward();
await _controller2.forward();
await _controller3.forward();
// 모든 애니메이션이 완료된 후 초기 상태로 재설정
await _controller3.reverse();
await _controller2.reverse();
await _controller1.reverse();
}
6.2 복합 애니메이션 (Chaining)
Interval
을 사용하여 단일 컨트롤러로 여러 애니메이션 연결:
class ChainedAnimationExample extends StatefulWidget {
@override
_ChainedAnimationExampleState createState() => _ChainedAnimationExampleState();
}
class _ChainedAnimationExampleState extends State<ChainedAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _rotateAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 3),
);
// 0% - 30%: 크기 애니메이션
_scaleAnimation = Tween<double>(begin: 1.0, end: 2.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.3, curve: Curves.easeOut),
),
);
// 30% - 60%: 회전 애니메이션
_rotateAnimation = Tween<double>(begin: 0.0, end: 2 * pi).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.3, 0.6, curve: Curves.easeInOut),
),
);
// 60% - 100%: 색상 애니메이션
_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.6, 1.0, curve: Curves.easeIn),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotateAnimation.value,
child: Container(
width: 100,
height: 100,
color: _colorAnimation.value,
),
),
);
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Text('애니메이션 실행'),
),
],
);
}
}
7. Flare와 Rive (고급 애니메이션 도구)
7.1 Rive 애니메이션 통합
Rive(이전의 Flare)는 더 복잡한 벡터 애니메이션을 구현하기 위한 툴:
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
class RiveAnimationExample extends StatefulWidget {
@override
_RiveAnimationExampleState createState() => _RiveAnimationExampleState();
}
class _RiveAnimationExampleState extends State<RiveAnimationExample> {
// Rive 컨트롤러
RiveAnimationController? _controller;
@override
void initState() {
super.initState();
_controller = SimpleAnimation('idle');
}
@override
Widget build(BuildContext context) {
return RiveAnimation.asset(
'assets/animations/animated_character.riv',
controllers: [_controller!],
onInit: (_) => setState(() {}),
fit: BoxFit.cover,
);
}
}
8. 애니메이션 성능 최적화 방법
8.1 RepaintBoundary 사용
필요한 부분만 다시 그리도록 최적화합니다:
RepaintBoundary(
child: MyAnimatedWidget(),
)
8.2 페인팅 영역 제한
클리핑을 사용하여 애니메이션 효과가 필요한 영역만 다시 그립니다:
ClipRect(
child: MyAnimatedWidget(),
)
8.3 AnimatedBuilder 최적화
불필요한 위젯 재구축을 방지합니다:
// 효율적인 AnimatedBuilder 사용
AnimatedBuilder(
animation: _controller,
// 애니메이션 값에 영향을 받지 않는 자식은 builder 외부에 배치
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * pi,
child: child, // 재사용 가능한 자식 전달
);
},
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)
9. 애니메이션 디버깅 및 프로파일링
9.1 Timelines와 Tracking
Timeline.startSync('애니메이션 디버깅');
// 애니메이션 코드...
Timeline.finishSync();
9.2 플러터 Inspector 사용
Flutter 개발자 도구의 Performance 탭과 Widget Inspector를 사용하여 애니메이션 성능 분석.
10. 모범 사례 및 권장 사항
적절한 애니메이션 유형 선택:
- 간단한 애니메이션은 암시적 애니메이션 위젯 사용
- 복잡한 애니메이션이나 정밀한 제어가 필요한 경우 명시적 애니메이션 사용
- UI 간 전환은 Hero 애니메이션 고려
- 물리적인 느낌이 필요한 경우 물리 기반 애니메이션 사용
애니메이션 기간 가이드라인:
- 아주 작은 애니메이션: 100-200ms
- 일반적인 UI 애니메이션: 200-300ms
- 장면 전환: 300-500ms
- 긴 애니메이션: 최대 1000ms (너무 길면 사용자 경험 저하)
성능 유의사항:
- 애니메이션 동안 무거운 작업 피하기
- 너무 많은 동시 애니메이션 피하기
- 해제된 화면의 AnimationController 반드시 dispose()
- Canvas 렌더링은 최적화가 필요함
결론
Flutter는 다양한 수준의 복잡성을 가진 애니메이션을 구현할 수 있는 강력한 도구를 제공합니다. 간단한 암시적 애니메이션부터 복잡한 물리 기반 애니메이션, Rive와 같은 외부 도구를 활용한 애니메이션까지 필요에 따라 적절한 방법을 선택할 수 있습니다.
애니메이션은 사용자 경험을 향상시키는 중요한 요소이지만, 과도하게 사용하면 오히려 방해가 될 수 있으므로 목적에 맞게 적절히 사용하는 것이 중요합니다. 또한, 성능 문제를 방지하기 위해 애니메이션의 복잡성과 동시에 실행되는 애니메이션의 수를 관리해야 합니다.
적절한 애니메이션 기법을 선택하고 성능 가이드라인을 준수함으로써, 사용자에게 부드럽고 즐거운 인터페이스 경험을 제공할 수 있습니다.