Code前端首页关于Code前端联系我们

Flutter教程:利用Flipboard风格的图片实现3D翻转动画

terry 2年前 (2023-09-23) 阅读数 83 #移动小程序

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画 通过观察可以看到,这个动画分为两个过程

  • 过程一:底部向上倾斜

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

  • 过程二:向上翻转

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画过程三:右侧向上倾斜

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

将三维图像投影到二维平面上

图像绕x轴旋转。左视图是旋转后投影到二维平面上的图像,右视图是旋转过程中的三维视图。 Flutter教程:实现一个仿 Flipboard 图片3D翻转动画

处理一

您可以将图像分割为顶部和底部。上半部分根本不动,下半部分绕x轴旋转。通过不断改变旋转角度,可以实现流程一Flutter教程:实现一个仿 Flipboard 图片3D翻转动画

流程二

第二个流程稍微复杂一点。我们先看单帧情况。

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画红线的下半部分向上倾斜,但上半部分则没有。因此,请考虑将其分成两部分来绘制

下半部分

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画
  1. 图像环绕 绕 z 轴旋转 20 度
  2. 裁剪图像并仅删除一半
  3. 将图像旋转 45 度乘 45 度。 -axis
  4. 将图像绕 z 轴旋转 -20 度

顶部

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画
  1. 图像绕 z 轴旋转 20 度
  2. 裁剪图像并仅拍摄上部❀图像绕 z 轴 0 度x轴(为什么?为了和其他进程统一,也为了方便写代码的处理)
  3. 图像绕z轴旋转-20度

拼接

Flutter教程:实现一个仿 Flipboard 图片3D翻转动画 这两部分图像得到流程二中特定帧的效果

实现流程二的动画

保持每一帧绕x轴旋转角度固定,通过改变左右旋转角度即可实现流程二的动画z 轴。

改进流程一(方便写代码)

流程后半部分

  1. 图像绕 z 轴旋转 0 度
  2. 裁剪图像并仅去除一半图像绕 z 轴旋转x轴一定量。角度
  3. 图像绕z轴旋转0度

不断改变x轴的旋转角度可以在图像下部产生动画效果。处理

处理顶部

  1. 图像旋转 绕 z 轴旋转 0 度
  2. 裁剪图像并仅拍摄顶部
  3. 将图像绕 z 轴旋转 0 度

处理三

处理三并过程类似,不再赘述。

整个动画具体参数

  • 流程一:
    • 上半部分:旋转角度均为0
    • 下半部分:绕z轴旋转角度始终为0,x轴旋转角度为0从0到-45度移动到
  • 过程2:
    • 上:绕z轴旋转角度从0到270度变化,绕x轴旋转角度固定为0度。绕z轴旋转角度始终为270度,绕x轴旋转角度为0-shift到45度
    • 下半部分:绕z轴旋转角度始终为270度,并且绕 x 轴旋转角度始终为 0 度

代码编写

首先定义一个枚举来标识动画当前要去的位置 流程

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

设置动画参数并监控动画状态 ' 参见核心类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布局,可以一起推断上下半部的变换结果(注:无法使用Solumn布局),两个儿童的变化是上下部分变化的结果。可以观察到,两个Transforms与之前的变换过程一致(绕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来指定特定的裁剪区域。

作者:MarioFeng
链接:https://juejin.im/post/5d0b4e65e51d45105e0212d6
来源:掘金版权所有若为商业转载请联系作者授权。非商业转载请注明出处。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门