Flutter:手把手教你實現一個仿 Flipboard 圖片3D翻轉動畫

MarioFeng發表於2019-06-20

思路參考自: 扔物線

整體效果

話不多少,直接上效果

Flutter:手把手教你實現一個仿 Flipboard 圖片3D翻轉動畫

通過觀察可以發現這個動畫分為三個過程

  • 過程一: 底部翹起來
Flutter:手把手教你實現一個仿 Flipboard 圖片3D翻轉動畫
  • 過程二: 轉起來
Flutter:手把手教你實現一個仿 Flipboard 圖片3D翻轉動畫

過程三:右邊翹起來

Flutter:手把手教你實現一個仿 Flipboard 圖片3D翻轉動畫

三維影象投影到二維平面

圖片繞著 x 軸旋轉,左側檢視為旋轉後投影到二位平面的圖片,右側為旋轉過程中的三維檢視。

在這裡插入圖片描述

過程一

可以把圖片分成上下兩部分,上半邊完全沒動,下半部分繞著 x 軸旋轉,不斷改變轉動角度就可以達到過程一的效果

在這裡插入圖片描述

過程二

過程二稍複雜,先看其中某一幀的情況

Flutter:手把手教你實現一個仿 Flipboard 圖片3D翻轉動畫

紅線下半部分翹起來了,上半部分沒有翹起來,所以考慮分為上下兩部分繪製

下半部分

在這裡插入圖片描述

  1. 圖片繞著 z 軸旋轉 20 度
  2. 裁剪圖片,只取下半部分
  3. 圖片繞著 x 軸旋轉 45 度
  4. 圖片繞著 z 軸旋轉 -20 度

上半部分

在這裡插入圖片描述

  1. 圖片繞著 z 軸旋轉 20 度
  2. 裁剪圖片,只取上半部分
  3. 圖片繞著 x 軸旋轉 0 度(為什麼?為了和其他過程統一過程,方便程式碼編寫)
  4. 圖片繞著 z 軸旋轉 -20 度

拼接

Flutter:手把手教你實現一個仿 Flipboard 圖片3D翻轉動畫 Flutter:手把手教你實現一個仿 Flipboard 圖片3D翻轉動畫

把這兩部分圖拼接起來就是過程二中某一幀的效果

實現過程二的動畫

保持每一幀 繞著 x 軸旋轉的角度固定,改變繞著 z 軸旋轉的角度就可以實現過程二的動畫。

改進過程一(方便程式碼編寫)

過程一下半部分

  1. 圖片繞著 z 軸旋轉 0 度
  2. 裁剪圖片,只取下半部分
  3. 圖片繞著 x 軸旋轉某個角度
  4. 圖片繞著 z 軸旋轉 0 度

不斷改變 x 軸旋轉的角度就可以就可以實現過程一中下半部分的動畫效果

過程一上半部分

  1. 圖片繞著 z 軸旋轉 0 度
  2. 裁剪圖片,只取上半部分
  3. 圖片繞著 x 軸旋轉 0 度
  4. 圖片繞著 z 軸旋轉 0 度

過程三

過程三和過程一類似,不再贅述。

整個動畫具體引數

  • 過程一:

    • 上半部分:旋轉角度都是 0
    • 下半部分:繞 z 軸旋轉角度始終為 0,繞 x 軸旋轉角度從 0 過渡到 -45 度
  • 過程二:

    • 上半部分:繞著 z 軸旋轉角度從 0 過渡到270 度,繞著 x 軸旋轉的角度固定為 0 度
    • 下半部分:繞著 z 軸旋轉角度從 0 過渡到270 度,繞著 x 軸旋轉的角度固定為 -45 度
  • 過程三

    • 上半部分:繞 z 軸旋轉角度始終為 270 度,繞 x 軸旋轉角度從 0 過渡到 45 度
    • 下半部分:繞 z 軸旋轉角度始終為 270 度,繞 x 軸旋轉角度始終為 0 度

程式碼編寫

首先定義一個enum,標識動畫當前進行到那個過程

enum FlipAnimationSteps { animation_step_1, animation_step_2, animation_step_3 }
複製程式碼

設定動畫引數,監聽動畫狀態

