마인디 프로젝트를 하며 뱃지 기능을 추가할 기회가 생겼다.
유저가 뱃지를 획득한 순간,
단순하게 이미지만 보여주기보단 애니메이션을 통한 시각적 요소를 더해 축하하는 의미를 더하고 싶었다.
(글 작성에 사용한 이미지는 무료 이미지를 사용함, 마인디 뱃지는 훨씬 귀여움)
여러 뱃지 획득 인터랙션 중, 다음 링크의 모습을 Flutter로 구현해보고자 했다.
https://kr.pinterest.com/pin/146085581658042462/
(좀 있어보임)
class _FlipBadgeState extends State<FlipBadge>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation _animation;
List<String> images = [
'assets/star-medal.png',
'assets/badge.png',
'assets/award.png',
];
int _currentIndex = 0;
먼저 애니메이션의 프레임 관리를 최적화 하기 위한 SingleTickerProviderStateMixin
을 추가하고,
애니메이션 컨트롤러와, 애니메이션 객체를 선언한다.
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
)..addListener(_onAnimationUpdate);
_animation = TweenSequence([
TweenSequenceItem(
tween: Tween(begin: 90.0, end: 0.0).chain(
CurveTween(curve: Curves.fastOutSlowIn),
),
weight: 3,
),
TweenSequenceItem(
tween: Tween(begin: 0.0, end: -10.0).chain(
CurveTween(curve: Curves.easeOut),
),
weight: 1,
),
]).animate(_animationController);
_animationController.forward();
}
void dispose() {
_animationController.dispose();
super.dispose();
}
이제 _animationController
와 _animaion
을 초기화 해보자.
간단하게 _animationController
에는 애니메이션이 움직일 시간을 설정하는 Duration
과, 리스너를 추가해준다.
나는 뱃지 애니메이션이 손날처럼 y축으로 수직
인 상태에서 시작하고,
4/3 바퀴를 돌아 평면이 된 후,
천천히 약간 더 돌면서 멈추는 방식으로 구현하고자 했다.
따라서 TweenSequence
를 이용해서 복합 애니메이션을 구성했고,
각각의 애니메이션은 다음과 같다.
1. 애니메이션-1
Tween(begin: 90.0, end: 0.0)
CurveTween(curve: Curves.fastOutSlowIn)
weight: 3
2.애니메이션-2
Tween(begin: 0.0, end: -10.0)
CurveTween(curve: Curves.easeOut)
weight: 1
void _onAnimationUpdate() {
if (_animationController.status == AnimationStatus.completed &&
_animation.value == -10.0) {
setState(() {
_currentIndex = (_currentIndex + 1) % images.length;
});
_animationController.reset(); // 애니메이션 초기화
_animationController.forward(); // 다시 시작
}
}
리스너는 애니메이션 종료 시점을 캐치하여 기능을 실행하도록 했는데
하나의 모달에 단일 이미지만 보여주기 때문에,
여러 animation controller를 사용하기보다는 하나의 컨트롤러를 사용해 애니메이션을 초기화하고, 다시 정방향 실행해주는 방식으로 사용했다.
build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.002)
..rotateY(_animation.value * (math.pi / 180))
..rotateX(-5 * (math.pi / 180)),
child: Stack(
clipBehavior: Clip.antiAlias,
children: [
Positioned(
top: 5,
left: 5,
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Image.asset(
images[_currentIndex],
color: Colors.black.withOpacity(0.4),
colorBlendMode: BlendMode.srcATop,
width: 200,
height: 200,
),
),
),
Image.asset(
images[_currentIndex],
width: 200,
height: 200,
),
],
),
);
},
);
}
Widget
위젯에서는 크게 Transform
위젯을 사용해 회전을 구현하고,
이미지 필터를 사용해 입체적인 느낌을 살렸다.
transform: Matrix4.identity()
..setEntry(3, 2, 0.002)
..rotateY(_animation.value * (math.pi / 180))
..rotateX(-5 * (math.pi / 180)),
먼저 identity()
로 항등행렬을 만들고,
setEntry(3, 2, 0.002)
로 z축 원근 효과를 추가했다.
애니메이션 값을 이용해 y축으로 회전시키고,
약간의 입체 효과를 추가하기 위해 x축 회전도 추가했다.
행렬 변환을 찾아보니 아래 표의 왼쪽 행렬이 원근 효과를 준다고 한다.
하지만 실제 코드에서 적용해보니 위치를 이동해버렸다..
그래서 표의 오른쪽 행렬을 이용하니 원근 효과를 줄 수 있었다. setEntry 참고
(왜지...?)
문서상 원근 행렬..? | 실제 적용한 행렬 |
---|---|
![]() | ![]() |
여기까지 애니메이션을 추가하니 그럴듯한 뱃지 획득 인터랙션이 완성됐다.
하지만 그림자를 추가해 조금 더 입체적인 느낌을 살리고 싶었다..! (깔롱 Time)
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Image.asset(
images[_currentIndex],
color: Colors.black.withOpacity(0.4),
colorBlendMode: BlendMode.srcATop,
width: 200,
height: 200,
),
),
먼저 뱃지 이미지 하단에 동일한 이미지를 넣고,
ImageFilter.blur
로 블러 효과를 추가한다.
이때 color
, colorBlendMode
로 이미지 위에 검은색 반투명 오버레이를 블렌딩해준다.
그냥 color
만 사용하면 뱃지와 그림자가 정확히 구분되고,
colorBlendMode
를 추가하면 뱃지의 색상이 그림자에 묻어 나오는 효과를 줄 수 있다.
(참고로 원형 뱃지 이미지를 사용한다면, Container
위젯의 BoxShadow
등으로 쉽게 구현할 수 있다.)
같은 기능이라도 유저 인터랙션을 추가하면 확실히 앱이 풍성하게 보인다.
직접 애니메이션을 하나씩 구현하면서 3D효과나 행렬 변환등에 대해 자세히 볼 수 있었고,
Tween 애니메이션 동작에 대해서도 이해도가 높아진 것 같다.
앞으로도 유저 인터랙션을 적절히 추가해서 유저들이 앱과 인터랙션 하는 느낌을 더 줄 수 있도록 해야겠다.
(곧 뱃지 기능이 추가 될!!!) 마인디 프로젝트 링크를 소개하며 마무리!