플러터로 화투 카드 뒤집는 애니메이션 만들기

Written by 코드팩토리 JC

1월 15, 2024

플러터 화투 뒤집는 애니메이션 만들기

강의 소개

안녕하세요 코드팩토리입니다! 제가 최근에 타짜라는 영화를 봤는데요 섯다를 치는 신세경이er 로 화투 카드 뒤집는 애니메이션을 만들어보려고 합니다.

이미지 다운로드

자 일단은 아래 이미지들을 다운받아 주세요. 위부터 아래로 각각 화투의 뒷면, 3광, 8광입니다.

화투 뒷면

3광

8광

이 카드를 렌더링하는 함수를 먼저 작성 해보겠습니다.

  renderCard({
    @required Key key,
    bool isBack = true,
    bool isThree = true,
  }) {
    assert(key != null);

    String basePath = 'assets/card_flip/';

    if (isBack) {
      basePath += 'back.jpg';
    } else {
      if (isThree) {
        basePath += '3.jpg';
      } else {
        basePath += '8.jpg';
      }
    }
  }
Dart

아주 간단한 함수예요. ValueKey 를 받아서 Container 에 인젝트 해주고 (애니메이션을 할때 같은 위젯을 사용할경우 위젯간 구분을 하기위해 key 값이 꼭 필요합니다) isBack 과 isThree 파라미터를 사용해서 특정 이미지를 불러내고 있어요.

이미지 렌더링하기

다음은 renderBack 과 renderFront 함수를 작성해서 38 광땡의 앞 이미지 또는 뒤 이미지를 위젯화 해보겠습니다.

  renderFront({
    bool isThree = true,
  }) {
    return renderCard(
      key: ValueKey(isThree ? 3 : 2),
      isBack: false,
      isThree: isThree,
    );
  }

  renderBack() {
    return renderCard(
      key: ValueKey(false),
      isBack: true,
    );
  }
Dart

별로 안어렵죠?

AnimatedSwitcher 사용하기

그럼 AnimatedSwitcher 위젯을 사용해서 간단하게 Fade 애니메이션을 만들어볼게요

  bool showFront;

  @override
  void initState() {
    super.initState();

    showFront = false;
  }

  @override
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: GestureDetector(
              onTap: () {
                setState(() {
                  showFront = !showFront;
                });
              },
              child: AnimatedSwitcher(
                duration: Duration(milliseconds: 300),
                child: showFront ? renderFront() : renderBack(),
              ),
            ),
          ),
        ],
      ),
    );
  }
Dart

이미지를 화면의 중앙에 포지션하고 이미지를 누를때마다 앞과 뒤가 바뀌는 애니메이션을 넣었습니다.

Fade 애니메이션

여기까진 별로 안어렵죠? 그런데 애니메이션이 저희가 원하는 애니메이션이 아니예요. Fade 애니메이션이 아니고 앞면이 뒷면으로 뒤집히고 뒷면이 앞면으로 뒤집히는 애니메이션을 만들고 싶어요.

AnimatedBuilder 로 뒤집는 애니메이션 만들기

AnimatedSwitcher 의 transitionBuilder 파라미터를 이용하면 쉽게 Flip 애니메이션을 만들 수 있어요.

  Widget wrapAnimatedBuilder(Widget widget, Animation<double> animation){
    final rotate = Tween(begin: pi, end: 0.0).animate(animation);

    return AnimatedBuilder(
      animation: rotate,
      child: widget,
      builder: (_, widget){
        return Transform(
          transform: Matrix4.rotationY(rotate.value),
          child: widget,
          alignment: Alignment.center,
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: GestureDetector(
              onTap: () {
                setState(() {
                  showFront = !showFront;
                });
              },
              child: AnimatedSwitcher(
                transitionBuilder: wrapAnimatedBuilder,
                duration: Duration(milliseconds: 300),
                child: showFront ? renderFront() : renderBack(),
              ),
            ),
          ),
        ],
      ),
    );
  }
