[μ•±κ°œλ°œ] [Flutter] ν™”νˆ¬ μΉ΄λ“œ λ’€μ§‘λŠ” μ• λ‹ˆλ©”μ΄μ…˜ λ§Œλ“€κΈ°

κ°•μ˜ μ†Œκ°œ

μ•ˆλ…•ν•˜μ„Έμš” μ½”λ“œνŒ©ν† λ¦¬μž…λ‹ˆλ‹€! μ œκ°€ μ΅œκ·Όμ— νƒ€μ§œλΌλŠ” μ˜ν™”λ₯Ό λ΄€λŠ”λ°μš” μ„―λ‹€λ₯Ό μΉ˜λŠ” 신세경이er 둜 ν™”νˆ¬ μΉ΄λ“œ λ’€μ§‘λŠ” μ• λ‹ˆλ©”μ΄μ…˜μ„ λ§Œλ“€μ–΄λ³΄λ €κ³  ν•©λ‹ˆλ‹€.

이미지 λ‹€μš΄λ‘œλ“œ

자 일단은 μ•„λž˜ 이미지듀을 λ‹€μš΄λ°›μ•„ μ£Όμ„Έμš”. μœ„λΆ€ν„° μ•„λž˜λ‘œ 각각 ν™”νˆ¬μ˜ λ’·λ©΄, 3κ΄‘, 8κ΄‘μž…λ‹ˆλ‹€.

ν™”νˆ¬ λ’·λ©΄

3κ΄‘

8κ΄‘

이 μΉ΄λ“œλ₯Ό λ Œλ”λ§ν•˜λŠ” ν•¨μˆ˜λ₯Ό λ¨Όμ € μž‘μ„± ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

  renderCard({
     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';
      }
    }
  }

μ•„μ£Ό κ°„λ‹¨ν•œ ν•¨μˆ˜μ˜ˆμš”. 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,
    );
  }

λ³„λ‘œ μ•ˆμ–΄λ ΅μ£ ?

AnimatedSwitcher μ‚¬μš©ν•˜κΈ°

그럼 AnimatedSwitcher μœ„μ ―μ„ μ‚¬μš©ν•΄μ„œ κ°„λ‹¨ν•˜κ²Œ Fade μ• λ‹ˆλ©”μ΄μ…˜μ„ λ§Œλ“€μ–΄λ³Όκ²Œμš”

  bool showFront;

  
  void initState() {
    super.initState();

    showFront = false;
  }

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

이미지λ₯Ό ν™”λ©΄μ˜ 쀑앙에 ν¬μ§€μ…˜ν•˜κ³  이미지λ₯Ό λˆ„λ₯Όλ•Œλ§ˆλ‹€ μ•žκ³Ό λ’€κ°€ λ°”λ€ŒλŠ” μ• λ‹ˆλ©”μ΄μ…˜μ„ λ„£μ—ˆμŠ΅λ‹ˆλ‹€.

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

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

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

μΆ”κ°€μ μœΌλ‘œ 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(),
              ),
            ),
          ),
        ],
      ),
    );
  }

이제 ν™”νˆ¬κ°€ 잘 λ’€μ§‘μ–΄μ§€λŠ” κ±Έ λ³Ό 수 μžˆμ–΄μš”. ν•˜μ§€λ§Œ 아직 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,
        );
      },
    );
  }

μœ„ μ½”λ“œλ₯Ό μ μš©ν•˜λ©΄ μ•„λž˜μ™€ 같은 κ²°κ³Όλ₯Ό 얻을 수 μžˆμ–΄μš”.

Perspective ν™œμš©ν•œ Flip

μ–΄λ–€κ°€μš”? 훨씬 더 μžμ—°μŠ€λŸ¬μ›Œμ‘Œμ£ ? 이제 38 광땑을 λ‚˜λž€νžˆ μœ„μΉ˜μ‹œμΌœ λ’€μ§‘λŠ” μœ„μ ―μ„ μ œμž‘ν•˜κ³  마무리 ν• κ²Œμš”.

  List<bool> showFronts;

  
  void initState() {
    super.initState();

    showFronts = [false, false];
  }

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

결과물은 μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€~

μ™„λ£Œ

Β©Code Factory All Rights Reserved