[UI/UX] 3D 애니메이션과 팝업 알림창

rO_Or·2025년 2월 22일
0

기록하기

목록 보기
4/4

먼저 우리 디자이너가 만든 귀여운 하트 편지지부터 보고 가요.


요즘 앱들을 사용해 보면 3D 애니메이션이 정말 많이 보인다.
그래서 우리 앱에도 적용시켜 보면 어떨까라는 의견이 나와서 해보기로 했다.

블렌더에서 추출된 파일 받아서 적용시키기

디자이너가 열심히 만든 .glb .gltf 확장자로 된 받아서 플러터에 적용시킬 수 있는데
여러 방법 중에 가장 간단하게 라이브러리를 사용해서 화면에 표시해 보기로 했다.

바로 flutter_3d_controller라는 라이브러리이다.

애니메이션을 시작, 정지, 처음 상태로 되돌리기나 카메라 조절 기능도 제공해준다.

Flutter3DController controller = Flutter3DController();
// ...
Flutter3DViewer(
	onLoad: (String modleAddress) {
    	// 파일이 다 불러와졌을 경우
    	controller.playAnimation();
    },
    controller: controller,
    src: 'assets/letter.glb',
);

먼저 컨트롤러를 선언한 다음
Flutter3DViewer 속성에 컨트롤러를 등록해 준다.
src 속성에 파일 경로를 작성해 준 다음
onLoad에서 컨트롤러를 통해 애니메이션을 재생시켜주면 된다.








그러면 이렇게 다양한 각도에서 애니메이션을 볼 수 있다.

아직 라이브러리를 제대로 이해한 게 아니라서 .glb 파일로 표시하는 애니메이션의 경우
최적화나 사이즈 조절 등... 하는 방법을 찾아봐야 한다.


json 파일로 애니메이션 표시하기

위의 라이브러리를 써보면서 살짝 렉이 있는 듯한 느낌이 들어서, 우리 앱에 바로 적용시키기에는 힘들어 보였다.
그래서 우리 앱에서 이미 적용시킨 json (lottie) 방식을 사용하기로 결정했다.

이것도 역시 lottie라는 라이브러리를 쓰면 간단하게 화면에 애니메이션을 표시할 수 있다.

late final Future<LottieComposition> letter;

  
  void initState() {
    super.initState();
    letter = AssetLottie('assets/lottie/letter.json').load();
    checkPopup();
  }

먼저 json 파일을 불러와야 한다.

그 다음에는 FutureBuilder를 활용해서 이 파일이 다 불러와졌을 때
화면에 나타나도록 하면 된다.

FutureBuilder
비동기 작업이 완료될 때까지 기다렸다가 결과를 UI에 반영하는 데에 주로 사용됨.
future 값에 비동기 함수를 전달하면 해당 상태에 따라 UI를 다르게 표시할 수 있다.
snapshot.connectionStateConnecitonState로 비교하면 된다.

상태설명예제
waitingfuture 작업 대기 중로딩 UI
done완료데이터 표시
active수신 중주로 StreamBuilder 사용
noneFuture가 null기본 UI 표시
//...
FutureBuilder<LottieComposition>(
	future: letter,
    builder: (context, snapshot) {
    	var lottie = snapshot.data;
        if (lottie != null) {
        	// null이 아니라면, 다 불러와진 것.
        	return Lottie(
            	// 사이즈 등 옵션 주기
            	composition: lottie,
            );
        } else {
        	return Container();
        }
    }
)

위와 같이 작성해 보면

이렇게 귀여운 하트 편지지가 화면에 나타나게 된다.


팝업 애니메이션 적용시키기

아무래도 json 파일의 로딩이 완료된 후 나타나기 때문에 화면에는 갑자기 나타나기 때문에
사용자 입장에서는 레이아웃들이 밀려서 잘못 터치할 수 있겠다, 라는 생각이 들었다.

그래서 팝업이 나타나는 애니메이션을 줘서 미리 대비(?)할 수 있게 하는 게 어떨까 싶었다.

애니메이션 위젯의 크기를 알아내기

먼저 애니메이션이 포함된 위젯의 크기가 얼마인지 알아내야했다.

우리 앱에서는 height에 값을 주는 건 최소한으로 하고 padding 활용하여 UI를 구성하고 있다.
기기마다 해상도나 DPI가 다르기 때문에 고정된 값을 사용하면 특정 기기에서 레이아웃이 깨질 가능성이 있기 때문이다.
기기에 height 크기를 렌더링 하는 것을 맡기고 우리는 padding 같이 여백만 주면
다양한 기기에서도 일관된 UI를 유지할 수가 있다.

GlobalKey를 애니메이션 위젯에 적용한 다음,
json 로딩이 끝났을 경우가 우리의 원하는 크기이므로 FutureBuilder를 활용해서
로딩이 끝났을 때의 크기를 가져오면 된다.

//...
if (lottie != null) {
	final RenderBox? containerRenderBox =
    	containerKey.currentContext?.findRenderObject() as RenderBox?;

    if (containerRenderBox != null) {
    	if (!isClose) {
        	containerSize = containerRenderBox.size.height;
        }
                 
    }
}

슬라이드 애니메이션 적용시키기

AnimatedContainer를 사용해서 height 값에 변화를 주면
아래로 밀리는 느낌의 애니메이션을 줄 수가 있다.

AnimatedContainer(
	height: containerSize,
    duration: const Duration(milliseconds: 300),
    onEnd: () {
    	// 밀리는 애니메이션이 끝나면 팝업 표시
    	if (isClose) return;
        setState(() {
        	isLoading = false;
        });
        doPopup();
    },
),

팝업 애니메이션 적용시키기

저번에 활용했던 mixin 클래스를 써서 애니메이션을 분리시킨 뒤
애니메이션 먼저 만들어줬다.

mixin PopupAnimationMixin<T extends StatefulWidget>
    on State<T>, SingleTickerProviderStateMixin<T> {
  late AnimationController popupController;
  late Animation<double> popupAnimation;

  
  void initState() {
    super.initState();

    popupController = AnimationController(
      vsync: this,
      duration: const Duration(
        milliseconds: 300,
      ),
    );

    popupAnimation = Tween(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: popupController,
        curve: Curves.fastLinearToSlowEaseIn,
      ),
    );
  }

  
  void dispose() {
    popupController.dispose();
    super.dispose();
  }

  void doPopup() async {
    popupController.forward();
  }
}

또, 저번에 활용했던 AnimatedBuilder를 사용하여
Transform.scaleOpacitiy 위젯을 쓰면 페이드인이 되면서 점점 커지는 애니메이션을 구현할 수 있다.

AnimatedBuilder(
	animation: popUpAnimation,
    builder: (context, child) {
    	return isLoading
        	? Container()
            : Opacitiy(
            	opacity: popUpanimation.value,
                child: Transform.scale(
                	scale: popupAnimation.value,
                    child: child,
                ),
            );
       child: // ...
    }
)

🤔 isLoading을 쓴 이유?

Transform.scale 같은 경우에는 value 값이 (0,0)이더라도 크기를 이미 차지하고 있기 때문에
위에서 적용시켰던 슬라이드 애니메이션이 보이지 않게 된다.
이미 공간을 차지하고 있기 때문에 애니메이션이 진행되고 있는 게 가려져서 보이지 않는 것.

그래서 isLoading으로 로딩 중일 때는 빈 컨테이너를 보여주고
로딩이 끝나면 팝업 위젯을 보여주는 식으로 했다.


profile
즐거워지고 싶다.

0개의 댓글