Dart

wrapAnimatedBuilder 함수를 작성하고 AnimatedSwitcher 의 transitionBuilder 파라미터에 넣어줬어요.

이제 아래와 같이 Flip 애니메이션이 작동해요. 어느정도는 진짜 카드를 뒤지븐것 같은 느낌이 나긴 하네요.

Flip 애니메이션

하지만 아직 부족합니다. 뭔가 부자연스럽게 카드가 다 뒤집히기 전에 이미지가 변경되는걸 볼 수 있어요. 이걸 한번 해결 해볼게요.

앞면은 반만 돌리기

앞면이 반 이상 돌아가면 뒷면 이미지로 위젯은 변경을 해야합니다. 그래야 자연스럽게 앞면이 뒷면으로 바뀌는 애니메이션을 작성할 수 있어요. 그러기 위해선 이미지를 탭 했을때 현재 뒷면이 어떤 이미지인지 파악을 해야돼요. 저희가 설정해놓은 key 파라미터를 이용하면 앞면과 뒷면을 아주 쉽게 구분할 수 있습니다.

  Widget wrapAnimatedBuilder(Widget widget, Animation<double> animation) {
    final rotate = Tween(begin: pi, end: 0.0).animate(animation);

    return AnimatedBuilder(
      animation: rotate,
      child: widget,
      builder: (_, widget) {
        final isBack = showFront
            ? widget.key == ValueKey(false)
            : widget.key != ValueKey(false);

        final value = isBack ? min(rotate.value, pi / 2) : rotate.value;

        return Transform(
          transform: Matrix4.rotationY(value),
          child: widget,
          alignment: Alignment.center,
        );
      },
    );
  }
Dart

추가적으로 layoutBuilder 를 아래와 같이 고쳐주세요. AnimatedSwitcher 는 상태변경을 받는 순간 바로 위젯을 변경하는데 저희는 자연스럽게 첫번째 위젯이 두번째 위젯으로 변경되는걸 원하기 때문에 현재 위젯을 다시 최상위로 올려주는 작업을 해야합니다.

  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: GestureDetector(
              onTap: () {
                setState(() {
                  showFront = !showFront;
                });
              },
              child: AnimatedSwitcher(
                transitionBuilder: wrapAnimatedBuilder,
                layoutBuilder: (widget, list) {
                  return Stack(
                    children: [widget, ...list],
                  );
                },
                duration: Duration(milliseconds: 1000),
                child: showFront ? renderFront() : renderBack(),
              ),
            ),
          ),
        ],
      ),
    );
  }
Dart

이제 화투가 잘 뒤집어지는 걸 볼 수 있어요. 하지만 아직 1프로 부족해요. 정말 자연스럽게 뒤집으려면 시각적 효과를 첨가할 필요가 있어요.

Perspective 활용하기

Flutter Perspective Matrix 에 대해서는 여기 에서 더 자세히 알아볼 수 있어요. 이번에는 wrapAnimationBuilder 에 perspective 를 추가해서 멀리있는 부분이 더 짧고 가까이 있는부분이 더 길게 보이게 효과를 줘서 자연스럽게 카드가 뒤집히는 효과를 구현 해볼게요.

  Widget wrapAnimatedBuilder(Widget widget, Animation<double> animation) {
    final rotate = Tween(begin: pi, end: 0.0).animate(animation);

    return AnimatedBuilder(
      animation: rotate,
      child: widget,
      builder: (_, widget) {
        final isBack = showFront
            ? widget.key == ValueKey(false)
            : widget.key != ValueKey(false);

        final value = isBack ? min(rotate.value, pi / 2) : rotate.value;

        var tilt = ((animation.value - 0.5).abs() - 0.5) * 0.0025;

        tilt *= isBack ? -1.0 : 1.0;

        return Transform(
          transform: Matrix4.rotationY(value)..setEntry(3, 0, tilt),
          child: widget,
          alignment: Alignment.center,
        );
      },
    );
  }
