Flutter에서 기본 애니메이션을 어떻게 만드나요?
질문
Flutter에서 기본 애니메이션을 어떻게 만드나요?
답변
Flutter에서는 두 가지 주요 애니메이션 접근 방식이 있습니다: 암시적(implicit) 애니메이션과 명시적(explicit) 애니메이션입니다. 각 방식은 서로 다른 사용 사례에 적합합니다.
1. 암시적(Implicit) 애니메이션
암시적 애니메이션은 Animated
로 시작하는 위젯을 사용하여 간단하게 구현할 수 있습니다. 이러한 위젯은 속성 값이 변경될 때 자동으로 애니메이션을 적용합니다.
예시: AnimatedContainer
class MyAnimationWidget extends StatefulWidget {
@override
_MyAnimationWidgetState createState() => _MyAnimationWidgetState();
}
class _MyAnimationWidgetState extends State<MyAnimationWidget> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: AnimatedContainer(
width: _isExpanded ? 200.0 : 100.0,
height: _isExpanded ? 200.0 : 100.0,
color: _isExpanded ? Colors.blue : Colors.red,
alignment: _isExpanded ? Alignment.center : Alignment.topLeft,
duration: Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn,
child: FlutterLogo(size: 50),
),
);
}
}
이 예시에서는 컨테이너를 탭하면 크기, 색상, 정렬이 애니메이션되면서 변경됩니다.
주요 암시적 애니메이션 위젯
AnimatedContainer
: 컨테이너의 속성 변화에 애니메이션 적용AnimatedOpacity
: 투명도 변화에 애니메이션 적용AnimatedPadding
: 패딩 변화에 애니메이션 적용AnimatedPositioned
: 위치 변화에 애니메이션 적용AnimatedCrossFade
: 두 위젯 간의 교차 페이드 애니메이션AnimatedSwitcher
: 자식 위젯 교체 시 애니메이션 적용AnimatedSize
: 크기 변화에 애니메이션 적용AnimatedAlign
: 정렬 변화에 애니메이션 적용
예시: 여러 애니메이션 속성 조합
AnimatedContainer(
duration: Duration(milliseconds: 800),
width: _isExpanded ? 200.0 : 100.0,
height: _isExpanded ? 200.0 : 100.0,
padding: _isExpanded
? EdgeInsets.all(20)
: EdgeInsets.all(10),
decoration: BoxDecoration(
color: _isExpanded ? Colors.blue : Colors.red,
borderRadius: _isExpanded
? BorderRadius.circular(20)
: BorderRadius.circular(5),
boxShadow: _isExpanded
? [BoxShadow(blurRadius: 10, color: Colors.black45)]
: [],
),
transform: _isExpanded
? Matrix4.rotationZ(0.1)
: Matrix4.rotationZ(0),
curve: Curves.easeInOutBack,
child: FlutterLogo(),
)
AnimatedSwitcher 사용 예시
위젯 전환에 애니메이션을 적용하려면:
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: _isExpanded
? Container(
key: ValueKey('container1'),
color: Colors.blue,
height: 200,
width: 200,
)
: Container(
key: ValueKey('container2'),
color: Colors.red,
height: 100,
width: 100,
),
)
2. 명시적(Explicit) 애니메이션
명시적 애니메이션은 더 세밀한 제어가 필요한 경우 사용합니다. 이를 위해 AnimationController
와 Animation
객체를 직접 만들고 관리해야 합니다.
기본 명시적 애니메이션 예시
class ExplicitAnimationDemo extends StatefulWidget {
@override
_ExplicitAnimationDemoState createState() => _ExplicitAnimationDemoState();
}
class _ExplicitAnimationDemoState extends State<ExplicitAnimationDemo>
with SingleTickerProviderStateMixin { // TickerProvider 믹스인 추가
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
// 애니메이션 컨트롤러 초기화
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this, // 틱 제공자
);
// 애니메이션 생성 및 구성
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
),
);
// 애니메이션 시작
_controller.forward();
}
@override
void dispose() {
_controller.dispose(); // 컨트롤러 자원 해제
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('명시적 애니메이션')),
body: Center(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: child,
);
},
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: Center(
child: Text(
'애니메이션',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Icon(Icons.play_arrow),
),
);
}
}
애니메이션 컨트롤러 메서드
애니메이션 컨트롤러는 다양한 제어 메서드를 제공합니다:
forward()
: 애니메이션 시작 또는 재개reverse()
: 애니메이션 역방향 재생repeat()
: 애니메이션 반복reset()
: 애니메이션 초기 상태로 리셋stop()
: 애니메이션 정지animateTo(double target)
: 특정 값으로 애니메이션 진행animateBack(double target)
: 특정 값으로 역방향 애니메이션 진행
Tween 사용 예시
다양한 값 유형에 애니메이션을 적용할 수 있습니다:
// 더블 값 애니메이션
Animation<double> _sizeAnimation = Tween<double>(begin: 100, end: 200).animate(_controller);
// 색상 애니메이션
Animation<Color?> _colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller);
// 오프셋 애니메이션
Animation<Offset> _offsetAnimation = Tween<Offset>(
begin: Offset.zero,
end: Offset(1.5, 0),
).animate(_controller);
// 정수 애니메이션
Animation<int> _intAnimation = IntTween(begin: 0, end: 100).animate(_controller);
여러 애니메이션 속성 적용하기
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
color: _colorAnimation.value,
transform: Matrix4.translationValues(
_offsetAnimation.value.dx * 100,
0,
0,
),
child: Center(
child: Text(
'${_intAnimation.value}%',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
);
},
);
}
3. 애니메이션 리스너 사용하기
애니메이션 진행 상태를 감지하고 그에 따라 로직을 실행할 수 있습니다:
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print('애니메이션 완료');
// 애니메이션 완료 시 실행할 작업
} else if (status == AnimationStatus.dismissed) {
print('애니메이션 초기 상태');
// 애니메이션이 초기 상태로 돌아왔을 때 실행할 작업
}
});
_controller.addListener(() {
// 애니메이션 값이 변경될 때마다 호출됨
print('현재 애니메이션 값: ${_controller.value}');
setState(() {
// 필요한 경우 상태 업데이트
});
});
4. 여러 애니메이션 조합하기
복잡한 애니메이션 효과를 만들기 위해 여러 애니메이션을 조합할 수 있습니다.
순차 애니메이션 (Sequential Animations)
// 첫 번째 애니메이션 (0%-50%)
final firstAnimation = CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeOut),
);
// 두 번째 애니메이션 (50%-100%)
final secondAnimation = CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0, curve: Curves.easeIn),
);
// 크기 애니메이션 (첫 번째 반절 동안)
final sizeAnimation = Tween<double>(begin: 100, end: 200).animate(firstAnimation);
// 색상 애니메이션 (두 번째 반절 동안)
final colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(secondAnimation);
5. 트랜지션 위젯 사용하기
Flutter는 일반적인 애니메이션 효과를 쉽게 구현할 수 있는 여러 트랜지션 위젯을 제공합니다:
// 페이드 효과
FadeTransition(
opacity: _animation,
child: Container(color: Colors.blue, width: 200, height: 200),
)
// 크기 조절 효과
ScaleTransition(
scale: _animation,
child: Container(color: Colors.green, width: 200, height: 200),
)
// 회전 효과
RotationTransition(
turns: _animation,
child: Container(color: Colors.orange, width: 200, height: 200),
)
// 위치 이동 효과
SlideTransition(
position: _offsetAnimation,
child: Container(color: Colors.purple, width: 200, height: 200),
)
// 크기 조절 효과 (조정 가능)
SizeTransition(
sizeFactor: _animation,
axis: Axis.horizontal,
child: Container(color: Colors.red, width: 200, height: 200),
)
6. 애니메이션 효과 커스텀하기
커스텀 커브를 정의하여 독특한 애니메이션 효과를 만들 수 있습니다:
class CustomBounce extends Curve {
final double a;
final double b;
CustomBounce({this.a = 0.5, this.b = 2.0});
@override
double transformInternal(double t) {
if (t < a) {
return b * t * t;
} else {
t = t - a;
t = 1.0 - t;
return 1.0 - b * t * t;
}
}
}
// 사용 예시
_animation = CurvedAnimation(
parent: _controller,
curve: CustomBounce(a: 0.3, b: 1.5),
);
완전한 예제: 복합 애니메이션
다음은 여러 애니메이션 효과를 조합한 완전한 예제입니다:
class ComplexAnimationDemo extends StatefulWidget {
@override
_ComplexAnimationDemoState createState() => _ComplexAnimationDemoState();
}
class _ComplexAnimationDemoState extends State<ComplexAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
late Animation<Color?> _colorAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 3),
vsync: this,
);
// 크기 애니메이션 (0%-60%)
_scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.6, curve: Curves.elasticOut),
),
);
// 회전 애니메이션 (20%-80%)
_rotationAnimation = Tween<double>(begin: 0.0, end: 2.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.2, 0.8, curve: Curves.easeInOut),
),
);
// 색상 애니메이션 (40%-100%)
_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.purple).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.4, 1.0, curve: Curves.easeIn),
),
);
// 슬라이드 애니메이션 (60%-100%)
_slideAnimation = Tween<Offset>(begin: Offset.zero, end: Offset(0.2, 0)).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.6, 1.0, curve: Curves.easeInOut),
),
);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
Future.delayed(Duration(milliseconds: 500), () {
_controller.reverse();
});
} else if (status == AnimationStatus.dismissed) {
Future.delayed(Duration(milliseconds: 500), () {
_controller.forward();
});
}
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('복합 애니메이션')),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return SlideTransition(
position: _slideAnimation,
child: Transform.rotate(
angle: _rotationAnimation.value * 3.14,
child: Transform.scale(
scale: _scaleAnimation.value,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: _colorAnimation.value,
borderRadius: BorderRadius.circular(20 * _scaleAnimation.value),
),
child: Center(
child: Text(
'애니메이션',
style: TextStyle(
color: Colors.white,
fontSize: 24 * _scaleAnimation.value,
),
),
),
),
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_controller.isAnimating) {
_controller.stop();
} else {
if (_controller.status == AnimationStatus.forward) {
_controller.forward();
} else {
_controller.reverse();
}
}
},
child: Icon(_controller.isAnimating ? Icons.pause : Icons.play_arrow),
),
);
}
}
7. 애니메이션 성능 최적화 팁
- RepaintBoundary 사용: 애니메이션이 있는 위젯을 RepaintBoundary로 감싸 다시 그려지는 영역을 제한합니다.
RepaintBoundary(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) { ... },
),
)
리소스 관리: 반드시 dispose() 메서드에서 애니메이션 컨트롤러를 해제하세요.
상수 자식 사용: AnimatedBuilder의 child 인자를 사용하여 불필요한 재빌드를 방지하세요.
불필요한 setState() 호출 피하기: 애니메이션 값이 변경될 때마다 setState()를 호출하지 마세요. 대신 AnimatedBuilder 또는 AnimatedWidget을 사용하세요.
결론
Flutter는 다양한 애니메이션 기법을 제공하여 매력적인 사용자 경험을 만들 수 있게 합니다. 간단한 애니메이션에는 AnimatedContainer와 같은 암시적 애니메이션 위젯을 사용하고, 더 복잡하거나 세밀한 제어가 필요한 경우 AnimationController와 같은 명시적 애니메이션 도구를 활용하세요.
애니메이션은 애플리케이션의 디자인과 사용자 경험을 크게 향상시킬 수 있지만, 과도하게 사용하면 사용자가 혼란스러울 수 있고 성능에 영향을 줄 수 있습니다. 목적에 맞게 적절히 사용하는 것이 중요합니다.