
플러터 공식 문서(Animations & transitions의 Tutorial)에서 번역 및 정리
플러터에서의 애니메이션 시스템은 타입이 지정된 '애니메이션(Animation)' 객체를 기반으로 합니다. 위젯(화면에 보이는 각 요소)은 이 애니메이션을 직접 사용하여 현재 값을 읽고 상태 변화를 감지하거나, 더 복잡한 애니메이션을 만들어 다른 위젯에 전달할 수 있습니다.
Animation
플러터에서 'Animation' 객체는 화면에 무엇이 보이는지는 모릅니다. 이것은 현재 값과 상태(완료되었는지, 취소되었는지)를 알고 있는 추상 클래스입니다. 자주 사용되는 타입 중 하나가 'Animation'입니다.
'Animation' 객체는 일정 기간 동안 두 값 사이에서 숫자를 순차적으로 생성합니다. 이 출력은 직선, 곡선, 계단 함수 등 다양한 형태가 될 수 있습니다. 'Animation' 객체가 어떻게 제어되는지에 따라 거꾸로 실행되거나, 심지어 중간에 방향을 바꿀 수도 있습니다.
'Animation'은 double 이외의 타입, 예를 들어 '애니메이션' 또는 '애니메이션'와 같은 것들도 처리할 수 있습니다.
'Animation' 객체는 상태를 가지고 있으며, 현재 값은 항상 .value 멤버에서 사용할 수 있습니다.
'Animation' 객체는 렌더링(화면에 그리는 것)이나 build() 함수(화면을 구성하는 함수)에 대해서는 알지 못합니다.
간단히 말하면, 플러터에서 'Animation'은 화면에 어떤 것을 움직이게 하는 마법 같은 도구입니다. 이 도구를 사용하여 우리가 만든 앱이나 게임에서 다양한 움직임을 만들 수 있어요.
'애니메이션 컨트롤러(AnimationController)'는 특별한 'Animation' 객체로, 하드웨어가 새로운 프레임을 준비할 때마다 새로운 값을 생성합니다. 기본적으로, 'Animation 컨트롤러'는 정해진 시간 동안 0.0부터 1.0까지의 숫자를 순차적으로 만들어냅니다. 예를 들어, 아래 코드는 'Animation' 객체를 만들지만, 실행을 시작하지는 않습니다.
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
'Animation 컨트롤러'는 'Animation'에서 파생되므로, 'Animation' 객체가 필요한 곳이면 어디서든 사용할 수 있습니다. 하지만, 'Animation 컨트롤러'에는 애니메이션을 제어하기 위한 추가 메소드들이 있습니다. 예를 들어, .forward() 메소드로 애니메이션을 시작할 수 있습니다. 숫자 생성은 화면 새로고침에 연결되어 있으므로, 보통 1초에 60개의 숫자가 생성됩니다. 숫자가 생성될 때마다 각 'Animation' 객체는 연결된 리스너 객체를 호출합니다. 각 자식에 대한 사용자 정의 디스플레이 목록을 만들려면 'RepaintBoundary'를 참조하세요.
'Animation 컨트롤러'를 만들 때, vsync 인자를 전달합니다. vsync의 존재는 화면 밖의 애니메이션이 불필요한 자원을 소비하는 것을 방지합니다. 클래스 정의에 'SingleTickerProviderStateMixin'을 추가함으로써 상태가 있는 객체를 vsync로 사용할 수 있습니다. 이에 대한 예는 GitHub의 animate1에서 볼 수 있습니다.
간단히 말하면, 'Animation 컨트롤러'는 애니메이션을 움직이게 하는 마법의 리모컨과 같아요. 이 리모컨을 사용하여 우리가 원하는 때에 애니메이션을 시작하고, 멈추고, 조절할 수 있답니다. 이것은 앱이나 게임에서 더 멋진 움직임을 만들 수 있게 도와줍니다.
'트윈(Tween)'은 애니메이션에서 숫자나 데이터 유형을 다양하게 만들 수 있는 도구예요. 기본적으로, '애니메이션 컨트롤러(AnimationController)' 객체는 0.0부터 1.0까지의 범위를 가지고 있지만, 다른 범위나 다른 데이터 유형이 필요한 경우 '트윈'을 사용하여 애니메이션을 다르게 설정할 수 있어요. 예를 들어, 다음 '트윈'은 -200.0부터 0.0까지의 범위를 만듭니다.
tween = Tween<double>(begin: -200, end: 0);
'트윈'은 상태가 없는 객체로, 시작과 끝만을 가집니다. '트윈'의 주된 역할은 입력 범위를 출력 범위로 매핑하는 것입니다. 입력 범위는 보통 0.0부터 1.0까지이지만, 꼭 그럴 필요는 없어요.
'트윈'은 'Animatable'로부터 상속받으며, 'Animation'과 마찬가지로 꼭 double을 출력할 필요는 없어요. 예를 들어, '컬러트윈(ColorTween)'은 두 색상 사이의 진행을 지정합니다.
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
'트윈' 객체는 상태를 저장하지 않습니다. 대신, 현재 애니메이션 값(0.0과 1.0 사이)을 실제 애니메이션 값으로 매핑하는 transform 함수를 사용하는 evaluate(Animation animation) 메소드를 제공합니다.
애니메이션 객체의 현재 값은 .value 메소드에서 찾을 수 있습니다. evaluate 함수는 시작과 끝이 각각 애니메이션 값이 0.0과 1.0일 때 반환되도록 하는 등의 관리 작업도 수행합니다.
Tween.animate
'트윈' 객체를 사용하려면, 컨트롤러 객체를 전달하면서 '트윈'에서 animate()를 호출합니다. 예를 들어, 다음 코드는 500밀리초 동안 0부터 255까지의 정수 값을 생성합니다.
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
참고: animate() 메소드는 '애니메이터블'이 아닌 '애니메이션'을 반환합니다.
다음 예는 컨트롤러, 곡선, 그리고 '트윈'을 보여줍니다:
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> curve =
CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);
애니메이션 알림(Animation notifications)
'애니메이션' 객체는 addListener()와 addStatusListener()로 정의된 리스너와 상태 리스너를 가질 수 있습니다. 애니메이션 값이 변경될 때마다 리스너가 호출됩니다. 리스너의 가장 일반적인 동작은 setState()를 호출하여 재구축하는 것입니다. 상태 리스너는 애니메이션이 시작되거나 끝나거나 앞으로 나아가거나 뒤로 가는 것과 같이 '애니메이션 상태'에 의해 정의될 때 호출됩니다. addListener() 메소드의 예는 다음 섹션에 있고, 애니메이션 진행 상황을 모니터링하는 것은 addStatusListener()의 예를 보여줍니다.
간단히 말하면, '트윈'은 애니메이션의 숫자 범위나 종류를 바꾸는 마법의 도구예요. 이를 사용해서 앱이나 게임에서 더 다양하고 멋진 움직임을 만들 수 있어요.
애니메이션 렌더링
'애니메이션 렌더링'은 위젯에 기본 애니메이션을 추가하는 방법에 대해 알려줍니다. '애니메이션'이 새로운 숫자를 생성할 때마다, addListener() 함수가 setState()를 호출해요. 이는 필요한 vsync 매개변수와 함께 '애니메이션 컨트롤러'를 정의하는 방법을 이해하는 데 도움이 됩니다. 또한, Dart의 캐스케이드 표기법(“..addListener”에서 볼 수 있는 ".." 구문)에 대해서도 배울 수 있어요. 클래스를 비공개로 만들려면 이름 앞에 밑줄(_)을 추가하면 돼요.
지금까지 시간에 따라 숫자 시퀀스를 생성하는 방법을 배웠어요. 하지만 화면에 아무것도 표시되지 않았죠. '애니메이션' 객체로 렌더링하려면, 위젯의 멤버로 '애니메이션' 객체를 저장하고, 그 값을 사용하여 어떻게 그릴지 결정해야 해요.
아래는 애니메이션 없이 플러터 로고를 그리는 앱 예제에요:
import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());
class LogoApp extends StatefulWidget {
const LogoApp({super.key});
State<LogoApp> createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> {
Widget build(BuildContext context) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: 300,
width: 300,
child: const FlutterLogo(),
),
);
}
}
이 코드를 수정하여 로고가 아무것도 없는 상태에서 전체 크기로 성장하도록 애니메이션을 추가할 수 있어요. '애니메이션 컨트롤러'를 정의할 때는 vsync 객체를 전달해야 합니다. vsync 매개변수는 '애니메이션 컨트롤러' 섹션에서 설명됩니다.
비애니메이션 예제와 비교하여 변경된 부분은 다음과 같아요:
// ... 기존 코드 ...
late Animation<double> animation;
late AnimationController controller;
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {
// 애니메이션 객체의 값이 변경될 때 상태가 변경됩니다.
});
});
controller.forward();
}
Widget build(BuildContext context) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: animation.value, // 애니메이션 값으로 높이와 너비를 설정
width: animation.value,
child: const FlutterLogo(),
),
);
}
void dispose() {
controller.dispose(); // 메모리 누수를 방지하기 위해 컨트롤러를 폐기
super.dispose();
}
addListener() 함수가 setState()를 호출하기 때문에, '애니메이션'이 새로운 숫자를 생성할 때마다 현재 프레임이 다시 빌드되도록 표시됩니다. build()에서 컨테이너의 크기가 변경되는데, 이는 높이와 너비가 하드코딩된 값 대신 animation.value를 사용하기 때문입니다.
이 몇 가지 변경을 통해 Flutter에서 첫 번째 애니메이션을 만들었어요!
Dart 언어 팁: "..addListener()"에서 볼 수 있는 Dart의 캐스케이드 표기법, 즉 두 개의 점 구문에 익숙하지 않을 수도 있어요. 이 구문은 animate()에서 반환된 값으로 addListener() 메소드를 호출한다는 의미에요. 다음 예제를 고려해 보세요:
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
// ...
});
이 코드는 다음과 동일합니다:
animation = Tween<double>(begin: 0, end: 300).animate(controller);
animation.addListener(() {
// ...
});
캐스케이드에 대해 더 알아보려면 Dart 언어 문서의 'Cascade notation'을 확인하세요.
애니메이티드위젯(AnimatedWidget)
보통 애니메이션을 위해서는 'addListener()'와 'setState()'를 사용하지만, '애니메이티드위젯'을 사용하면 이런 과정이 필요 없어요.
예를 들어, '애니메이티드로고(AnimatedLogo)'라는 클래스를 만들 수 있어요. 이 클래스는 애니메이션 값을 사용하여 화면에 로고를 표시합니다.
import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());
// #docregion AnimatedLogo
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
// #enddocregion AnimatedLogo
class LogoApp extends StatefulWidget {
const LogoApp({super.key});
State<LogoApp> createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
}
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
void dispose() {
controller.dispose();
super.dispose();
}
}
'애니메이티드로고'는 애니메이션의 현재 값을 사용해서 로고의 크기를 결정해요. 그리고 '로고앱(LogoApp)'은 '애니메이션 컨트롤러'와 '트윈'을 관리하고, '애니메이티드로고'에게 애니메이션 객체를 전달합니다. 이렇게 하면, 애니메이션과 위젯 코드를 분리할 수 있어서 더 깔끔하고 관리하기 쉬워져요.
간단히 말해서, '애니메이티드위젯'은 애니메이션을 쉽게 추가할 수 있는 도구로, 복잡한 코드 없이 멋진 애니메이션 효과를 위젯에 적용할 수 있게 해줘요.
애니메이션 진행 상황 모니터링
애니메이션의 상태가 바뀔 때 알림을 받으려면 'addStatusListener()'를 사용하면 돼요. 예를 들어, 애니메이션이 완료되거나 시작 상태로 돌아갔을 때 방향을 반대로 하는 무한 루프 애니메이션을 만들 수 있어요.
다음 코드는 애니메이션 상태가 변경될 때 업데이트를 출력하도록 이전 예제를 수정한 것이에요:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) => print('$status'));
controller.forward();
}
// ...
}
이 코드를 실행하면 다음과 같은 출력이 나와요:
AnimationStatus.forward
AnimationStatus.completed
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
// #enddocregion print-state
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
})
// #docregion print-state
..addStatusListener((status) => print('$status'));
controller.forward();
}
이 코드는 '플러터(Flutter)'에서 애니메이션을 만드는 방법을 보여줍니다. 여기서는 '로고 앱(LogoApp)'이라는 게임이나 앱에 로고가 어떻게 움직일지를 설정하는 부분이에요.
클래스와 믹스인(Mixin): 우리는 '_LogoAppState'라는 클래스를 만들고, 'SingleTickerProviderStateMixin'이라는 것을 사용해요. 이것은 애니메이션을 위한 특별한 도구로, 애니메이션을 더 부드럽고 예쁘게 보이게 해줘요.
애니메이션 컨트롤러와 트윈: 'controller'와 'animation'이라는 두 가지 중요한 부분을 만들어요. 'controller'는 애니메이션이 얼마나 오래 지속될지(여기서는 2초)와 같은 것들을 관리하고, 'animation'은 '트윈(Tween)'을 사용해 애니메이션이 어떻게 변할지를 결정해요(예를 들어, 0에서 300까지 커지는 애니메이션).
상태 리스너(Status Listener): 'addStatusListener'는 애니메이션의 상태가 바뀔 때마다 무슨 일이 일어나는지를 알려줘요. 예를 들어, 애니메이션이 끝나면 (completed) 다시 반대로 재생되게 하거나 (reverse), 처음 상태로 돌아가면 (dismissed) 다시 앞으로 재생되게 (forward) 설정할 수 있어요.
애니메이션 시작: 마지막으로, 'controller.forward()'를 사용해서 애니메이션을 시작해요.
이렇게 해서, 로고가 점점 커졌다가 다시 작아지는 멋진 애니메이션을 만들 수 있어요. 이 코드는 컴퓨터가 애니메이션을 어떻게 작동시키는지를 알려주는 지시사항입니다.
'동시 애니메이션(Simultaneous animations)'은 여러 가지 애니메이션 효과를 동시에 적용하는 방법을 보여줍니다. 예를 들어, 로고가 커졌다 작아지는 동안에 로고의 투명도도 변하게 만들 수 있어요.
이 예제에서는 '애니메이션 컨트롤러(AnimationController)'에 두 가지 '트윈(Tween)'을 사용합니다. 하나는 로고의 크기를 조절하고, 다른 하나는 로고의 투명도를 조절해요.
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);
'사이즈 애니메이션(sizeAnimation)'은 로고의 크기를, '투명도 애니메이션(opacityAnimation)'은 로고의 투명도를 결정해요. 이 두 가지 값을 조합하여 로고가 커지면서 점점 더 선명해지는 효과를 만들 수 있어요.
'애니메이티드로고(AnimatedLogo)'라는 위젯은 이 두 가지 애니메이션 값을 사용하여 로고를 어떻게 그릴지 결정해요.
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: const FlutterLogo(),
),
),
);
}
}
'로고앱(LogoApp)' 클래스에서는 이 애니메이션을 시작하고, '애니메이티드로고'를 사용해서 화면에 로고를 보여줍니다.
이렇게 하면, 로고가 커지면서 투명도가 바뀌는 멋진 애니메이션을 볼 수 있어요. 컴퓨터에게 어떻게 로고를 그리고 애니메이션을 적용해야 하는지 알려주는 것이죠!