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
- The picture is rotated 20 degrees around the Z axis
- Crop the picture, removing only the bottom half
- The picture is rotated 45 degrees about the X-axis
- The image is rotated -20 degrees around the Z-axis
The upper part
- The picture is rotated 20 degrees around the Z axis
- Crop the picture, taking only the top half
- The picture is rotated 0 degrees about the X-axis (why? In order to unify the process with other processes, easy to code)
- 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
- The picture is rotated 0 degrees about the z axis
- Crop the picture, removing only the bottom half
- The picture is rotated at some Angle about the x axis
- 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
- The picture is rotated 0 degrees about the z axis
- Crop the picture, taking only the top half
- The picture is rotated 0 degrees about the X-axis
- 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