1. 소개
애니메이션은 앱의 사용자 환경을 개선하고, 사용자에게 중요한 정보를 전달하고, 앱을 더 세련되고 사용하기 즐거운 앱으로 만드는 데 효과적인 방법입니다.
Flutter의 애니메이션 프레임워크 개요
Flutter는 각 프레임에서 위젯 트리의 일부를 다시 빌드하여 애니메이션 효과를 표시합니다. 애니메이션을 더 쉽게 만들고 구성할 수 있도록 사전 빌드된 애니메이션 효과와 기타 API를 제공합니다.
- 암시적 애니메이션은 전체 애니메이션을 자동으로 실행하는 사전 빌드된 애니메이션 효과입니다. 애니메이션의 타겟 값이 변경되면 현재 값에서 타겟 값으로 애니메이션을 실행하고, 위젯이 원활하게 애니메이션 처리되도록 그 사이의 각 값을 표시합니다. 암시적 애니메이션의 예로는
AnimatedSize
,AnimatedScale
,AnimatedPositioned
가 있습니다. - 명시적 애니메이션도 사전 빌드된 애니메이션 효과이지만 작동하려면
Animation
객체가 필요합니다. 예를 들면SizeTransition
,ScaleTransition
또는PositionedTransition
입니다. - Animation은 실행 중인 애니메이션 또는 중지된 애니메이션을 나타내는 클래스이며, 애니메이션이 실행 중인 타겟 값을 나타내는 값과 특정 시점에 애니메이션이 화면에 표시하는 현재 값을 나타내는 상태로 구성됩니다.
Listenable
의 서브클래스이며 애니메이션이 실행되는 동안 상태가 변경되면 리스너에게 알립니다. - AnimationController는 애니메이션을 만들고 상태를 제어하는 방법입니다.
forward()
,reset()
,stop()
,repeat()
와 같은 메서드를 사용하면 크기, 위치와 같이 표시되는 애니메이션 효과를 정의하지 않고도 애니메이션을 제어할 수 있습니다. - 트윈은 시작 값과 끝 값 간에 값을 보간하는 데 사용되며 double,
Offset
,Color
와 같은 모든 유형을 나타낼 수 있습니다. - 곡선은 시간 경과에 따른 매개변수의 변화율을 조정하는 데 사용됩니다. 애니메이션이 실행될 때는 일반적으로 이징 커브를 적용하여 애니메이션 시작 또는 종료 시 변경 속도를 더 빠르게 또는 더 느리게 만듭니다. 곡선은 0.0과 1.0 사이의 입력 값을 사용하고 0.0과 1.0 사이의 출력 값을 반환합니다.
빌드할 항목
이 Codelab에서는 다양한 애니메이션 효과와 기법이 포함된 객관식 퀴즈 게임을 빌드합니다.
다음 작업을 수행하는 방법을 알아보세요.
- 크기와 색상에 애니메이션 효과를 주는 위젯 빌드
- 3D 카드 플립 효과 빌드
- 애니메이션 패키지의 멋진 사전 빌드된 애니메이션 효과 사용
- 최신 버전의 Android에서 사용할 수 있는 뒤로 탐색 예측 동작 지원 추가
학습할 내용
이 Codelab에서는 다음을 학습합니다.
- 암시적 애니메이션 효과를 사용하여 많은 코드 없이도 멋진 애니메이션을 만드는 방법
- 명시적으로 애니메이션이 적용된 효과를 사용하여
AnimatedSwitcher
또는AnimationController
와 같은 사전 빌드된 애니메이션 위젯을 사용하여 자체 효과를 구성하는 방법 AnimationController
를 사용하여 3D 효과를 표시하는 자체 위젯을 정의하는 방법animations
패키지를 사용하여 최소한의 설정으로 멋진 애니메이션 효과를 표시하는 방법
필요한 항목
- Flutter SDK
- IDE(예: VSCode, Android 스튜디오 / IntelliJ)
2. Flutter 개발 환경 설정
이 실습을 완료하려면 Flutter SDK와 편집기라는 두 가지 소프트웨어가 필요합니다.
다음 기기 중 하나를 사용하여 이 Codelab을 실행할 수 있습니다.
- 컴퓨터에 연결되어 있고 개발자 모드로 설정된 실제 Android (7단계에서 뒤로 탐색 예측을 구현하는 경우 권장) 또는 iOS 기기
- iOS 시뮬레이터 (Xcode 도구 설치 필요)
- Android Emulator(Android 스튜디오 설정 필요)
- 브라우저(디버깅 시 Chrome 필요)
- Windows, Linux 또는 macOS 데스크톱 컴퓨터 배포에 사용할 플랫폼에서 개발해야 합니다. 따라서 Windows 데스크톱 앱을 개발하려면 적절한 빌드 체인에 액세스할 수 있도록 Windows에서 개발해야 합니다. docs.flutter.dev/desktop에 운영체제별 요구사항이 자세히 설명되어 있습니다.
설치 확인하기
Flutter SDK가 올바르게 구성되었고 위의 대상 플랫폼 중 하나 이상이 설치되어 있는지 확인하려면 Flutter Doctor 도구를 사용하세요.
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale en) [✓] Android toolchain - develop for Android devices [✓] Xcode - develop for iOS and macOS [✓] Chrome - develop for the web [✓] Android Studio [✓] IntelliJ IDEA Ultimate Edition [✓] VS Code [✓] Connected device (4 available) [✓] Network resources • No issues found!
3. 시작 앱 실행
시작 앱 다운로드
git
를 사용하여 GitHub의 flutter/samples 저장소에서 시작 앱을 클론합니다.
$ git clone https://github.com/flutter/codelabs.git $ cd codelabs/animations/step_01/
또는 소스 코드를 .zip 파일로 다운로드할 수 있습니다.
앱 실행
앱을 실행하려면 flutter run
명령어를 사용하고 android
, ios
또는 chrome
와 같은 대상 기기를 지정합니다. 지원되는 플랫폼의 전체 목록은 지원되는 플랫폼 페이지를 참고하세요.
$ flutter run -d android
원하는 IDE를 사용하여 앱을 실행하고 디버그할 수도 있습니다. 자세한 내용은 공식 Flutter 문서를 참고하세요.
코드 둘러보기
시작 앱은 모델-뷰-뷰-모델(MVVM) 디자인 패턴에 따라 두 화면으로 구성된 객관식 퀴즈 게임입니다. QuestionScreen
(뷰)는 QuizViewModel
(뷰 모델) 클래스를 사용하여 사용자에게 QuestionBank
(모델) 클래스의 객관식 질문을 합니다.
- home_screen.dart - New Game(새 게임) 버튼이 있는 화면을 표시합니다.
- main.dart - Material 3을 사용하고 홈 화면을 표시하도록
MaterialApp
를 구성합니다. - model.dart - 앱 전체에서 사용되는 핵심 클래스를 정의합니다.
- question_screen.dart: 퀴즈 게임의 UI를 표시합니다.
- view_model.dart -
QuestionScreen
로 표시되는 퀴즈 게임의 상태와 로직을 저장합니다.
앱은 아직 애니메이션 효과를 지원하지 않습니다. 단, 사용자가 New Game 버튼을 누르면 Flutter의 Navigator
클래스에 의해 표시되는 기본 뷰 전환은 예외입니다.
4. 암시적 애니메이션 효과 사용
암시적 애니메이션은 특별한 구성이 필요하지 않으므로 많은 상황에서 유용합니다. 이 섹션에서는 애니메이션 스코어보드를 표시하도록 StatusBar
위젯을 업데이트합니다. 일반적인 암시적 애니메이션 효과를 찾으려면 ImplicitlyAnimatedWidget API 문서를 둘러보세요.
애니메이션이 적용되지 않은 스코어보드 위젯 만들기
다음 코드를 사용하여 새 파일 lib/scoreboard.dart
를 만듭니다.
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
Icon(
Icons.star,
size: 50,
color:
score < i + 1 ? Colors.grey.shade400 : Colors.yellow.shade700,
)
],
),
);
}
}
그런 다음 StatusBar
위젯의 하위 요소에 Scoreboard
위젯을 추가하여 이전에 점수와 총문항 수를 표시했던 Text
위젯을 대체합니다. 편집기에서 파일 상단에 필요한 import "scoreboard.dart"
를 자동으로 추가합니다.
lib/question_screen.dart
class StatusBar extends StatelessWidget {
final QuizViewModel viewModel;
const StatusBar({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Scoreboard( // NEW
score: viewModel.score, // NEW
totalQuestions: viewModel.totalQuestions, // NEW
),
],
),
),
);
}
}
이 위젯에는 각 질문에 별표 아이콘이 표시됩니다. 질문에 올바르게 답변하면 애니메이션 없이 별표가 즉시 켜집니다. 다음 단계에서는 크기와 색상에 애니메이션을 적용하여 사용자에게 점수가 변경되었음을 알립니다.
암시적 애니메이션 효과 사용하기
별표가 활성화되면 AnimatedScale
위젯을 사용하여 scale
값을 0.5
에서 1.0
로 변경하는 AnimatedStar
라는 새 위젯을 만듭니다.
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
AnimatedStar( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
이제 사용자가 질문에 올바르게 답변하면 AnimatedStar
위젯이 암시적 애니메이션을 사용하여 크기를 업데이트합니다. 여기서는 Icon
의 color
가 애니메이션되지 않고 AnimatedScale
위젯에서 실행하는 scale
만 애니메이션됩니다.
트윈을 사용하여 두 값 사이를 보간
isActive
필드가 true로 변경된 직후 AnimatedStar
위젯의 색상이 변경됩니다.
애니메이션 색상 효과를 구현하려면 AnimatedContainer
위젯 (ImplicitlyAnimatedWidget
의 또 다른 서브클래스)을 사용해 보세요. 색상을 비롯한 모든 속성을 자동으로 애니메이션할 수 있기 때문입니다. 안타깝게도 위젯은 컨테이너가 아닌 아이콘을 표시해야 합니다.
아이콘 도형 간의 전환 효과를 구현하는 AnimatedIcon
도 시도해 볼 수 있습니다. 하지만 AnimatedIcons
클래스에는 별표 아이콘의 기본 구현이 없습니다.
대신 Tween
를 매개변수로 사용하는 ImplicitlyAnimatedWidget
의 또 다른 서브클래스인 TweenAnimationBuilder
를 사용합니다. 트윈은 두 값 (begin
및 end
)을 사용하여 애니메이션에서 표시할 수 있도록 중간 값을 계산하는 클래스입니다. 이 예에서는 애니메이션 효과를 빌드하는 데 필요한 Tween<Color>
인터페이스를 충족하는 ColorTween
를 사용합니다.
Icon
위젯을 선택하고 IDE에서 '빌더로 래핑' 빠른 작업을 사용하여 이름을 TweenAnimationBuilder
로 변경합니다. 그런 다음 기간과 ColorTween
를 제공합니다.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: TweenAnimationBuilder( // Add from here...
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) { // To here.
return Icon(
Icons.star,
size: 50,
color: value, // Modify from here...
);
}, // To here.
),
);
}
}
이제 앱을 핫 리로드하여 새 애니메이션을 확인합니다.
ColorTween
의 end
값은 isActive
매개변수의 값에 따라 변경됩니다. TweenAnimationBuilder
는 Tween.end
값이 변경될 때마다 애니메이션을 다시 실행하기 때문입니다. 이 경우 새 애니메이션이 현재 애니메이션 값에서 새 끝 값으로 실행되므로 언제든지 (애니메이션이 실행 중인 경우에도) 색상을 변경하고 올바른 중간 값으로 부드러운 애니메이션 효과를 표시할 수 있습니다.
곡선 적용
두 애니메이션 효과 모두 일정한 속도로 실행되지만 애니메이션은 속도를 높이거나 낮추면 시각적으로 더 흥미롭고 유용한 정보를 전달할 수 있습니다.
Curve
는 시간 경과에 따른 매개변수의 변화율을 정의하는 이완 함수를 적용합니다. Flutter는 Curves
클래스에 사전 빌드된 이음선 곡선 모음(예: easeIn
또는 easeOut
)을 제공합니다.
이 다이어그램 (Curves
API 문서 페이지에서 확인 가능)은 곡선이 작동하는 방식을 보여줍니다. 곡선은 0.0과 1.0 사이의 입력 값 (x축에 표시됨)을 0.0과 1.0 사이의 출력 값 (y축에 표시됨)으로 변환합니다. 이 다이어그램은 이완 곡선을 사용할 때 다양한 애니메이션 효과가 어떻게 표시되는지 미리 보여줍니다.
AnimatedStar에 _curve
라는 새 필드를 만들고 이를 AnimatedScale
및 TweenAnimationBuilder
위젯에 매개변수로 전달합니다.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
final Curve _curve = Curves.elasticOut; // NEW
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
curve: _curve, // NEW
duration: _duration,
child: TweenAnimationBuilder(
curve: _curve, // NEW
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) {
return Icon(
Icons.star,
size: 50,
color: value,
);
},
),
);
}
}
이 예에서 elasticOut
곡선은 스프링 모션으로 시작하여 끝쪽으로 균형을 맞추는 과장된 스프링 효과를 제공합니다.
앱을 핫 리로드하면 이 커브가 AnimatedSize
및 TweenAnimationBuilder
에 적용된 것을 확인할 수 있습니다.
DevTools를 사용하여 느린 애니메이션 사용 설정하기
애니메이션 효과를 디버그하기 위해 Flutter DevTools에서는 앱의 모든 애니메이션을 느리게 재생하여 애니메이션을 더 명확하게 볼 수 있는 방법을 제공합니다.
DevTools를 열려면 앱이 디버그 모드로 실행 중인지 확인하고 VSCode의 디버그 툴바에서 Widget Inspector를 선택하거나 IntelliJ / Android 스튜디오의 디버그 도구 창에서 Open Flutter DevTools 버튼을 선택하여 엽니다.
위젯 검사기가 열리면 툴바에서 애니메이션 느리게 재생 버튼을 클릭합니다.
5. 명시적 애니메이션 효과 사용
명시적 애니메이션은 암시적 애니메이션과 마찬가지로 사전 빌드된 애니메이션 효과이지만 타겟 값을 사용하는 대신 Animation
객체를 매개변수로 사용합니다. 따라서 애니메이션이 이미 탐색 전환, AnimatedSwitcher
또는 AnimationController
로 정의된 경우에 유용합니다.
명시적 애니메이션 효과 사용하기
명시적 애니메이션 효과를 시작하려면 Card
위젯을 AnimatedSwitcher
로 래핑합니다.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
), // NEW
);
}
}
AnimatedSwitcher
는 기본적으로 크로스페이드 효과를 사용하지만 transitionBuilder
매개변수를 사용하여 재정의할 수 있습니다. 전환 빌더는 AnimatedSwitcher
에 전달된 하위 위젯과 Animation
객체를 제공합니다. 이때 명시적 애니메이션을 사용하는 것이 좋습니다.
이 Codelab에서는 첫 번째 명시적 애니메이션으로 SlideTransition
를 사용합니다. 이 애니메이션은 수신 및 발신 위젯이 이동하는 시작 및 종료 오프셋을 정의하는 Animation<Offset>
를 사용합니다.
트윈에는 모든 Animation
를 트윈이 적용된 다른 Animation
로 변환하는 도우미 함수 animate()
가 있습니다. 즉, Tween<Offset>
를 사용하여 AnimatedSwitcher
에서 제공하는 Animation<double>
를 Animation<Offset>
로 변환하여 SlideTransition
위젯에 제공할 수 있습니다.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) { // Add from here...
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
return SlideTransition(position: offsetAnimation, child: child);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
여기서는 Tween.animate
를 사용하여 Curve
를 Animation
에 적용한 다음 0.0~1.0 범위의 Tween<double>
에서 x축에서 -0.1~0.0으로 전환되는 Tween<Offset>
로 변환합니다.
또는 Animation 클래스에는 Tween
(또는 Animatable
)을 받아 새로운 Animation
로 변환하는 drive()
함수가 있습니다. 이렇게 하면 트윈을 '연쇄'하여 결과 코드를 더 간결하게 만들 수 있습니다.
lib/question_screen.dart
transitionBuilder: (child, animation) {
var offsetAnimation = animation
.drive(CurveTween(curve: Curves.easeInCubic))
.drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
return SlideTransition(position: offsetAnimation, child: child);
},
명시적 애니메이션을 사용하는 또 다른 이점은 쉽게 함께 구성할 수 있다는 것입니다. SlideTransition 위젯을 래핑하여 동일한 곡선 애니메이션을 사용하는 또 다른 명시적 애니메이션인 FadeTransition을 추가합니다.
lib/question_screen.dart
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
final fadeInAnimation = curveAnimation; // NEW
return FadeTransition( // NEW
opacity: fadeInAnimation, // NEW
child: SlideTransition(position: offsetAnimation, child: child), // NEW
); // NEW
},
layoutBuilder 맞춤설정
AnimationSwitcher에 약간의 문제가 있을 수 있습니다. QuestionCard가 새 질문으로 전환되면 애니메이션이 실행되는 동안 사용 가능한 공간의 중앙에 배치되지만 애니메이션이 중지되면 위젯이 화면 상단으로 이동합니다. 이렇게 하면 질문 카드의 최종 위치가 애니메이션이 실행되는 동안의 위치와 일치하지 않아 애니메이션이 끊기게 됩니다.
이를 해결하기 위해 AnimatedSwitcher에는 레이아웃을 정의하는 데 사용할 수 있는 layoutBuilder 매개변수도 있습니다. 이 함수를 사용하여 카드를 화면 상단에 정렬하도록 레이아웃 빌더를 구성합니다.
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
이 코드는 AnimatedSwitcher 클래스의 defaultLayoutBuilder를 수정한 버전이지만 Alignment.center
대신 Alignment.topCenter
를 사용합니다.
요약
- 명시적 애니메이션은 Animation 객체를 사용하는 애니메이션 효과입니다 (타겟 값과 기간을 사용하는 ImplicitlyAnimatedWidgets와 달리).
- Animation 클래스는 실행 중인 애니메이션을 나타내지만 특정 효과는 정의하지 않습니다.
- Tween().animate 또는 Animation.drive()를 사용하여 애니메이션에 트윈과 곡선을 적용합니다 (CurveTween 사용).
- AnimatedSwitcher의 layoutBuilder 매개변수를 사용하여 하위 요소의 레이아웃을 조정합니다.
6. 애니메이션 상태 제어
지금까지는 모든 애니메이션이 프레임워크에서 자동으로 실행되었습니다. 암시적 애니메이션은 자동으로 실행되며 명시적 애니메이션 효과는 애니메이션이 올바르게 작동해야 합니다. 이 섹션에서는 AnimationController를 사용하여 자체 Animation 객체를 만들고 TweenSequence를 사용하여 트윈을 결합하는 방법을 알아봅니다.
AnimationController를 사용하여 애니메이션 실행
AnimationController를 사용하여 애니메이션을 만들려면 다음 단계를 따르세요.
- StatefulWidget 만들기
- State 클래스에서 SingleTickerProviderStateMixin 믹스인을 사용하여 AnimationController에 Ticker 제공
- initState 수명 주기 메서드에서 AnimationController를 초기화하여 현재 State 객체를
vsync
(TickerProvider) 매개변수에 제공합니다. - AnimatedBuilder를 사용하거나 listen() 및 setState를 수동으로 호출하여 AnimationController가 리스너에게 알릴 때마다 위젯이 다시 빌드되도록 합니다.
flip_effect.dart라는 새 파일을 만들고 다음 코드를 복사하여 붙여넣습니다.
lib/flip_effect.dart
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: widget.duration);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
}
@override
void didUpdateWidget(covariant CardFlipEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.child.key != oldWidget.child.key) {
_handleChildChanged(widget.child, oldWidget.child);
}
}
void _handleChildChanged(Widget newChild, Widget previousChild) {
_previousChild = previousChild;
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationController.value * math.pi),
child: _animationController.isAnimating
? _animationController.value < 0.5
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
}
이 클래스는 AnimationController를 설정하고 프레임워크가 didUpdateWidget을 호출하여 위젯 구성이 변경되었으며 새 하위 위젯이 있을 수 있음을 알릴 때마다 애니메이션을 다시 실행합니다.
AnimatedBuilder는 AnimationController가 리스너에게 알릴 때마다 위젯 트리가 다시 빌드되도록 하고, Transform 위젯은 3D 회전 효과를 적용하여 카드가 뒤집어지는 것을 시뮬레이션하는 데 사용됩니다.
이 위젯을 사용하려면 각 답변 카드를 CardFlipEffect 위젯으로 래핑합니다. 카드 위젯에 key
를 제공해야 합니다.
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card.filled( // NEW
key: ValueKey(answers[index]), // NEW
color: color,
elevation: 2,
margin: EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTapped(index),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
answers.length > index ? answers[index] : '',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.clip,
),
),
),
),
), // NEW
);
}),
);
}
이제 앱을 핫 리로드하여 CardFlipEffect 위젯을 사용하여 답변 카드가 뒤집어지는 것을 확인합니다.
이 클래스는 명시적 애니메이션 효과와 매우 유사해 보입니다. 실제로 AnimatedWidget 클래스를 직접 확장하여 자체 버전을 구현하는 것이 좋습니다. 안타깝게도 이 클래스는 이전 위젯을 스테이트에 저장해야 하므로 StatefulWidget을 사용해야 합니다. 자체 명시적 애니메이션 효과를 만드는 방법에 관한 자세한 내용은 AnimatedWidget의 API 문서를 참고하세요.
TweenSequence를 사용하여 지연 추가
이 섹션에서는 각 카드가 한 번에 하나씩 뒤집어지도록 CardFlipEffect 위젯에 지연을 추가합니다. 시작하려면 delayAmount
라는 새 필드를 추가합니다.
lib/flip_effect.dart
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
final double delayAmount; // NEW
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
required this.delayAmount, // NEW
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
그런 다음 AnswerCards
빌드 메서드에 delayAmount
를 추가합니다.
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect(
delayAmount: index.toDouble() / 2, // NEW
duration: const Duration(milliseconds: 300),
child: Card.filled(
key: ValueKey(answers[index]),
그런 다음 _CardFlipEffectState
에서 TweenSequence
를 사용하여 지연을 적용하는 새 애니메이션을 만듭니다. 이 경우 Future.delayed
와 같은 dart:async
라이브러리의 유틸리티는 사용하지 않습니다. 지연은 애니메이션의 일부이며 위젯이 AnimationController를 사용할 때 명시적으로 제어하는 것이 아니기 때문입니다. 이렇게 하면 동일한 TickerProvider를 사용하므로 DevTools에서 느린 애니메이션을 사용 설정할 때 애니메이션 효과를 더 쉽게 디버그할 수 있습니다.
TweenSequence
를 사용하려면 두 개의 TweenSequenceItem
를 만듭니다. 하나는 상대 시간 동안 애니메이션을 0으로 유지하는 ConstantTween
를 포함하고 다른 하나는 0.0
에서 1.0
로 전환되는 일반 Tween
입니다.
lib/flip_effect.dart
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
late final Animation<double> _animationWithDelay; // NEW
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: widget.duration * (widget.delayAmount + 1));
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
_animationWithDelay = TweenSequence<double>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
마지막으로 build 메서드에서 AnimationController의 애니메이션을 새로운 지연된 애니메이션으로 바꿉니다.
lib/flip_effect.dart
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationWithDelay, // Modify this line
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationWithDelay.value * math.pi), // And this line
child: _animationController.isAnimating
? _animationWithDelay.value < 0.5 // And this one.
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
이제 앱을 핫 리로드하고 카드가 하나씩 펼쳐지는 것을 확인합니다. Transform
위젯에서 제공하는 3D 효과의 관점을 변경해 보세요.
7. 맞춤 탐색 전환 사용
지금까지는 단일 화면에서 효과를 맞춤설정하는 방법을 알아봤지만 애니메이션을 사용하는 또 다른 방법은 애니메이션을 사용하여 화면 간에 전환하는 것입니다. 이 섹션에서는 내장 애니메이션 효과와 pub.dev의 공식 애니메이션 패키지에서 제공하는 멋진 사전 빌드된 애니메이션 효과를 사용하여 화면 전환에 애니메이션 효과를 적용하는 방법을 알아봅니다.
탐색 전환 애니메이션 처리
PageRouteBuilder
클래스는 전환 애니메이션을 맞춤설정할 수 있는 Route
입니다. 이를 통해 transitionBuilder
콜백을 재정의할 수 있습니다. 이 콜백은 Navigator에서 실행되는 수신 및 발신 애니메이션을 나타내는 두 개의 Animation 객체를 제공합니다.
전환 애니메이션을 맞춤설정하려면 MaterialPageRoute
를 PageRouteBuilder
로 바꾸고 사용자가 HomeScreen
에서 QuestionScreen
로 이동할 때 전환 애니메이션을 맞춤설정합니다. FadeTransition (명시적으로 애니메이션이 적용된 위젯)을 사용하여 새 화면이 이전 화면 위에 페이드 인되도록 합니다.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
child: Text('New Game'),
),
애니메이션 패키지는 FadeThroughTransition과 같은 멋진 사전 빌드된 애니메이션 효과를 제공합니다. 애니메이션 패키지를 가져오고 FadeTransition을 FadeThroughTransition 위젯으로 바꿉니다.
lib/home_screen.dart
import 'package;animations/animations.dart';
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
뒤로 탐색 예측 애니메이션 맞춤설정
뒤로 탐색 예측은 사용자가 탐색하기 전에 현재 경로 또는 앱 뒤에 있는 항목을 살펴볼 수 있는 새로운 Android 기능입니다. 엿보기 애니메이션은 사용자가 화면을 뒤로 드래그할 때의 손가락 위치에 따라 실행됩니다.
Flutter는 탐색 스택에 팝할 경로가 없거나 뒤로 탐색하면 앱이 종료되는 경우 시스템 수준에서 이 기능을 사용 설정하여 시스템 뒤로 탐색 예측을 지원합니다. 이 애니메이션은 Flutter 자체가 아닌 시스템에서 처리합니다.
Flutter는 Flutter 앱 내에서 경로 간에 탐색할 때 뒤로 탐색 예측도 지원합니다. PredictiveBackPageTransitionsBuilder
라는 특수 PageTransitionsBuilder가 시스템 뒤로 탐색 예측 동작을 리슨하고 동작의 진행률로 페이지 전환을 실행합니다.
뒤로 탐색 예측은 Android U 이상에서만 지원되지만 Flutter는 원래 뒤로 동작 동작 및 ZoomPageTransitionBuilder로 원활하게 대체됩니다. 자체 앱에서 설정하는 방법에 관한 섹션을 포함한 자세한 내용은 블로그 게시물을 참고하세요.
앱의 ThemeData 구성에서 PageTransitionsTheme를 Android에서 뒤로 탐색 예측을 사용하도록 구성하고 다른 플랫폼에서는 애니메이션 패키지의 페이드 전환 효과를 사용하도록 구성합니다.
lib/main.dart
import 'package:animations/animations.dart'; // NEW
import 'package:flutter/material.dart';
import 'home_screen.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), // NEW
TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.windows: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.linux: FadeThroughPageTransitionsBuilder(), // NEW
},
),
),
home: HomeScreen(),
);
}
}
이제 Navigator.push() 콜백을 MaterialPageRoute로 변경할 수 있습니다.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
FadeThroughTransition을 사용하여 현재 질문 변경하기
AnimatedSwitcher 위젯은 빌더 콜백에서 하나의 애니메이션만 제공합니다. 이를 해결하기 위해 animations
패키지는 PageTransitionSwitcher를 제공합니다.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
OpenContainer 사용
animations
패키지의 OpenContainer 위젯은 두 위젯 간에 시각적 연결을 만들기 위해 확장되는 컨테이너 변환 애니메이션 효과를 제공합니다.
closedBuilder
에서 반환된 위젯이 처음에 표시되고 컨테이너를 탭하거나 openContainer
콜백이 호출되면 openBuilder
에서 반환된 위젯으로 확장됩니다.
openContainer
콜백을 뷰 모델에 연결하려면 새 뷰 모델을 QuestionCard 위젯에 추가하고 'Game Over' 화면을 표시하는 데 사용할 콜백을 저장합니다.
lib/question_screen.dart
class QuestionScreen extends StatefulWidget {
const QuestionScreen({super.key});
@override
State<QuestionScreen> createState() => _QuestionScreenState();
}
class _QuestionScreenState extends State<QuestionScreen> {
late final QuizViewModel viewModel =
QuizViewModel(onGameOver: _handleGameOver);
VoidCallback? _showGameOverScreen; // NEW
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
onPressed:
viewModel.hasNextQuestion && viewModel.didAnswerQuestion
? () {
viewModel.getNextQuestion();
}
: null,
child: const Text('Next'),
)
],
),
body: Center(
child: Column(
children: [
QuestionCard( // NEW
onChangeOpenContainer: _handleChangeOpenContainer, // NEW
question: viewModel.currentQuestion?.question, // NEW
viewModel: viewModel, // NEW
), // NEW
Spacer(),
AnswerCards(
onTapped: (index) {
viewModel.checkAnswer(index);
},
answers: viewModel.currentQuestion?.possibleAnswers ?? [],
correctAnswer: viewModel.didAnswerQuestion
? viewModel.currentQuestion?.correctAnswer
: null,
),
StatusBar(viewModel: viewModel),
],
),
),
);
},
);
}
void _handleChangeOpenContainer(VoidCallback openContainer) { // NEW
_showGameOverScreen = openContainer; // NEW
} // NEW
void _handleGameOver() { // NEW
if (_showGameOverScreen != null) { // NEW
_showGameOverScreen!(); // NEW
} // NEW
} // NEW
}
새 위젯 GameOverScreen을 추가합니다.
lib/question_screen.dart
class GameOverScreen extends StatelessWidget {
final QuizViewModel viewModel;
const GameOverScreen({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Scoreboard(
score: viewModel.score,
totalQuestions: viewModel.totalQuestions,
),
Text(
'You Win!',
style: Theme.of(context).textTheme.displayLarge,
),
Text(
'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
style: Theme.of(context).textTheme.displaySmall,
),
ElevatedButton(
child: Text('OK'),
onPressed: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
),
],
),
),
);
}
}
QuestionCard 위젯에서 Card를 애니메이션 패키지의 OpenContainer 위젯으로 대체하고 viewModel 및 open container 콜백에 두 가지 새 필드를 추가합니다.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.onChangeOpenContainer,
required this.question,
required this.viewModel,
super.key,
});
final ValueChanged<VoidCallback> onChangeOpenContainer;
final QuizViewModel viewModel;
static const _backgroundColor = Color(0xfff2f3fa);
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: OpenContainer( // NEW
key: ValueKey(question), // NEW
tappable: false, // NEW
closedColor: _backgroundColor, // NEW
closedShape: const RoundedRectangleBorder( // NEW
borderRadius: BorderRadius.all(Radius.circular(12.0)), // NEW
), // NEW
closedElevation: 4, // NEW
closedBuilder: (context, openContainer) { // NEW
onChangeOpenContainer(openContainer); // NEW
return ColoredBox( // NEW
color: _backgroundColor, // NEW
child: Padding( // NEW
padding: const EdgeInsets.all(16.0), // NEW
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
);
},
openBuilder: (context, closeContainer) { // NEW
return GameOverScreen(viewModel: viewModel); // NEW
}, // NEW
),
);
}
}
8. 축하합니다
축하합니다. Flutter 앱에 애니메이션 효과를 추가하고 Flutter 애니메이션 시스템의 핵심 구성요소에 대해 알아보았습니다. 구체적으로 다음 내용을 배웠습니다.
- ImplicitlyAnimatedWidget 사용 방법
- ExplicitlyAnimatedWidget 사용 방법
- 애니메이션에 곡선 및 트윈을 적용하는 방법
- AnimatedSwitcher 또는 PageRouteBuilder와 같은 사전 빌드된 전환 위젯을 사용하는 방법
- FadeThroughTransition 및 OpenContainer와 같은
animations
패키지의 멋진 사전 빌드된 애니메이션 효과를 사용하는 방법 - Android에서 뒤로 탐색 예측 지원을 추가하는 등 기본 전환 애니메이션을 맞춤설정하는 방법
다음 단계
다음 Codelab을 확인해 보세요.
- Material 3을 사용하여 애니메이션이 적용된 적응형 앱 레이아웃 빌드
- Flutter용 Material 모션을 사용하여 멋진 전환 빌드하기
- 지루한 Flutter 앱을 멋지게 바꿔보세요
또는 다양한 애니메이션 기법을 보여주는 애니메이션 샘플 앱을 다운로드하세요.
추가 자료
flutter.dev에서 더 많은 애니메이션 리소스를 확인할 수 있습니다.
- 애니메이션 소개
- 애니메이션 튜토리얼 (튜토리얼)
- 암시적 애니메이션 (튜토리얼)
- 컨테이너의 속성 애니메이션 (레시피)
- 위젯 페이드 인 및 페이드 아웃 (레시피)
- 히어로 애니메이션
- 페이지 경로 전환 애니메이션 처리 (레시피)
- 물리 시뮬레이션을 사용하여 위젯 애니메이션화 (레시피)
- 시차 애니메이션
- 애니메이션 및 모션 위젯 (위젯 카탈로그)
또는 Medium의 다음 도움말을 확인하세요.
- 애니메이션 심층 분석
- Flutter의 맞춤 암시적 애니메이션
- Flutter 및 Flux / Redux를 사용한 애니메이션 관리
- 나에게 적합한 Flutter 애니메이션 위젯을 선택하는 방법
- 기본 제공 명시적 애니메이션이 포함된 방향 애니메이션
- 암시적 애니메이션을 사용한 Flutter 애니메이션 기본사항
- AnimatedBuilder 또는 AnimatedWidget은 언제 사용해야 하나요?