애니메이션 기초
개요
Flitter는 Flutter와 동일한 강력한 애니메이션 시스템을 제공합니다. AnimationController, Tween, Curve를 조합하여 부드럽고 자연스러운 애니메이션을 쉽게 구현할 수 있습니다.
왜 애니메이션이 중요한가?
잘 설계된 애니메이션은 사용자 경험을 크게 향상시킵니다:
- 시각적 피드백: 사용자 액션에 대한 즉각적인 반응 제공
- 상태 전환: 화면이나 요소의 변화를 부드럽게 표현
- 주의 유도: 중요한 정보나 변화에 사용자의 시선을 끌기
- 브랜드 아이덴티티: 독특한 애니메이션으로 앱의 개성 표현
- 인지적 연속성: 요소의 이동이나 변형을 자연스럽게 보여줌
핵심 개념
1. AnimationController
애니메이션의 재생, 정지, 반복 등을 제어하는 핵심 클래스입니다:
class MyWidgetState extends State<MyWidget> {
controller!: AnimationController;
initState(context: BuildContext) {
super.initState(context);
// duration은 밀리초 단위로 지정
this.controller = new AnimationController({
duration: 1000 // 1초
});
}
dispose() {
this.controller.dispose();
super.dispose();
}
}
2. Tween
시작값과 끝값 사이의 보간을 정의합니다:
// 숫자 Tween
const sizeTween = new Tween({
begin: 50,
end: 200
});
// 색상 Tween
const colorTween = new ColorTween({
begin: '#3b82f6',
end: '#ef4444'
});
// Offset Tween (위치 이동)
const positionTween = new Tween({
begin: new Offset(0, 0),
end: new Offset(100, 100)
});
3. Animation
Tween과 AnimationController를 연결하여 실제 애니메이션 값을 생성합니다:
const animation = sizeTween.animated(this.controller);
// CurvedAnimation으로 이징 효과 적용
const curvedAnimation = new CurvedAnimation({
parent: this.controller,
curve: Curves.easeInOut
});
const animation = sizeTween.animated(curvedAnimation);
4. 애니메이션 리스너
애니메이션 값이 변경될 때마다 위젯을 재빌드하려면 리스너를 추가합니다:
class MyWidgetState extends State<MyWidget> {
controller!: AnimationController;
animation!: Animation<number>;
initState(context: BuildContext) {
super.initState(context);
this.controller = new AnimationController({ duration: 1000 });
this.animation = new Tween({ begin: 0, end: 100 }).animated(this.controller);
// 리스너 추가 - 애니메이션 값이 변경될 때마다 setState 호출
this.controller.addListener(() => {
this.setState();
});
}
build(context: BuildContext): Widget {
return Container({
width: this.animation.value,
height: this.animation.value,
color: '#3b82f6'
});
}
}
코드 예제
기본 애니메이션 구현
AnimationController 기본 예제
크기, 색상, 회전을 동시에 애니메이션하는 예제
코드 보기
// 커스텀 BoxDecoration Tween
class DecorationTween extends Tween<BoxDecoration> {
constructor({ begin, end }: { begin: BoxDecoration; end: BoxDecoration }) {
super({ begin, end });
}
protected lerp(t: number): BoxDecoration {
return BoxDecoration.lerp(this.begin, this.end, t);
}
}
class AnimationExample extends StatefulWidget {
createState() {
return new AnimationExampleState();
}
}
class AnimationExampleState extends State<AnimationExample> {
controller!: AnimationController;
sizeAnimation!: Animation<number>;
decorationAnimation!: Animation<BoxDecoration>;
rotationAnimation!: Animation<number>;
initState(context: BuildContext) {
super.initState(context);
// AnimationController 생성 (duration은 밀리초 단위)
this.controller = new AnimationController({
duration: 2000 // 2초
});
// 크기 애니메이션
this.sizeAnimation = new Tween({
begin: 100,
end: 200
}).animated(new CurvedAnimation({
parent: this.controller,
curve: Curves.easeInOut
}));
// BoxDecoration 애니메이션 (색상 포함)
this.decorationAnimation = new DecorationTween({
begin: new BoxDecoration({
color: '#3b82f6',
shape: 'rectangle',
borderRadius: BorderRadius.circular(16)
}),
end: new BoxDecoration({
color: '#ef4444',
shape: 'rectangle',
borderRadius: BorderRadius.circular(16)
})
}).animated(new CurvedAnimation({
parent: this.controller,
curve: Curves.easeInOut
}));
// 회전 애니메이션
this.rotationAnimation = new Tween({
begin: 0,
end: Math.PI * 2
}).animated(this.controller);
// 애니메이션 리스너 추가
this.controller.addListener(() => {
this.setState();
});
}
dispose() {
this.controller.dispose();
super.dispose();
}
build(context: BuildContext) {
return GestureDetector({
onClick: () => {
if (this.controller.isCompleted) {
this.controller.reverse();
} else {
this.controller.forward();
}
},
child: Transform.rotate({
angle: this.rotationAnimation.value,
child: Container({
width: this.sizeAnimation.value,
height: this.sizeAnimation.value,
decoration: this.decorationAnimation.value,
child: Center({
child: Text(
this.controller.isCompleted ? "뒤로" : "앞으로",
{
style: new TextStyle({
color: '#ffffff',
fontSize: 18,
fontWeight: 'bold'
})
}
)
})
})
})
});
}
}
애니메이션 제어
// 애니메이션 시작
this.controller.forward();
// 애니메이션 역재생
this.controller.reverse();
// 애니메이션 반복
this.controller.repeat();
// 특정 값으로 애니메이션
this.controller.animateTo(0.5);
// 애니메이션 정지
this.controller.stop();
// 애니메이션 리셋
this.controller.reset();
실습 예제
1. 펄스 애니메이션
class PulseAnimation extends StatefulWidget {
createState() {
return new PulseAnimationState();
}
}
class PulseAnimationState extends State<PulseAnimation> {
controller!: AnimationController;
animation!: Animation<number>;
initState(context: BuildContext) {
super.initState(context);
this.controller = new AnimationController({
duration: 1000 // 1초
});
this.animation = new Tween({
begin: 1.0,
end: 1.2
}).animated(new CurvedAnimation({
parent: this.controller,
curve: Curves.easeInOut
}));
this.controller.repeat({ reverse: true });
}
dispose() {
this.controller.dispose();
super.dispose();
}
build(context: BuildContext): Widget {
return Transform.scale({
scale: this.animation.value,
child: Container({
width: 100,
height: 100,
decoration: new BoxDecoration({
color: '#3b82f6',
shape: 'circle'
})
})
});
}
}
2. 순차 애니메이션
class SequentialAnimation extends StatefulWidget {
createState() {
return new SequentialAnimationState();
}
}
class SequentialAnimationState extends State<SequentialAnimation> {
controller!: AnimationController;
slideAnimation!: Animation<Offset>;
fadeAnimation!: Animation<number>;
initState(context: BuildContext) {
super.initState(context);
this.controller = new AnimationController({
duration: 2000 // 2초
});
// 슬라이드 애니메이션
this.slideAnimation = new Tween({
begin: new Offset(-1, 0),
end: new Offset(0, 0)
}).animated(new CurvedAnimation({
parent: this.controller,
curve: Curves.easeOut
}));
// 페이드 애니메이션
this.fadeAnimation = new Tween({
begin: 0.0,
end: 1.0
}).animated(new CurvedAnimation({
parent: this.controller,
curve: Curves.easeIn
}));
// 애니메이션 리스너 추가
this.controller.addListener(() => {
this.setState();
});
this.controller.forward();
}
dispose() {
this.controller.dispose();
super.dispose();
}
build(context: BuildContext): Widget {
return Transform.translate({
offset: new Offset(
this.slideAnimation.value.dx * 200, // x축 이동
this.slideAnimation.value.dy * 0 // y축 이동
),
child: Opacity({
opacity: this.fadeAnimation.value,
child: Container({
width: 200,
height: 100,
color: '#10b981',
child: Center({
child: Text("순차 애니메이션", {
style: new TextStyle({
color: '#ffffff',
fontSize: 18
})
})
})
})
})
});
}
}
3. 커스텀 커브 애니메이션
class CustomCurveAnimation extends StatefulWidget {
createState() {
return new CustomCurveAnimationState();
}
}
class CustomCurveAnimationState extends State<CustomCurveAnimation> {
controller!: AnimationController;
bounceAnimation!: Animation<number>;
initState(context: BuildContext) {
super.initState(context);
this.controller = new AnimationController({
duration: 1500 // 1.5초
});
// 바운스 효과를 위한 커스텀 커브
this.bounceAnimation = new Tween({
begin: 0,
end: 300
}).animated(new CurvedAnimation({
parent: this.controller,
curve: Curves.bounceOut
}));
// 애니메이션 리스너 추가
this.controller.addListener(() => {
this.setState();
});
this.controller.forward();
}
dispose() {
this.controller.dispose();
super.dispose();
}
build(context: BuildContext): Widget {
return Transform.translate({
offset: new Offset(this.bounceAnimation.value, 0),
child: Container({
width: 50,
height: 50,
decoration: new BoxDecoration({
color: '#10b981',
shape: 'circle'
})
})
});
}
}
주의사항
1. 성능 최적화
애니메이션은 성능에 영향을 줄 수 있으므로 최적화가 중요합니다:
// ❌ 나쁜 예: 전체 위젯 트리 재빌드
class MyWidgetState extends State<MyWidget> {
build(context: BuildContext): Widget {
return Column({
children: [
ComplexWidget({}), // 매번 재생성
Container({
width: this.animation.value,
height: 100
})
]
});
}
}
// ✅ 좋은 예: 애니메이션 영향을 받는 부분만 분리
class MyWidgetState extends State<MyWidget> {
complexWidget = ComplexWidget({}); // 한 번만 생성
build(context: BuildContext): Widget {
return Column({
children: [
this.complexWidget, // 재사용
Container({
width: this.animation.value,
height: 100
})
]
});
}
}
2. 메모리 관리
AnimationController는 반드시 dispose해야 합니다:
dispose() {
this.controller.dispose(); // 필수!
super.dispose();
}
3. vsync 설정
SingleTickerProviderStateMixin 또는 TickerProviderStateMixin을 사용해야 합니다:
// Flitter에서는 mixin을 사용하지 않습니다
// AnimationController를 직접 생성하고 관리합니다
class MyState extends State<MyWidget> {
controller!: AnimationController;
initState(context: BuildContext) {
super.initState(context);
this.controller = new AnimationController({
duration: 1000
});
}
dispose() {
this.controller.dispose();
super.dispose();
}
}
내장 애니메이션 위젯
Flitter는 자주 사용되는 애니메이션을 위한 내장 위젯들을 제공합니다:
- AnimatedContainer: 속성 변경 시 자동 애니메이션
- AnimatedOpacity: 투명도 애니메이션
- AnimatedPositioned: Stack 내 위치 애니메이션
- AnimatedScale: 크기 애니메이션
- AnimatedRotation: 회전 애니메이션
- AnimatedSlide: 슬라이드 애니메이션
이러한 위젯들은 AnimationController 없이도 간단한 애니메이션을 구현할 수 있게 해줍니다.
다음 단계
애니메이션 기초를 마스터했다면, 다음 주제들을 학습해보세요:
- 고급 애니메이션 - 복잡한 애니메이션 패턴과 최적화
- 커스텀 페인팅 - CustomPaint로 고급 그래픽 구현
- 위젯 레퍼런스 - 내장 애니메이션 위젯 상세 가이드