Flutter 复杂 Loading 动画的抽丝剥茧
一、前言
之前在我的一篇文章 iOS复杂动画之抽丝剥茧 中有分析过一个复杂的 Loading 动画的实现过程,现在有意看了下 Flutter 的动画,所以就有了一个用 Flutter 来实现这个动画当练习的想法,最终使用 Flutter 实现效果如下
welcome.gif
下面我们就来重新分析下这个动画的实现过程
二、动画的步骤分析
上面图中的动画第一眼看起来确实是有点复杂,但是我们来一步步分析,就会发现其实并不是那么难。仔细看一下就会发现,大致步骤如下:
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, ), ), ); }, ); }效果如下
circle.gif
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, ), ), ), ), ); }, ); }最后三角形动画变化效果如下
triangle.gif
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), ), ), ) ], ), ), ); }, );}最后实现效果如下
square.gif
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 实现效果如下
wave.gif
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!
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Flutter 复杂 Loading 动画的抽丝剥茧