Flutter Animation을 직접 만들고 싶었다. 그래서 자료를 살펴보는데, "Matrix4"를 알아야 했다. 이것은 행렬이다. 이 행렬을 위젯으로 만들면 Transform Widget이라고 한다. 그래서 먼저 TransformWidget을 사용해보면서 감을 잡고자 이 글을 작성한다.
나는 아래와 같은 애니메이션을 직접 만들고 싶었다.
출처: loading_animation_widget package
"transforms" 은 형태나 사이즈 위치를 변경하는 것을 뜻한다. 그것을 Widget을 감싸서 처리하면 된다.
cf) flutter Docs: Transform class
공식문서에서는 아래와 같이 정의하고 있다.
A widget that applies a transformation before painting its child.
하위 위젯을 그리기 전에, 형태를 변형하도록 해주는 위젯
그리고 생성자를 보면 끝이다. 아래 gif를 보면 단박에 이해가 될 것이다.
Transform.flip
출처: Medium: Flutter:flip animation
Transform.rotate
(출처: Medium: A Deep Dive Into Transform Widgets in Flutter)
Transform.scale
(출처: Medium: A Deep Dive Into Transform Widgets in Flutter)
Transform.translate
: 이 친구는 그냥 이동이다.간단하게 예제를 만들어보자.
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
title: 'My app', // used by the OS task switcher
home: const MyApp(),
),
);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Transform.rotate(
angle: 1.0,
origin: Offset(0.0, 0.0),
child: Container(
height: 100.0,
width: 100.0,
color: Colors.indigo,
),
),
),
);
}
}
이 코드를 실행해보면, 아래와 같다.
잘 돌아가있지만, 심심하다. 그렇다면 animation을 불어넣어주면 된다.
이렇게 회전시키는 것은 AnimationController와 Aniation 을 사용하여 구현할 수 있다.
제일 먼저, 두 참조변수를 선언한다.
class _MyState extends State<MyApp> {
// 1. Controller & Aniation 선언
late AnimationController _controller;
late Animation<double> _animation;
}
이제 참조변수에 값을 전달한다. 이것은 initState
시점에 한다. 이 때, "SingleTickerProviderStateMixin" 를 조합한다. (mixin 이라 확장이라는 용어 대신 조합)
class _MyState extends State<MyApp> with SingleTickerProviderStateMixin {
...
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(begin: 0.0, end: 2 * pi)
.animate(controller)
..addListener(() {
setState(() {});
})
..addStatusListener((state) {
if (status == AnimationStatus.completed) {
_controller.repeate();
_controller.forward();
}
});
_controller.forward();
}
}
SingleTickerProviderStateMixin
: 애니메이션을 시작하기 위해서는 '티커' 라는 개념이 필요하다. 그 티커가 동작하도록 하게 해주는 mixin이다.vsync
: vsync에 보면 this를 건내주고 있다. 이로써. Animation을 그릴 때, 프레임에 맞게 그릴 수 있도록 되는 것이다. _animation
에 할당한 코드를 보면 "Tween" 이라는 객체로 할당하고 있다. 이것은 시작지점과 끝지점만 지정해주고 이것을 animation 하는 위젯에 전달한다. 그러면 마치 총알을 쏘듯 저기에 있는 값들을 입력해준다.이후 코드는 dispose 시점에 controller를 메모리 해제하는 것과 Transform.rotate에 돌아갈 각도에다가 _anmation
을 전달하면 된다.
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
title: 'My app', // used by the OS task switcher
home: const MyApp(),
),
);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
// 1. Controller와 Animation을 선언한다.
late AnimationController _controller;
late Animation<double> _animation;
void initState() {
super.initState();
// 2. 선언한 참조변수들에게 값을 할당한다.
// 이 때, with 를 사용하여 mixin을 조합한다.(extends 옆에 추가)
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this, // with를 통해 기능을 조합하면 this를 사용할 수 있다.
);
_animation = Tween<double>(begin: 0.0, end: pi *2).animate(_controller)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.repeat();
// _controller.forward();
}
});
void initState() {
super.initState();
// 2. 선언한 참조변수들에게 값을 할당한다.
// 이 때, with 를 사용하여 mixin을 조합한다.(extends 옆에 추가)
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this, // with를 통해 기능을 조합하면 this를 사용할 수 있다.
);
_animation = Tween<double>(begin: 0.0, end: pi *2).animate(_controller)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.repeat();
_controller.forward();
}
});
_controller.forward();
}
_controller.forward();
}
void dispose() {
// 3. Controller를 dispose 해준다.
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Transform.rotate(
angle: _animation.value,
origin: Offset(0.0, 0.0),
child: Stack(
children: [
Container(
height: 100.0,
width: 100.0,
color: Colors.indigo,
),
CircleAvatar(
radius: 10.0,
backgroundColor: Colors.red,
),
],
),
),
),
);
}
}
build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateZ(_rotationAnimation.value) // <-
..scale(_scaleAnimation.value), // <--
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: FlutterLogo(),
),
);
},
);
}
Widget