코드팩토리님의 애니메이션 영상중에서 "[Flutter] 화투 카드 뒤집는 애니메이션 만들기" 를 따라하고 있었다.
아래 코드는 Card의 모습을 구성하는 Widget 생성 메서드이다.
renderCard({
required Key key,
bool isBack = true,
bool isThree = true,
}) {
String path = 'assets/images/';
if (isBack) {
path += 'back.jpeg';
} else {
if (isThree) {
path += '3.jpeg';
} else {
path += '8.jpeg';
}
}
return Image.asset(
// key: key, // <-------- 여기!!!
path,
width: 130.0,
height: 200.0,
);
}
그리고 애니메이션이 적용된 부분 코드이다.
import 'package:flutter/material.dart';
class CardFlipScreen extends StatefulWidget { ... }
class _CardFlipScreenState extends State<CardFlipScreen> {
bool showFront = false;
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
// 여기서 상태값 변경 + UI 업데이트 명령 전달을 수행하고 있다.
onTap: () => setState(() => showFront = !showFront),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 1000), // 1sec
child: showFront ? renderFront() : renderBack(),
),
),
],
),
],
);
}
}
GestureDetector
에서 "setState()" 호출하여 UI 업데이트 사항을 전파하고 있다.transitionBuilder
에 default가 FadeAnimation 이므로, 해당 애니메이션 적용이 되길 기대한다.이상태에서 동작하면 다음과 같이 동작한다.
(깜빡거리듯 변경된다.)
우리가 원하는 동작은 깜빡거림이 아닌, FadeAnimation이다.
결론먼저 말하면,
원하는 FadeAnimation이 동작하지 않는 "Key" 값이 없기 때문이다.
그러면 왜 Key값이 필요한 것일까?
Flutter의 Widget Tree 재사용해야하는지 아니면 새롭게 생성해야하는지 판단 기준이 "Key"이다.
그래서 문제를 해결한 코드는 아까 주석처리한 부분을 주석해제하면 끝이다.
return Image.asset(
key: key, // <-------- 여기!!!
path,
width: 130.0,
height: 200.0,
);
문제는 해결했지만, 그래도 의문점이 남았다.
Q. 그래도 위에 gif를 보면, 이미지는 변경되고 있으면 위젯을 재생성한 것 아닌가?
답은, 일부분은 재생성했고 일부분은 재사용을 했다. 다만, 애니메이션을 적용하기 위해서는 전체를 다시 생성해야만 애니메이션이 적용된다.
Image
Widget의 '경로'만 변경된 것이다. '경로'만 변경되었다면, 해당 "Element"만 변경된다.
정확히는 "assetName"만 변경된 것이다. 나머지는 모두 재사용된다. 그러면 이미지만 변경이 되는 것이고, 위젯자체가 재사용되는 것은 아니다.
그래서 이미지의 애니메이션은 적용이 되진 않고 이미지만 변경된 것이다.
Flutter Framework에게 다시 그리도록 명령하는 가장 확실한 방법이, 다른 "key" 값인 위젯으로 변경하고 UI Update 명령을 전달하는 것이다.
import 'package:flutter/material.dart';
class CardFlipScreen extends StatefulWidget {
const CardFlipScreen({super.key});
State<CardFlipScreen> createState() => _CardFlipScreenState();
}
class _CardFlipScreenState extends State<CardFlipScreen> {
late bool showFront = false;
void initState() {
super.initState();
showFront = false;
}
renderCard({
required Key key,
bool isBack = true,
bool isThree = true,
}) {
String path = 'assets/images/';
if (isBack) {
path += 'back.jpeg';
} else {
if (isThree) {
path += '3.jpeg';
} else {
path += '8.jpeg';
}
}
return Image.asset(
key: key,
path,
width: 130.0,
height: 200.0,
);
}
renderFront({
bool isThree = true,
}) {
return renderCard(
key: ValueKey(isThree ? '3' : '8'),
isBack: false,
isThree: isThree,
);
}
renderBack() {
return renderCard(
key: const ValueKey(false),
);
}
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () => setState(() => showFront = !showFront),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 1000), // 1sec
child: showFront ? renderFront() : renderBack(),
),
),
],
),
],
);
}
}