The overall effect

Very few words, direct effect

It can be observed that this animation is divided into three processes

  • Process 1: Bottom up

  • Process two: Spin

Process 3: Right side up

A three-dimensional image is projected onto a two-dimensional plane

The image is rotated around the X-axis. The left view is the image projected to the two-position plane after rotation, and the right view is the 3D view during rotation.

Process a

You can divide the image into two parts, the top half is completely unchanged, and the bottom half rotates around the X-axis, constantly changing the rotation Angle to achieve the effect of process 1

Process 2

Process two is a little bit more complicated, so let’s look at one of the frames

The lower part of the red line is warped, but the upper part is not, so consider drawing it in two parts

The second part of

  1. The picture is rotated 20 degrees around the Z axis
  2. Crop the picture, removing only the bottom half
  3. The picture is rotated 45 degrees about the X-axis
  4. The image is rotated -20 degrees around the Z-axis

The upper part

  1. The picture is rotated 20 degrees around the Z axis
  2. Crop the picture, taking only the top half
  3. The picture is rotated 0 degrees about the X-axis (why? In order to unify the process with other processes, easy to code)
  4. The image is rotated -20 degrees around the Z-axis

Joining together


Joining these two parts together is the effect of one frame in process two

Implement the animation of procedure two

Keep the rotation Angle of each frame around the X axis fixed, and change the rotation Angle around the Z axis to realize the animation of process 2.

Improvement Process 1 (easy to code)

The second half of the process

  1. The picture is rotated 0 degrees about the z axis
  2. Crop the picture, removing only the bottom half
  3. The picture is rotated at some Angle about the x axis
  4. The picture is rotated 0 degrees about the z axis

Changing the rotation Angle of the x axis can achieve the animation effect of the second half of the process

The first half of the process

  1. The picture is rotated 0 degrees about the z axis
  2. Crop the picture, taking only the top half
  3. The picture is rotated 0 degrees about the X-axis
  4. The picture is rotated 0 degrees about the z axis

The process of three

Procedure three is similar to procedure one.

Specific parameters for the entire animation

  • A process:

    • Top half: Rotation Angle is 0
    • The lower part: The rotation Angle about z axis is always 0, and the rotation Angle about X axis transitions from 0 to -45 degrees
  • Process 2:

    • Upper part: The rotation Angle changes from 0 to 270 degrees about the Z axis, and the rotation Angle is fixed at 0 degrees about the X axis
    • Lower part: The rotation Angle changes from 0 to 270 degrees about the Z axis, and the rotation Angle is fixed at -45 degrees about the X axis
  • The process of three

    • Upper part: The rotation Angle about z axis is always 270 degrees, and the rotation Angle about X axis transitions from 0 to 45 degrees
    • Lower part: The rotation Angle about z axis is always 270 degrees, and the rotation Angle about X axis is always 0 degrees

The code

We first define an enum that identifies the current stage of the animation

enum FlipAnimationSteps { animation_step_1, animation_step_2, animation_step_3 }
Copy the code

Set animation parameters and monitor animation state

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(a) {
    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(a) {
    controller.dispose();
    super.dispose(); }}Copy the code

Take a look at the core class AnimateFlipWidget, where most of the animation-related logic is found.

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, ), ), ), ), ], ), ), ); }}Copy the code

This class returns a Stack layout that adds the top and bottom transformations together. The two transforms in children are the result of the top and bottom transformations. It can be found that both transforms follow the previous transformation flow (rotate around Z axis -> crop -> rotate around X axis -> rotate back around Z axis).

Take a look at the process of tailoring the bottom half

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; }}Copy the code

Define a class that inherits the CustomClipper class and overrides getClip to specify the specific clipping scope.

The source code

Source point here like words star oh