一、前言
之前在我的一篇文章 iOS复杂动画之抽丝剥茧 中有分析过一个复杂的 Loading 动画的实现过程,现在有意看了下 Flutter 的动画,所以就有了一个用 Flutter 来实现这个动画当练习的想法,最终使用 Flutter 实现效果如下
下面我们就来重新分析下这个动画的实现过程
二、动画的步骤分析
上面图中的动画第一眼看起来的确是有点复杂,但是我们来一步步分析,就会发现其实并不是那么难。仔细看一下就会发现,大致步骤如下:
1、先出来一个圆
2、圆形在水平和竖直方向上被挤压,呈椭圆形状的一个过程,最后恢复成圆形
3、圆形的左下角、右下角和顶部分别按顺序凸出一小部分(内部三角形拉伸)
4、圆和凸出部分形成的图形旋转一圈后变成三角形(三角形不变,圆缩小)
5、三角形的左边先后出来两个画矩形边框的动画,将三角形围在矩形中
6、矩形由底部向上被波浪状填满
7、被填满的矩形放大至全屏,弹出 Welcome
大致步骤如上,下面我们就来一步步实现每个步骤。
三、抽丝剥茧
1.分析
因为在 Flutter 中,万物皆 widget,所以首先根据我们的分析,我们大概需要以下几个 widget
- 圆形
- 三角,
- 两个矩形边框
- 水波
-
Text
文本
首先我们需要创建一个动画的控制器,然后依次是动画三要素
- AnimationController
- CurvedAnimation
- Tween
2.实现圆形变化
圆的变化过程大致如下(w -> width, h -> height):
-
(h = 0, w = 0)
此时无圆形 -
(h = 120, w = 120)
圆形由小变大 -
(h = 120, w = 130)->(h = 120, w = 120)->(h = 130, w = 120)->(h = 120, w = 120)
圆形变成椭圆的过程 -
(h = 0, w = 0)
圆形消失
上述过程就是圆在整个动画周期内的变化过程,所以我们用TweenSequence
来实现每个时间段内的时间比重
// 最开始圆的宽度变化
static TweenSequence circleWidthTweenSequence = TweenSequence([
TweenSequenceItem(tween: Tween(begin: 0.0, end: 120.0), weight: 5),
TweenSequenceItem(tween: Tween(begin: 120.0, end: 130.0), weight: 10),
TweenSequenceItem(tween: Tween(begin: 130.0, end: 120.0), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(120.0), weight: 20),
TweenSequenceItem(tween: Tween(begin: 120.0, end: 0.0), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 45),
]);
//最开始圆的高度变化
static TweenSequence circleHeightTweenSequence = TweenSequence([
TweenSequenceItem(tween: Tween(begin: 0.0, end: 120.0), weight: 5),
TweenSequenceItem(tween: ConstantTween<double>(120.0), weight: 20),
TweenSequenceItem(tween: Tween(begin: 120.0, end: 130.0), weight: 10),
TweenSequenceItem(tween: Tween(begin: 130.0, end: 120.0), weight: 10),
TweenSequenceItem(tween: Tween(begin: 120.0, end: 0.0), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 45),
]);
这样基于上面,就可以做出来圆的整个生命周期的动画
...
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(60),
child: Container(
color: Colors.purple,
height: _circleHeightTween.value,
width: _circleWidthTween.value,
),
),
);
},
);
}
效果如下
3.实现三角形变化
三角形的变化过程其实很简单,主要是以下几步
- 三角形从 0 到 大
- 三角形分别左边、右边、上边三个角拉长
- 旋转
知道了三角形的变化过程,首先我们需要绘制出来一个三角形,由于我们并没有三角形这种 widget,所以就需要我们手动去实现。其实在 Flutter 中实现各种复杂的图形也很简单,Flutter 为我们提供了一个 CustomPainter
抽象类,我们只要继承然后实现 paint
和 shouldRepaint
这两个抽象方法即可,自定义三角形实现如下
class TrianglePainter extends CustomPainter {
Color color;
Paint _paint = Paint()
..strokeWidth = 5.0
..color = Colors.purple
..isAntiAlias = true
..strokeJoin = StrokeJoin.round;
Path _path = Path();
double left, right, top;
TrianglePainter({this.left, this.right, this.top});
@override
void paint(Canvas canvas, Size size) {
final _width = size.width;
final _height = size.height;
_path.moveTo(left * _width, 0.85 * _height);
_path.lineTo(right * _width, 0.85 * _height);
_path.lineTo(0.5 * _width, top * _height);
_path.close();
canvas.drawPath(_path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
然后三角形变化的 Tween 如下
// 三角形size变化
static TweenSequence triangleSizeTweenSequence = TweenSequence([
TweenSequenceItem(tween: Tween(begin: 0.0, end: 120.0), weight: 15),
TweenSequenceItem(tween: ConstantTween<double>(120.0), weight: 85),
]);
// 三角形的左、右、上变化过程
static TweenSequence triangleLeftTweenSequence = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(0.2), weight: 15),
TweenSequenceItem(tween: Tween(begin: 0.2, end: 0.02), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(0.02), weight: 75),
]);
static TweenSequence triangleRightTweenSequence = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(0.8), weight: 25),
TweenSequenceItem(tween: Tween(begin: 0.8, end: 0.98), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(0.98), weight: 65),
]);
static TweenSequence triangleTopTweenSequence = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(0.05), weight: 35),
TweenSequenceItem(tween: Tween(begin: 0.05, end: -0.1), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(-0.1), weight: 55),
]);
// 整体旋转的变化过程
static TweenSequence rotationTweenSequence = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 45),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 2.0), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(2.0), weight: 45),
]);
最后拿到 Tween 值去渲染动画
...
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return Center(
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationZ(Math.pi * _rotationTween.value),
child: Container(
height: _triangleSizeTween.value,
width: _triangleSizeTween.value,
child: CustomPaint(
painter: TrianglePainter(
left: _triangleLeftTween.value,
right: _triangleRightTween.value,
top: _triangleTopTween.value,
),
),
),
),
);
},
);
}
最后三角形动画变化效果如下
4.实现矩形框变化
同样,矩形框的变化我们也得使用 CustomPainter
class SquarePainter extends CustomPainter {
double progress;
Color color;
final Paint _paint = Paint()
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 5;
SquarePainter({this.progress, this.color = Colors.purple});
@override
void paint(Canvas canvas, Size size) {
_paint.color = color;
if (progress > 0) {
var path = createPath(4, size.width);
PathMetric pathMetric = path.computeMetrics().first;
Path extractPath =
pathMetric.extractPath(0.0, pathMetric.length * progress);
canvas.drawPath(extractPath, _paint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
Path createPath(int sides, double radius) {
Path path = Path();
// 根据三角形的系数画矩形
double wFartor = 0.02; //左下
double hFactor = 0.85; //右下
double tFactor = 0.10; //顶部三角形
path.moveTo(wFartor * radius, hFactor * radius);
for (int i = 1; i <= sides; i++) {
double x, y;
if (i == 1) {
x = wFartor * radius;
y = -tFactor * radius;
} else if (i == 2) {
x = radius;
y = -tFactor * radius;
} else if (i == 3) {
x = radius;
y = radius * hFactor;
} else {
x = wFartor * radius;
y = radius * hFactor;
}
path.lineTo(x, y);
}
path.close();
return path;
}
}
需要注意的是,我们需要用 PathMetric
来拿到路径, 类似于 Android 中的 PathMeasure.getSegment()
, 两个线性矩形的变化 Tween 如下
// 线性矩形变化
static TweenSequence rectTweenSequence1 = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 55),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 10),
TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.0), weight: 35),
]);
static TweenSequence rectTweenSequence2 = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 65),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(1.0), weight: 25),
]);
由于是两个矩形的变化,所以我们使用 Stack
包裹
...
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return Center(
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationZ(Math.pi * _rotationTween.value),
child: Stack(
alignment: Alignment.center,
children: [
Container(
height: _triangleSizeTween.value,
width: _triangleSizeTween.value,
child: CustomPaint(
painter: SquarePainter(progress: _rect1Tween.value),
),
),
Container(
height: _triangleSizeTween.value,
width: _triangleSizeTween.value,
child: CustomPaint(
painter: SquarePainter(
progress: _rect2Tween.value,
color: Color(0xff40e0b0),
),
),
)
],
),
),
);
},
);
}
最后实现效果如下
5.实现水波变化以及放大效果
实现水波变化稍微复杂一点,因为我们整个过程所有的动画都是用一个 AnimationController
来控制,所以我们还需要一个 Animation
来控制水波震荡的效果,但是我们的 _HWAnimatePageState
是继承于 SingleTickerProviderStateMixin
的,里面只能有一个`AnimationController。基于这种情况,将水波动画抽成了一个单独的 widget,可以在 自定义wave_progress 看到源码,画水波代码如下
// 画水波纹动画
Paint wavePaint = new Paint()..color = waveColor;
// 水波振幅
double amp = 2.0;
double p = progress / 100.0;
double baseHeight = (1 - p) * size.height;
Path path = Path();
path.moveTo(0.0, baseHeight);
for (double i = 0.0; i < size.width; i++) {
path.lineTo(
i,
baseHeight +
Math.sin((i / size.width * 2 * Math.pi) +
(animation.value * 2 * Math.pi)) * amp - 15);
}
path.lineTo(size.width, size.height - 15);
path.lineTo(0.0, size.height - 15);
path.close();
canvas.drawPath(path, wavePaint);
水波升高变大,然后显示文本的 Tween 过程如下
// 水波升高变化动画
static TweenSequence waveTweenSequence = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 75),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(1.0), weight: 15),
]);
// 水波宽高变化
static TweenSequence waveWidthTweenSequence = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(120.0), weight: 80),
TweenSequenceItem(tween: Tween(begin: 120.0, end: screenWidth), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(screenWidth), weight: 10),
]);
static TweenSequence waveHeightTweenSequence = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(120.0), weight: 80),
TweenSequenceItem(
tween: Tween(begin: 120.0, end: screenHeight), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(screenHeight), weight: 10),
]);
// 最后显示的文本变化过程
static TweenSequence textSizeTweenSequence = TweenSequence([
TweenSequenceItem(tween: ConstantTween<double>(0), weight: 85),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 30.0), weight: 10),
TweenSequenceItem(tween: ConstantTween<double>(50), weight: 5),
]);
根据 Tween 实现效果如下
6.实现组合动画
将上面步骤的动画效果都放在一个 Stack
中
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return Center(
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationZ(Math.pi * _rotationTween.value),
child: Stack(
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(60),
child: Container(
color: Colors.purple,
height: _circleHeightTween.value,
width: _circleWidthTween.value,
),
),
Container(
height: _triangleSizeTween.value,
width: _triangleSizeTween.value,
child: CustomPaint(
painter: TrianglePainter(
left: _triangleLeftTween.value,
right: _triangleRightTween.value,
top: _triangleTopTween.value,
),
),
),
Container(
height: _triangleSizeTween.value,
width: _triangleSizeTween.value,
child: CustomPaint(
painter: SquarePainter(progress: _rect1Tween.value),
),
),
Container(
height: _triangleSizeTween.value,
width: _triangleSizeTween.value,
child: CustomPaint(
painter: SquarePainter(
progress: _rect2Tween.value,
color: Color(0xff40e0b0),
),
),
),
Container(
height: _waveHeightTween.value,
width: _waveWidthTween.value,
child: WaveProgress(
size: 120,
borderWidth: 0.0,
backgroundColor: Colors.transparent,
borderColor: Colors.transparent,
waveColor: Color(0xff40e0b0),
progress: 100 * _waveProgressTween.value,
offsetY: _waveOffsetYTween.value,
),
),
Text(
'Welcome',
style: TextStyle(
fontSize: _textSizeTween.value,
color: Colors.white,
),
)
],
),
),
);
},
);
}
这样每个 widget 就会根据自己所依赖的 Tween 值去做动画了,最后实现了 Loading 动画的效果。
四、最后
其实相对于原来 iOS 原生开发来说,Flutter 实现一些效果更加方便,比如 Layout,比如 Hero 动画,所以我是比较看好 Flutter 的。后面也会更多的去分享一些 Flutter 的知识。就比如说这个动画其实分析下来每一步也不是太难,但是要有足够的耐心去分析,迎难而上。
最后所有的源码你都可以从 这里 看到,欢迎 star!