Flutter教程:实现一个仿 Flipboard 图片3D翻转动画
通过观察可以发现这个动画分为三个过程
- 过程一: 底部翘起来
- 过程二: 转起来
过程三:右边翘起来
三维图像投影到二维平面
图片绕着 x 轴旋转,左侧视图为旋转后投影到二位平面的图片,右侧为旋转过程中的三维视图。

过程一
可以把图片分成上下两部分,上半边完全没动,下半部分绕着 x 轴旋转,不断改变转动角度就可以达到过程一的效果

过程二
过程二稍复杂,先看其中某一帧的情况
红线下半部分翘起来了,上半部分没有翘起来,所以考虑分为上下两部分绘制
下半部分

- 图片绕着 z 轴旋转 20 度
- 裁剪图片,只取下半部分
- 图片绕着 x 轴旋转 45 度
- 图片绕着 z 轴旋转 -20 度
上半部分

- 图片绕着 z 轴旋转 20 度
- 裁剪图片,只取上半部分
- 图片绕着 x 轴旋转 0 度(为什么?为了和其他过程统一过程,方便代码编写)
- 图片绕着 z 轴旋转 -20 度
拼接
把这两部分图拼接起来就是过程二中某一帧的效果
实现过程二的动画
保持每一帧 绕着 x 轴旋转的角度固定,改变绕着 z 轴旋转的角度就可以实现过程二的动画。
改进过程一(方便代码编写)
过程一下半部分
- 图片绕着 z 轴旋转 0 度
- 裁剪图片,只取下半部分
- 图片绕着 x 轴旋转某个角度
- 图片绕着 z 轴旋转 0 度
不断改变 x 轴旋转的角度就可以就可以实现过程一中下半部分的动画效果
过程一上半部分
- 图片绕着 z 轴旋转 0 度
- 裁剪图片,只取上半部分
- 图片绕着 x 轴旋转 0 度
- 图片绕着 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指定具体的裁剪范围。
作者:MarioFeng
链接:https://juejin.im/post/5d0b4e65e51d45105e0212d6
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
THE END