์ ๋๋ฉ์ด์ - ์์์ ์์ ๋ช ์์ , Hero
์ํ ๊ด๋ฆฌ์์ ๋ฐฐ์ด ๋๋ก ๊ฐ์ ๋ฐ๊พธ๋ฉด ํ๋ฉด์ด ๋ค์ ๊ทธ๋ ค์ง๋ค. ๊ทธ๋ฐ๋ฐ ๋ค์ ๊ทธ๋ ค์ง๋ ์๊ฐ ํ๋ฉด์ ํ ๋ฐ๋๋ค. ์์๋ฅผ ๋๋ฅด๋ฉด ์ปค์ง๊ฒ ๋ง๋ค์ด๋ณด์.
class GrowBox extends StatefulWidget {
const GrowBox({super.key});
@override
State<GrowBox> createState() => _GrowBoxState();
}
class _GrowBoxState extends State<GrowBox> {
bool big = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => big = !big),
child: Container(
width: big ? 200 : 100,
height: big ? 200 : 100,
color: Colors.blue,
),
);
}
}ํญํ๋ฉด 100์ง๋ฆฌ ์์๊ฐ ์๊ฐ์ ์ผ๋ก 200์ง๋ฆฌ๋ก ๋ฐ๋๋ค. ๋ฐ๋๋ ๊ฑด ๋ง๋๋ฐ ๋ถ๋๋ฝ์ง ์๋ค. ๊ฐ๋ง ๋ฐ๊ฟจ์ ๋ฟ "100์์ 200๊น์ง ์ฒ์ฒํ"๋ผ๋ ์ ๋ณด๊ฐ ์๊ธฐ ๋๋ฌธ์ด๋ค.
ํด๊ฒฐ 1: ์์์ ์ ๋๋ฉ์ด์ - Container๋ฅผ AnimatedContainer๋ก ๋ฐ๊พผ๋ค
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: big ? 200 : 100,
height: big ? 200 : 100,
color: Colors.blue,
)Container ์์ Animated๋ฅผ ๋ถ์ด๊ณ duration๋ง ์ฃผ๋ฉด ๋์ด๋ค. ์ด์ big์ด ๋ฐ๋ ๋๋ง๋ค ์ด์ ๊ฐ์์ ์ ๊ฐ๊น์ง 300๋ฐ๋ฆฌ์ด ๋์ ์์์ ์ด์ด์ ๊ทธ๋ ค์ค๋ค.
duration: ์ผ๋ง ๋์ ์์ง์ผ์ง๋ค. ํ์ ๊ฐ์ด๋คcurve: ์์ง์์ ๊ฐ์ ๋ชจ์์ด๋ค.Curves.easeInOut์ ์ฒ์ฒํ ์์ํด ๋นจ๋ผ์ก๋ค๊ฐ ์ฒ์ฒํ ๋๋๋ค. ์๋ตํ๋ฉด ์ผ์ ํ ์๋๋ก ์์ง์ธ๋ค
์ด๋ฐ ๋ฐฉ์์ ์์์ (implicit) ์ ๋๋ฉ์ด์ ์ด๋ผ๊ณ ๋ถ๋ฅธ๋ค. "์์ง์ฌ๋ผ"๊ณ ์ํจ ์ ์ด ์๊ณ , ๊ฐ์ด ๋ฐ๋์๋ค๋ ์ฌ์ค๋ง์ผ๋ก ์์ง์์ด ์์๋๊ธฐ ๋๋ฌธ์ด๋ค. ์ปจํธ๋กค๋ฌ๋ ํ์ ์๋ค. ์์ฑ๋ณ๋ก ์ด๋ฐ ์์ ฏ์ด ๋ฐ๋ก ์๋ค.
AnimatedContainer: ํฌ๊ธฐ, ์, ์ฌ๋ฐฑ, ๋ชจ์๋ฆฌ ๋ฑ ์ฌ๋ฌ ์์ฑAnimatedOpacity: ํฌ๋ช ๋AnimatedAlign,AnimatedPositioned: ์์นAnimatedSwitcher: ์์ ฏ์ด ๋ค๋ฅธ ์์ ฏ์ผ๋ก ๋ฐ๋ ๋ ๊ต์ฒด ํจ๊ณผ
๊ธ์ ์ซ์๊ฐ ์ฌ๋ผ๊ฐ๋ ๊ฒ์ฒ๋ผ ์ ํด์ง ์์ ฏ์ด ์๋ ๊ฐ์ TweenAnimationBuilder๋ก ์ฒ์ ๊ฐ๊ณผ ๋ ๊ฐ์ ์ง์ ํด ์์ง์ด๊ฒ ํ ์ ์๋ค.
์์์ ์ ๋๋ฉ์ด์ ์ผ๋ก ๋ถ์กฑํ ๋
์์์ ์ ๋๋ฉ์ด์ ์ "๊ฐ์ด ๋ฐ๋๋ฉด ๊ทธ ๋ฐฉํฅ์ผ๋ก ํ ๋ฒ ์์ง์ธ๋ค"๋ง ํ๋ค. ์ด๋ฐ ๊ฑด ๋ชป ํ๋ค.
- ๋น๊ธ๋น๊ธ ๊ณ์ ๋ฐ๋ณตํ๊ธฐ
- ์ค๊ฐ์ ๋ฉ์ท๋ค๊ฐ ์ด์ด์ ํ๊ธฐ, ๊ฑฐ๊พธ๋ก ๋๊ฐ๊ธฐ
- ์ฌ๋ฌ ์์์ ํ์ด๋ฐ์ ์๋ก ๋ง์ถ๊ธฐ
์ด๋ ๋ช
์์ (explicit) ์ ๋๋ฉ์ด์
์ ์ด๋ค. ์์ง์์ ์งํํ๋ AnimationController๋ฅผ ์ง์ ๋ง๋ ๋ค.
ํด๊ฒฐ 2: ๋ช ์์ ์ ๋๋ฉ์ด์ - AnimationController
class Spinner extends StatefulWidget {
const Spinner({super.key});
@override
State<Spinner> createState() => _SpinnerState();
}
class _SpinnerState extends State<Spinner>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(); // ๊ณ์ ๋ฐ๋ณต
}
@override
void dispose() {
_controller.dispose(); // ์ด ๊ฑด ์ ๋ฆฌ
super.dispose();
}
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _controller,
child: const Icon(Icons.refresh, size: 48),
);
}
}์ฒ์ ๋ณด๋ฉด ๋ฏ์ ์ค์ด ๋ง์ง๋ง, ์ด๋ฏธ ์๋ ๊ฒ๋ค์ ์กฐํฉ์ด๋ค.
late final AnimationController _controller์initState์์ ์ฑ์ฐ๊ธฐ: ์ฒซ ๊ธ์late final,StatefulWidget๊ธ์initState๋ค. ์ปจํธ๋กค๋ฌ๋State๊ฐ ๋ง๋ค์ด์ง ๋ค์์ผ ๋ง๋ค ์ ์์ด์late๊ฐ ํ์ํ๋คwith SingleTickerProviderStateMixin: ์์ ๊ธ์ mixin์ด๋ค. ํ๋ฉด์ด ํ ํ๋ ์ ๊ทธ๋ ค์ง ๋๋ง๋ค ์๋ ค์ฃผ๋ ํฐ์ปค๋ฅผ State์ ์น๋๋ค. ์ปจํธ๋กค๋ฌ๊ฐ ํ๋๋ฟ์ด๋ฉดSingleTicker..., ์ฌ๋ฌ ๊ฐ๋ฉดTickerProviderStateMixin์ ์ด๋คvsync: this: "์ด State๊ฐ ํ๋ ์ ์ ํธ๋ฅผ ์ค๋ค"๊ณ ์ปจํธ๋กค๋ฌ์ ์๋ ค์ค๋ค. ํ๋ฉด ๋ฐ์ ์์ ๋ ๋ถํ์ํ๊ฒ ๋์ง ์๊ฒ ํ๋ค..repeat(): ์์ฑ์ ๊ธ์์ ๋ณธ..๋ค. ์ปจํธ๋กค๋ฌ๋ฅผ ๋ง๋ค๋ฉด์ ๋ฐ๋กrepeat()๋ฅผ ํธ์ถํ๋คdispose()์์_controller.dispose(): ์ ๋ ฅ ๊ธ์TextEditingController์ ๊ฐ๋คRotationTransition(turns: _controller): ์ปจํธ๋กค๋ฌ๋ 0์์ 1๊น์ง ๋ณํ๋ ๊ฐ์ด๊ณ ,turns: 1์ด ํ ๋ฐํด๋ค. 2์ด๋ง๋ค ํ ๋ฐํด ๋๋ค
์ปจํธ๋กค๋ฌ๋ ์ด๋ฐ ๋์ ํจ์๋ฅผ ๊ฐ์ง๊ณ ์๋ค.
_controller.forward(); // ์ฒ์๋ถํฐ ๋๊น์ง ์ฌ์
_controller.reverse(); // ๊ฑฐ๊พธ๋ก ์ฌ์
_controller.stop(); // ๋ฉ์ถค
_controller.repeat(); // ๋ฐ๋ณต์ฝ์ง ํฌ์ธํธ 1: with๋ฅผ ๋นผ๋จน์ผ๋ฉด vsync์์ ๋งํ๋ค
class _SpinnerState extends State<Spinner> { // with SingleTickerProviderStateMixin ์์
...
_controller = AnimationController(vsync: this); // ์ฌ๊ธฐ์ ์๋ฌ๋จ
}this๋ฅผ TickerProvider๋ก ์ธ ์ ์๋ค๋ ์๋ฌ๋ค. this(State)๊ฐ ํฐ์ปค๋ฅผ ์ ๊ณตํ์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ด๋ with SingleTickerProviderStateMixin์ ๋ถ์ด๋ฉด ํ๋ฆฐ๋ค.
์ฝ์ง ํฌ์ธํธ 2: dispose๋ฅผ ๋นผ๋จน์ผ๋ฉด ํ๋ฉด์ ๋๊ฐ ๋ ์๋ฌ๊ฐ ๋๋ค
์ปจํธ๋กค๋ฌ๋ ํ๋ ์๋ง๋ค ๊นจ์ด๋๋ ํฐ์ปค๋ฅผ ๋ค๊ณ ์๋ค. dispose์์ ์ ๋ฆฌํ์ง ์์ผ๋ฉด ํ๋ฉด์ด ์ฌ๋ผ์ง ๋ ํฐ์ปค๊ฐ ์ด์ ์๋ ์ฑ๋ก ํ๊ธฐ๋์๋ค๋ ์ทจ์ง์ ์๋ฌ๊ฐ ๋๊ณ , ๋ฉ๋ชจ๋ฆฌ๋ ์๋ค. StatefulWidget์์ initState์์ ๋ง๋ ๊ฑด dispose์์ ์ ๋ฆฌํ๋ค๋ ๊ท์น์ด ์ฌ๊ธฐ๋ ๋๊ฐ์ด ์ ์ฉ๋๋ค.
์ฝ์ง ํฌ์ธํธ 3: ์ปจํธ๋กค๋ฌ๊ฐ ๋ ๋๋ง๋ค setState๋ฅผ ๋ถ๋ฅด๋ฉด ๋น์ธ๋ค
์ปจํธ๋กค๋ฌ ๊ฐ์ด ๋ฐ๋ ๋๋ง๋ค setState๋ก ํ๋ฉด ์ ์ฒด๋ฅผ ๋ค์ ๊ทธ๋ฆฌ๋ ์ฝ๋๋ฅผ ์ฐ๊ธฐ ์ฝ๋ค. ์์ง์ด๋ ๋ถ๋ถ๋ง ๋ค์ ๊ทธ๋ฆฌ๊ณ ์ถ์ผ๋ฉด AnimatedBuilder๋ฅผ ์ด๋ค.
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _controller.value,
child: child,
);
},
child: const FlutterLogo(size: 100), // ์ ๋ณํ๋ ๋ถ๋ถ์ child๋ก ๋นผ์ ๋ค์ ์ ๋ง๋ ๋ค
)์ปจํธ๋กค๋ฌ ๊ฐ์ด ๋ฐ๋ ๋๋ง๋ค builder๋ง ๋ค์ ์คํ๋๋ค. ์ ๋ณํ๋ ๋ถ๋ถ์ child๋ก ๋๊ฒจ์ ์ฌ์ฌ์ฉํ๋ค. ๊ฐ์ ๋ฒ์๋ฅผ 0~1์ด ์๋๋ผ ์ํ๋ ๋ฒ์๋ก ๋ฐ๊พธ๋ ค๋ฉด Tween(begin: 0.0, end: 200.0).animate(_controller)๋ฅผ ์ด๋ค.
๋ ์ค ๋ญ ์ฐ๋
- ๊ฐ์ด ๋ฐ๋ ๋ ๋ถ๋๋ฝ๊ฒ ์ด์ด์ง๋ฉด ์ถฉ๋ถํ๋ค โ ์์์ (
Animated...). ์ฝ๋๊ฐ ํจ์ฌ ์งง๋ค - ๋ฐ๋ณต, ๋ฉ์ถค, ๋๊ฐ๊ธฐ, ์ฌ๋ฌ ์์ง์์ ๋๊ธฐํ๊ฐ ํ์ํ๋ค โ ๋ช
์์ (
AnimationController)
์ฒ์์๋ ์์์ ์ผ๋ก ์์ํ๊ณ , ์์์ ์ผ๋ก ์ ๋๋ ์๊ฐ์ ๋ช ์์ ์ผ๋ก ๋์ด๊ฐ๋ค.
ํด๊ฒฐ 3: Hero - ํ๋ฉด ์ฌ์ด๋ฅผ ๋ ์๊ฐ๋ ์์ง์
๋ชฉ๋ก์ ์ฌ์ง์ ๋๋ฅด๋ฉด ๊ทธ ์ฌ์ง์ด ์์ธ ํ๋ฉด์ ํฐ ์ฌ์ง์ผ๋ก ๋ ์๊ฐ๋ฉฐ ์ปค์ง๋ ์์ง์์ Hero๊ฐ ํด์ค๋ค. ์ปจํธ๋กค๋ฌ๊ฐ ํ์ ์๋ค.
// ๋ชฉ๋ก ํ๋ฉด
Hero(
tag: 'avatar-${user.id}',
child: CircleAvatar(child: Text(user.name[0])),
)
// ์์ธ ํ๋ฉด - ๊ฐ์ tag
Hero(
tag: 'avatar-${user.id}',
child: CircleAvatar(radius: 60, child: Text(user.name[0])),
)๋ ํ๋ฉด์์ ๊ฐ์ tag๋ฅผ ๊ฐ์ง Hero๋ฅผ ์ฐพ์์, ํ๋ฉด์ด ์ ํ๋๋ ๋์ ์์น์ ํฌ๊ธฐ๋ฅผ ์ด์ด์ ์์ง์ธ๋ค. ํ๋ฉด ์ด๋ ๊ธ์์ ๋ณธ context.push('/detail/${user.id}') ๊ฐ์ ์ด๋๋ง ์์ผ๋ฉด ๋๋ค.
ํจ์ : tag๊ฐ ๊ฒน์น๋ฉด ์๋ฌ๊ฐ ๋๋ค
ListView(
children: users.map((u) => Hero(tag: 'avatar', child: ...)).toList(), // ๋ชจ๋ ํญ๋ชฉ์ด ๊ฐ์ tag
)ํ ํ๋ฉด ์์ ๊ฐ์ tag๊ฐ ๋ ์ด์ ์์ผ๋ฉด ์ด๋ ๊ฑธ ๋ ๋ ค์ผ ํ ์ง ๋ชฐ๋ผ์ ์๋ฌ๊ฐ ๋๋ค. tag๋ ํญ๋ชฉ๋ง๋ค ๊ณ ์ ํ ๊ฐ์ผ๋ก ๋ง๋ ๋ค. ์ ์์์ฒ๋ผ 'avatar-${user.id}'๋ก ํญ๋ชฉ์ id๋ฅผ ๋ถ์ธ๋ค. ๋ ํ๋ฉด์ ๊ฐ์ ํญ๋ชฉ์ ๊ฐ์ tag๋ฅผ ๋ฐ๊ณ , ๊ฐ์ ํ๋ฉด์ ๋ค๋ฅธ ํญ๋ชฉ์ ๋ค๋ฅธ tag๋ฅผ ๋ฐ๋๋ค.
Compose, SwiftUI์ ๋น๊ตํ๋ฉด
// Compose
val size by animateDpAsState(if (big) 200.dp else 100.dp)
Box(Modifier.size(size).background(Color.Blue).clickable { big = !big })// SwiftUI
Rectangle()
.fill(.blue)
.frame(width: big ? 200 : 100, height: big ? 200 : 100)
.onTapGesture { withAnimation { big.toggle() } }- ๊ฐ์ด ๋ฐ๋ ๋ ๋ถ๋๋ฝ๊ฒ: Flutter
AnimatedContainer๊ฐ์Animated...์์ ฏ / Composeanimate*AsState/ SwiftUIwithAnimation,.animation - ์ง์ ์งํํ๋ ์์ง์: Flutter
AnimationController+Tween/ ComposeAnimatable,updateTransition/ SwiftUI๋TimelineView๋phaseAnimator - ํ๋ฉด ์ฌ์ด ๊ณต์ ์์: Flutter
Hero/ ComposeSharedTransitionLayout/ SwiftUImatchedGeometryEffect - ์ ๋ฆฌํด์ผ ํ๋๊ฐ: Flutter ์ปจํธ๋กค๋ฌ๋
disposeํ์ / Compose์ SwiftUI๋ ์๋
Flutter์ Animated... ์์ ฏ์ Compose์ animate*AsState์ SwiftUI์ withAnimation์ ํด๋นํ๋ค. ๋ค๋ง Flutter๋ ์์ ฏ ์ด๋ฆ์ผ๋ก ๋์ ์์ฑ์ด ์ ํด์ ธ ์๋ค. ๋ช
์์ ์ ๋๋ฉ์ด์
์์๋ ์ปจํธ๋กค๋ฌ๋ฅผ ์ง์ ๋ง๋ค๊ณ dispose๋ก ์ ๋ฆฌํด์ผ ํด์ ์ฝ๋๊ฐ ๊ธธ์ด์ง๋ค.
์ ๋ฆฌ
- ๊ฐ์
setState๋ก ๋ฐ๊พธ๊ธฐ๋ง ํ๋ฉด ํ๋ฉด์ด ์๊ฐ์ ์ผ๋ก ๋ฐ๋๋ฏ๋ก,Container๋์AnimatedContainer์ฒ๋ผAnimated...์์ ฏ์duration์ ์ฃผ๋ฉด ๋ถ๋๋ฝ๊ฒ ์ด์ด์ง๋ค (์์์ ์ ๋๋ฉ์ด์ ) - ๋ฐ๋ณต, ๋ฉ์ถค, ๋๊ฐ๊ธฐ, ์ฌ๋ฌ ์์ง์์ ๋๊ธฐํ๊ฐ ํ์ํ๋ฉด
AnimationController๋ฅผ ์ฐ๋ ๋ช ์์ ์ ๋๋ฉ์ด์ ์ผ๋ก ๊ฐ๋ค - ์ปจํธ๋กค๋ฌ๋ฅผ ์ฐ๋ ค๋ฉด State์
with SingleTickerProviderStateMixin์ ๋ถ์ด๊ณvsync: this๋ฅผ ์ฃผ๋ฉฐ,initState์์ ๋ง๋ค๊ณdispose์์ ๋ฐ๋์ ์ ๋ฆฌํ๋ค - ์ปจํธ๋กค๋ฌ ๊ฐ์ ๋ง์ถฐ ์ผ๋ถ๋ง ๋ค์ ๊ทธ๋ฆฌ๋ ค๋ฉด
setState๋ณด๋คAnimatedBuilder๋ฅผ ์ฐ๊ณ , ์ ๋ณํ๋ ๋ถ๋ถ์child๋ก ๋บ๋ค Hero๋ ๋ ํ๋ฉด์์ ๊ฐ์tag๋ฅผ ๊ฐ์ง ์์ ฏ์ ์ด์ด์ ๋ ๋ ค์ฃผ๊ณ , ํ ํ๋ฉด ์์์tag๊ฐ ๊ฒน์น๋ฉด ์๋ฌ๊ฐ ๋๋ค- ์ฒ์์๋ ์์์ ์ผ๋ก ์์ํ๊ณ , ๋ถ์กฑํ ๋ ๋ช ์์ ์ผ๋ก ๋์ด๊ฐ๋ค
๋ค์ ๊ธ์์๋ Flutter์์ Kotlin๊ณผ Swift๋ก ์ง ๋ค์ดํฐ๋ธ ์ฝ๋๋ฅผ ํธ์ถํ๋ ํ๋ซํผ ์ฑ๋์ ๋ค๋ค๋ณด๊ฒ ๋ค.
