[Flutter] 뱃지 획득 애니메이션 만들기

현리·2025년 1월 20일
0

[Flutter]

목록 보기
2/2
post-thumbnail

뱃지를 획득했다!

마인디 프로젝트를 하며 뱃지 기능을 추가할 기회가 생겼다.
유저가 뱃지를 획득한 순간,
단순하게 이미지만 보여주기보단 애니메이션을 통한 시각적 요소를 더해 축하하는 의미를 더하고 싶었다.
(글 작성에 사용한 이미지는 무료 이미지를 사용함, 마인디 뱃지는 훨씬 귀여움)


애니메이션 기능을 테스트한 경험을 공유하고자 작성했지만, 더 좋은 방법이 있다면 댓글로 마구 혼내주세요.😭

어떤 애니메이션이 멋있을까..?

여러 뱃지 획득 인터랙션 중, 다음 링크의 모습을 Flutter로 구현해보고자 했다.
https://kr.pinterest.com/pin/146085581658042462/
(좀 있어보임)

만들어보자

State

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 을 추가하고,
애니메이션 컨트롤러와, 애니메이션 객체를 선언한다.

InitState

  
  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)
    • 애니메이션이 90도에서 0도로 변하도록 설정
  • CurveTween(curve: Curves.fastOutSlowIn)
    • 부드럽게 가속하고 느리게 감속하는 곡선 효과를 추가
  • weight: 3
    • 전체 시퀀스의 3/4 동안 실행되도록 설정

2.애니메이션-2

  • Tween(begin: 0.0, end: -10.0)
    • 애니메이션이 0도에서 -10도로 변하도록 설정
  • CurveTween(curve: Curves.easeOut)
    • 부드럽게 끝나는 곡선 효과를 추가
  • weight: 1
    • 전체 시퀀스의 3/4 동안 실행되도록 설정

Listener

  void _onAnimationUpdate() {
    if (_animationController.status == AnimationStatus.completed &&
        _animation.value == -10.0) {
      setState(() {
        _currentIndex = (_currentIndex + 1) % images.length;
      });
      _animationController.reset(); // 애니메이션 초기화
      _animationController.forward(); // 다시 시작
    }
  }

리스너는 애니메이션 종료 시점을 캐치하여 기능을 실행하도록 했는데

하나의 모달에 단일 이미지만 보여주기 때문에,
여러 animation controller를 사용하기보다는 하나의 컨트롤러를 사용해 애니메이션을 초기화하고, 다시 정방향 실행해주는 방식으로 사용했다.

build Widget

  
  Widget 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,
              ),
            ],
          ),
        );
      },
    );
  }

위젯에서는 크게 Transform 위젯을 사용해 회전을 구현하고,
이미지 필터를 사용해 입체적인 느낌을 살렸다.

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 참고
(왜지...?)

문서상 원근 행렬..?실제 적용한 행렬

imageFilter

여기까지 애니메이션을 추가하니 그럴듯한 뱃지 획득 인터랙션이 완성됐다.
하지만 그림자를 추가해 조금 더 입체적인 느낌을 살리고 싶었다..! (깔롱 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 애니메이션 동작에 대해서도 이해도가 높아진 것 같다.
앞으로도 유저 인터랙션을 적절히 추가해서 유저들이 앱과 인터랙션 하는 느낌을 더 줄 수 있도록 해야겠다.

(곧 뱃지 기능이 추가 될!!!) 마인디 프로젝트 링크를 소개하며 마무리!

앱 스토어 링크
구글 플레이스토어 링크



profile
프론트엔드 개발자 입니다. 최근에는 Flutter를 이용한 크로스 플랫폼 앱 개발에 관심이 많습니다.

0개의 댓글