Flutter教程:实现一个仿 Flipboard 图片3D翻转动画

2019-06-2019:52:03APP与小程序开发Comments2,266 views字数 4710阅读模式

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画通过观察可以发现这个动画分为三个过程文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

  • 过程一: 底部翘起来

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

  • 过程二: 转起来

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画过程三:右边翘起来文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

三维图像投影到二维平面

图片绕着 x 轴旋转,左侧视图为旋转后投影到二位平面的图片,右侧为旋转过程中的三维视图。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画

过程一

可以把图片分成上下两部分,上半边完全没动,下半部分绕着 x 轴旋转,不断改变转动角度就可以达到过程一的效果文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画

过程二

过程二稍复杂,先看其中某一帧的情况文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画红线下半部分翘起来了,上半部分没有翘起来,所以考虑分为上下两部分绘制文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

下半部分

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画
  1. 图片绕着 z 轴旋转 20 度
  2. 裁剪图片,只取下半部分
  3. 图片绕着 x 轴旋转 45 度
  4. 图片绕着 z 轴旋转 -20 度

上半部分

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画
  1. 图片绕着 z 轴旋转 20 度
  2. 裁剪图片,只取上半部分
  3. 图片绕着 x 轴旋转 0 度(为什么?为了和其他过程统一过程,方便代码编写)
  4. 图片绕着 z 轴旋转 -20 度

拼接

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画 Flutter教程:实现一个仿 Flipboard 图片3D翻转动画把这两部分图拼接起来就是过程二中某一帧的效果文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

实现过程二的动画

保持每一帧 绕着 x 轴旋转的角度固定,改变绕着 z 轴旋转的角度就可以实现过程二的动画。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

改进过程一(方便代码编写)

过程一下半部分

  1. 图片绕着 z 轴旋转 0 度
  2. 裁剪图片,只取下半部分
  3. 图片绕着 x 轴旋转某个角度
  4. 图片绕着 z 轴旋转 0 度

不断改变 x 轴旋转的角度就可以就可以实现过程一中下半部分的动画效果文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

过程一上半部分

  1. 图片绕着 z 轴旋转 0 度
  2. 裁剪图片,只取上半部分
  3. 图片绕着 x 轴旋转 0 度
  4. 图片绕着 z 轴旋转 0 度

过程三

过程三和过程一类似,不再赘述。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

整个动画具体参数

  • 过程一:
    • 上半部分:旋转角度都是 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,标识动画当前进行到那个过程文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

enum FlipAnimationSteps { animation_step_1, animation_step_2, animation_step_3 }
复制代码

设置动画参数,监听动画状态文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

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,动画相关的主要逻辑都在里面。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

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 轴转回来)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

看一下下半部分裁剪的过程文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

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指定具体的裁剪范围。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

作者:MarioFeng
链接:https://juejin.im/post/5d0b4e65e51d45105e0212d6
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/13699.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/xcx/13699.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定