Dart

위 코드를 적용하면 아래와 같은 결과를 얻을 수 있어요.

Perspective 활용한 Flip

어떤가요? 훨씬 더 자연스러워졌죠? 이제 38 광땡을 나란히 위치시켜 뒤집는 위젯을 제작하고 마무리 할게요.

  List<bool> showFronts;

  @override
  void initState() {
    super.initState();

    showFronts = [false, false];
  }

  @override
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              GestureDetector(
                onTap: () {
                  setState(() {
                    showFronts = [!showFronts[0], showFronts[1]];
                  });
                },
                child: AnimatedSwitcher(
                  transitionBuilder: (Widget widget, Animation<double> animation){
                    return wrapAnimatedBuilder(widget, animation, showFronts[0]);
                  },
                  layoutBuilder: (widget, list) {
                    return Stack(
                      children: [widget, ...list],
                    );
                  },
                  duration: Duration(milliseconds: 1000),
                  child: showFronts[0] ? renderFront() : renderBack(),
                ),
              ),
              GestureDetector(
                onTap: () {
                  setState(() {
                    showFronts = [showFronts[0], !showFronts[1]];
                  });
                },
                child: AnimatedSwitcher(
                  transitionBuilder: (Widget widget, Animation<double> animation){
                    return wrapAnimatedBuilder(widget, animation, showFronts[1]);
                  },
                  layoutBuilder: (widget, list) {
                    return Stack(
                      children: [widget, ...list],
                    );
                  },
                  duration: Duration(milliseconds: 1000),
                  child: showFronts[1] ? renderFront(
                    isThree: false,
                  ) : renderBack(),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
Dart

결과물은 아래와 같습니다~

완료

관련 포스트

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

서론 불변 프로그래밍: 현대 개발의 핵심 현대 소프트웨어 개발에서 불변 프로그래밍(Immutable Programming)의 중요성은 간과할 수 없는 요소입니다. 플러터(Flutter)에서도 마찬가지로 불변 프로그래밍 개념이 매우 중요하며, copyWith 함수는 이러한 불변성을 유지하는 데 핵심적인 역할을 합니다. 이 글에서는 플러터를 배우기 시작하는 개발자들에게 불변 프로그래밍의 중요성을 강조하고, copyWith 함수의 역할과 사용 방법에 대해 설명 해보겠습니다!...

ChatGPT가 이야기하는 2024년 개발자 로드맵

ChatGPT가 이야기하는 2024년 개발자 로드맵

서론 개발자의 여정을 시작하며 안녕하세요, 미래의 개발자 여러분! 오늘부터 시작하는 여러분의 개발 여정에 함께할 수 있어서 기쁩니다. 2023년은 기술이 매우 빠르게 변화하는 해였으며, 이러한 변화 속에서 개발자가 되기 위한 길은 더욱 다채롭고 흥미로워졌습니다. 이 로드맵은 초보자인 여러분이 개발의 세계에 첫발을 내딛는 데 필요한 기초부터 시작해, 점차 심화 단계로 나아가는 길을 안내해 드릴 것입니다. 백엔드 개발 이 글은 단순히 기술을 배우는 것 이상의 의미를 가집니다....

Flutter Freezed 플러그인! Entity Code Generation은 이거 하나로 끝!

Flutter Freezed 플러그인! Entity Code Generation은 이거 하나로 끝!

https://youtu.be/i5p6wXLAX7I 서론 Flutter 는 Code Generation 기능이 상당히 많이 활성화되어 있어요. 흔히들 많이 사용하는 json_serializable 라이브러리도 있고 retrofit 및 chopper 라이브러리도 있습니다. 오늘 알려드릴 freezed 또한 데이터 클래스에 편의 기능들을 제공해주는 code generation 라이브러리입니다. Freezed vs Json Serializable Code Generation 이라는...