思路參考自: 扔物線
整體效果
話不多少,直接上效果
通過觀察可以發現這個動畫分為三個過程
- 過程一: 底部翹起來
- 過程二: 轉起來
過程三:右邊翹起來
三維影象投影到二維平面
圖片繞著 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指定具體的裁剪範圍。
原始碼
原始碼點這裡 喜歡的話 star 哦