class _FlipAnimationApp extends State<FlipAnimationApp>
    with SingleTickerProviderStateMixin {
  var imageWidget = Image.asset(
    'images/mario.jpg',
    width: 300.0,
    height: 300.0,
  );

  AnimationController controller;

  CurvedAnimation animation;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 1), vsync: this);
    animation = CurvedAnimation(
      parent: controller,
      curve: Curves.easeInOut,
    )..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          switch (currentFlipAnimationStep) {
            case FlipAnimationSteps.animation_step_1:
              currentFlipAnimationStep = FlipAnimationSteps.animation_step_2;
              controller.reset();
              controller.forward();
              break;

            case FlipAnimationSteps.animation_step_2:
              currentFlipAnimationStep = FlipAnimationSteps.animation_step_3;
              controller.reset();
              controller.forward();
              break;
            case FlipAnimationSteps.animation_step_3:
              break;
          }
        }
      });

    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimateFlipWidget(
      animation: animation,
      child: imageWidget,
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

複製程式碼

再來看看核心類AnimateFlipWidget,動畫相關的主要邏輯都在裡面。

class AnimateFlipWidget extends AnimatedWidget {
  final Widget child;

  double _currentTopRotationXRadian = 0;
  double _currentBottomRotationXRadian = 0;
  double _currentRotationZRadian = 0;

  static final _topRotationXRadianTween =
      Tween<double>(begin: 0, end: math.pi / 4);
  static final _bottomRotationXRadianTween =
      Tween<double>(begin: 0, end: -math.pi / 4);
  static final _rotationZRadianTween =
      Tween<double>(begin: 0, end: (1 + 1 / 2) * math.pi);

  AnimateFlipWidget({Key key, Animation<double> animation, this.child})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;

    return Center(
      child: Container(
        child: Stack(
          children: [
            Transform(
              alignment: Alignment.center,
              transform: Matrix4.rotationZ(currentFlipAnimationStep ==
                      FlipAnimationSteps.animation_step_2
                  ? _rotationZRadianTween.evaluate(animation) * -1
                  : _currentRotationZRadian * -1),
              child: Transform(
                transform: Matrix4.identity()
                  ..setEntry(3, 2, 0.002)
                  ..rotateX(currentFlipAnimationStep ==
                          FlipAnimationSteps.animation_step_3
                      ? _currentTopRotationXRadian =
                          _topRotationXRadianTween.evaluate(animation)
                      : _currentTopRotationXRadian),
                alignment: Alignment.center,
                child: ClipRect(
                  clipper: _TopClipper(context),
                  child: Transform(
                    alignment: Alignment.center,
                    transform: Matrix4.rotationZ(currentFlipAnimationStep ==
                            FlipAnimationSteps.animation_step_2
                        ? _currentRotationZRadian =
                            _rotationZRadianTween.evaluate(animation)
                        : _currentRotationZRadian),
                    child: child,
                  ),
                ),
              ),
            ),
            Transform(
              alignment: Alignment.center,
              transform: Matrix4.rotationZ(currentFlipAnimationStep ==
                      FlipAnimationSteps.animation_step_2
                  ? _rotationZRadianTween.evaluate(animation) * -1
                  : _currentRotationZRadian * -1),
              child: Transform(
                transform: Matrix4.identity()
                  ..setEntry(3, 2, 0.002)
                  ..rotateX(currentFlipAnimationStep ==
                          FlipAnimationSteps.animation_step_1
                      ? _currentBottomRotationXRadian =
                          _bottomRotationXRadianTween.evaluate(animation)
                      : _currentBottomRotationXRadian),
                alignment: Alignment.center,
                child: ClipRect(
                  clipper: _BottomClipper(context),
                  child: Transform(
                    alignment: Alignment.center,
                    transform: Matrix4.rotationZ(currentFlipAnimationStep ==
                            FlipAnimationSteps.animation_step_2
                        ? _currentRotationZRadian =
                            _rotationZRadianTween.evaluate(animation)
                        : _currentRotationZRadian),
                    child: child,
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

複製程式碼

這個類返回了一個 Stack 佈局,可以把上半部分和下半部分的變換結果疊加在一起(注意:不能用Column佈局哦),children裡面的兩個Transform就是上下兩部分變化之後的結果。可以發現兩個Transform都是符合前面的變換流程(繞 Z 軸旋轉 - > 裁剪 -> 繞 X 軸旋轉 -> 繞 Z 軸轉回來)。

看一下下半部分裁剪的過程

class _BottomClipper extends CustomClipper<Rect> {
  final BuildContext context;

  _BottomClipper(this.context);

  @override
  Rect getClip(Size size) {
    return new Rect.fromLTRB(
        -size.width, size.height / 2, size.width * 2, size.height * 2);
  }

  @override
  bool shouldReclip(CustomClipper<Rect> oldClipper) {
    return true;
  }
}
複製程式碼

定義一個類,繼承CustomClipper類,重寫getClip指定具體的裁剪範圍。

原始碼

原始碼點這裡 喜歡的話 star 哦

相關文章