독스 : https://docs.flutter.dev/ui/animations
https://www.youtube.com/watch?v=wnARLByOtKA
로티 : https://medium.com/@moralmk/lottie-%EC%83%88%EB%A1%9C%EC%9A%B4-%EC%98%A4%ED%94%88-%EC%86%8C%EC%8A%A4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%8F%84%EA%B5%AC-4488d3da6e26
이 포스트에서는 위젯 등 코드 기반 요소에 적용하기 쉬운 애니메이션에 대해 작성
(Code-based animations)
어떤 것을 사용해서 애니메이션을 구현할 것인가?
적용 대상에 따라 두 가지로 크게 분류할 수 있음
두 가지로 분류 가능
Explicit animation을 사용해야 하는 경우 : 아래 질문 중 1개 이상에 Yes로 답할 때
위 질문에 해당하지 않는 경우는 Implicit 애니메이션을 쓰는 것으로 충분(적절)
각 애니메이션 분류에 실제로 사용해야 하는 위젯은 다음과 같음
내용을 정리하기 위해 아래 순서도를 찬찬히 살펴보면 좋다. 읽기 싫게 생겼지만.
aka animatedFoo
플러터 기본 위젯들에는 이에 대응되는 animated위젯들이 있다.
AnimatedContainer의 배경색을 변경한다고 하면
넣을 수 있는 주요 프로퍼티는
AnimatedFoo(
duration : Duration(seconds: 5),
curve : Curves.easeOut,
...
(원래 Foo위젯에 있는 프로퍼티들),
)
실제로 어떤 컨테이너의 사이즈를 변하게 하는 예시를 들면
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
const appTitle = 'Theme Demo';
return MaterialApp(
title: appTitle,
home: Scaffold(
body: SafeArea(
child: DemoBody(),
),
),
);
}
}
class DemoBody extends StatefulWidget {
const DemoBody({super.key});
State<DemoBody> createState() => _DemoBodyState();
}
class _DemoBodyState extends State<DemoBody> {
bool active = false;
Widget build(BuildContext context) {
return Stack(
children: [
AnimatedContainer(
// 애니메이션을 위한 설정값
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
// 원래 Container에도 있던 프로퍼티들
width: active ? 200 : 0,
height: active ? 200 : 0,
padding: const EdgeInsets.all(10),
alignment: Alignment.center,
color: Colors.amber,
),
ElevatedButton(
child: Text('hi'),
onPressed: () {
setState(() {
active = !active;
});
},
)
],
);
}
}
AnimatedFoo가 없을 경우 커스텀 implicit 애니메이션을 만들기 위한 위젯
tween과 builder가 추가됐다.
ColorTwin
을 twin값으로 던져준다.Tween<double>
이다.TweenAnimationBuilder(
duration : Duration(seconds: 42),
curve : Curve.적당한_커브,
tween : 적절한_트윈_클래스,
builder: (_, 트윈의 현재 값 파라미터, __) {
return 애니메이션 적용할_위젯(),
}
)
예를 들면 다음과 같다. (실행하면 파란 사각형이 360도 돌아간다)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
const appTitle = 'Theme Demo';
return MaterialApp(
title: appTitle,
home: Scaffold(
body: SafeArea(
child: DemoBody(),
),
),
);
}
}
class DemoBody extends StatefulWidget {
const DemoBody({super.key});
State<DemoBody> createState() => _DemoBodyState();
}
class _DemoBodyState extends State<DemoBody> {
bool active = false;
Widget build(BuildContext context) {
return Container(
color: Colors.amber,
alignment: Alignment.center,
child: TweenAnimationBuilder(
tween: Tween<double>(begin: 0, end: 360),
duration: Duration(seconds: 60),
builder: (_, double ang, __) {
return Transform.rotate(
angle: ang,
child: Container(
alignment: Alignment.center,
width: 200,
height: 200,
padding: EdgeInsets.all(20),
color: Colors.indigo,
),
);
}),
);
}
}
FooTransition
위젯과 AnimationController
의 조합으로 Explicit애니메이션을 구성할 수 있다.
Explicit 애니메이션은 StatefulWidget에 들어간다.
FooTransition
생성자 값으로 일단 turns
가 들어가고, 그 외 각기 다른 FooTransition위젯마다 요구하는 프로퍼티가 더 있다.
어떤 프로퍼티는 애니메이션 컨트롤러를 값으로 받는다.
애니메이션 컨트롤러 생성자를 호출할 때 vsync
를 넣어줘야 한다. 수직동기화 해주는 역할이다.
with SingleTickerProviderStateMixin
을 붙여주면 된다.설명도, 코드도. 딱 보면 읽기 싫게 생겼다. 라이프사이클 관리도 해줘야된다.
가능하다면 Implicit 애니메이션 선에서 컷해버리면 삶에 도움이 된다.
class WidgetName extends StatefulWidget {
_WidgetNameState createState() => _WidgetNameState();
}
class _WidgetNameState extends State<WidgetName>
with SingleTickerProviderStateMixin {
AnimationController _ac;
void initState() {
super.initState();
_ac = AnimationController(
duration : Duration(seconds: 123),
vsync : this, // 플러터에게 변화를 알려주는 ticker provider. (수직동기화)
// 이 위젯(_WidgetNameState)을 vsync값으로 넘겨준다.
// ** 위에서 with로 믹스인을 가져왔으니까 가능한 일
);
}
Widget build(BuildContext context) {
return 뭐시기Transition(
애니메이션_컨트롤러_받는_프로퍼티 : 애니메이션_컨트롤러,
// 그리고 이 트랜지션 위젯이 요구하는 다른 프로퍼티들.
);
}
void dispose() {
_ac.dispose();
super.dispose();
}
}
그래서 실제로 동작하는 예를 들면 다음과 같다.
TweenAnimationBuilder와 마찬가지로 사각형이 회전하는 예제다.
차이점은
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
const appTitle = 'Theme Demo';
return MaterialApp(
title: appTitle,
home: Scaffold(
body: SafeArea(
child: DemoBody(),
),
),
);
}
}
class DemoBody extends StatefulWidget {
const DemoBody({super.key});
State<DemoBody> createState() => _DemoBodyState();
}
class _DemoBodyState extends State<DemoBody>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_controller.repeat();
}
Widget build(BuildContext context) {
return Center(
child: RotationTransition(
alignment: Alignment.center,
turns: _controller,
child: Container(
color: Colors.amber,
width: 100,
height: 100,
),
),
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
}
Custom Explicit 애니메이션을 만든다.
별거 없다.
FooTransition에서 했던 아래 내용은 다 똑같다
달라지는건 FooTransition 대신 AnimatedBuilder를 쓴다는 것 뿐
builder함수에 내부구현이 좀 들어가겠지만, 난이도는 별 차이가 없다. 그게 그거다.
// FooTransition 시절의 build 메소드
Widget build(BuildContext context) {
return 뭐시기Transition(
애니메이션_컨트롤러_받는_프로퍼티 : 애니메이션_컨트롤러,
// 그리고 이 트랜지션 위젯이 요구하는 다른 프로퍼티들.
);
}
// -------------------------
Widget build(BuildContext context) {
return AnimatedBuilder(
animation : 애니메이션_컨트롤러
builder : (_, __) {
return 애니메이션_적용할_위젯
}
);
}
사각형 돌아가는걸 RotationTransition말고 AnimatedBuilder로 구현하면 아래와 같다.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
const appTitle = 'Theme Demo';
return MaterialApp(
title: appTitle,
home: Scaffold(
body: SafeArea(
child: DemoBody(),
),
),
);
}
}
class DemoBody extends StatefulWidget {
const DemoBody({super.key});
State<DemoBody> createState() => _DemoBodyState();
}
class _DemoBodyState extends State<DemoBody>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 5),
vsync: this,
);
_controller.repeat();
}
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (_, __) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14,
child: Stack(
children: [
Container(
color: Colors.amber,
width: 100,
height: 100,
),
Text(_controller.value.toString().substring(0, 4)),
],
),
);
},
),
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
}