服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - 如何使用Flutter实现58同城中的加载动画详解

如何使用Flutter实现58同城中的加载动画详解

2022-11-02 14:19吴振 Android

这篇文章主要给大家介绍了关于如何使用Flutter实现58同城中加载动画详的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Flutter具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

前言

在应用中执行耗时操作时,为了避免界面长时间等待造成假死的现象,往往会添加一个加载中的动画来提醒用户,在58同城中也不例外,而且我们并没有使用系统默认的加载动画,而是制作了一个具有58特色的加载动画。

在本篇文章中,给大家分享下笔者使用Flutter实现58同城中加载动画的过程。先看一下加载动画的效果:

如何使用Flutter实现58同城中的加载动画详解

动画效果乍看比较复杂,难以看出端倪,其实我们可以先调慢动画的速度,这样能够比较清晰地分析出动画的流程。

动画的流程

动画由两个圆弧的动效组成,两个圆弧的起始点角度和扫过的弧度随着时间规律变化。仔细观察会发现,两个圆弧的动效其实是一样的,只不过起始位置是不一样的。我们先看下外部大圆弧的运动规律。

大圆弧从x轴正方向开始运动,按照动画的运动规律,可以将动画分为三个阶段:

第一阶段:圆弧起点的在x轴正方向,终点的角度x轴正方向开始向下逐渐增大,直到终点到达y轴负方向位置,最终圆弧扫过的角度为180度。

第二阶段:圆弧扫过的角度保持在180度,起点和终点一起顺时针旋转,直到旋转180度后终点到达x轴正方向。

第三阶段:圆弧的终点保持在x轴正方向,起点顺时针旋转,直到起点也到达x轴正方向,此时完成一个完整的动画。接下来继续重复动画的第一阶段,组成一个连贯的动画。

分析完动画的流程,思路就很清晰了,我们按照动画流程把动画拆分成三部分,通过对圆弧的起点、终点和扫过角度的变换,组合成一个完整的动画,然后不断地重复,最后就变成了一个加载中的动画效果。

接下来开始写代码实现。

由于动画是由一个圆弧不断变化组成的,如果使用Android,我们很自然的想到可以使用Canvas来进行圆弧的绘制,然后根据时间的变化不停地重新绘制圆弧,从而实现动画效果。那么在Flutter中是否也存在Canvas呢,答案是肯定的,Flutter和Android一样,也存在Canvas。

Flutter中的Canvas

Flutter中使用 CustomPainter 类在Canvas上进行绘制,该类包含一个 paint() 方法,该方法提供了一个Canvas对象,可以用来绘制各种图形。

?
1
2
3
4
5
abstract class CustomPainter extends Listenable {
 
void paint(Canvas canvas, Size size);
 
}

不过在Flutter中一切皆是Widget,而承载Canvas功能的Widget是 CustomPaint 类。 CustomPaint 包含一个painter属性,用来指定进行绘制的 CustomPainter,源码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
class CustomPaint extends SingleChildRenderObjectWidget {
 
const CustomPaint({
 
Key key,
 
this.painter,
 
});
 
final CustomPainter painter;
 
}

Flutter中的Canvas和Android类似,提供了一系列的API用来绘制点、线、圆形、正方形等,而且API很类似,对比一下Flutter与Android中Canvas的常见API(具体的参数列表请参考文档和源码,篇幅有限不再一一列出):

 

  Android Flutter

drawPoint()

drawPoints()

drawPoints()
线

drawLine()

drawLines()

drawLine()
drawCircle() drawCircle()
椭圆 drawOval() drawOval()
圆弧 drawArc() drawArc()
矩形 drawRect() drawRect()
Path drawPath() drawPath()
图片 drawBitmap() drawImage()
文字 drawText() drawParagraph()
变换

save()

restore()

save()

restore()


 

 

要绘制动画中的圆弧,应该使用 drawArc() 方法来实现,这里需要注意的是drawArc()方法的参数:startAngle和sweepAngle的单位是弧度(180度等于π弧度)。

具体来看一下 Canvas.drawArc() 方法的参数列表:

?
1
2
3
4
5
6
7
8
9
10
11
/// rect: 圆弧四周范围所形成的矩形,在本篇中圆弧为圆形,可以使用Rect.fromCircle()确定圆弧的范围
 
/// startAngle: 圆弧起始点的角度,x轴正方向为0度,按顺时针递增,y轴负方向为90度,以此类推
 
/// sweepAngle: 圆弧扫过的角度,即圆弧终点所在的角度为startAngle + sweepAngle
 
/// useCenter: 如果为true,圆弧两端会与圆心相连,形成一个扇形,本篇中应为false
 
/// paint: 画笔,下文中会进行简单介绍
 
void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)

在Canvas的一系列方法中会发现一个熟悉的名称:Paint,与Android类似,Flutter中的Paint类也是用来描述画笔的。

Paint类

Paint类位于 dart.ui 库中,Paint类保存了画笔的颜色、粗细、是否抗锯齿、着色器等属性。

下面简单的介绍下几个常用的属性:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Paint paint = Paint()
 
..color = Color(0xFFFF552E)
 
..strokeWidth = 2.0
 
..style = PaintingStyle.stroke
 
..isAntiAlias = true
 
..shader = LinearGradient(colors: []).createShader(rect)
 
..strokeCap = StrokeCap.round
 
..strokeJoin = StrokeJoin.bevel;

属性说明:

  • color:Color类型,设置画笔的颜色。
  • strokeWidth:double类型,设置画笔的粗细。
  • style:PaintingStyle枚举类型,设置画笔的样式, PaintingStyle.stroke 为描边, PaintingStyle.fill 为填充。
  • isAntiAlias:bool类型,设置是否抗锯齿,true为开启抗锯齿。
  • shader:Shader类型,着色器,一般用来绘制渐变效果,可以使用 LinearGradient、 RadialGradient、 SweepGradient 等。
  • strokeCap:StrokeCap枚举类型,设置线条两端点的样式, StrokeCap.butt 为无(默认值), StrokeCap.round 为圆形, StrokeCap.square 为方形。
  • strokeJoin:StrokeJoin枚举类型,设置线条交汇处的样式, StrokeJoin.miter 为锐角, StrokeJoin.round 为圆弧, StrokeJoin.bevel 为斜角,可以参考下图方便理解:

熟悉了Canvas和Paint的使用之后,就能够绘制出加载动画的圆弧了。当然,只是绘制出圆弧并没有什么用,主要是怎么让圆弧动起来。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Flutter中的动画
 
想要让圆弧动起来,我们需要使用到Flutter的动画。下面先来介绍下Flutter中动画的实现。
 
Flutter中的动画相关的类主要有以下几个:
 
 Animation:动画的核心类,是一个抽象类。用来生成动画执行过程中的插值,输出的结果可以是线性或曲线的,Animation对象与UI渲染没有任何关系。
 
 abstract class Animation<T> extends Listenable implements ValueListenable<T> {
 
  /// 添加动画状态的监听
 
  void addStatusListener(AnimationStatusListener listener);
 
 
  /// 移除动画状态的监听
 
  void removeStatusListener(AnimationStatusListener listener);
 
 
  /// 获取当前动画的状态
 
  AnimationStatus get status;
 
 
  /// 获取当前动画的插值,执行动画时需要根据该值进行UI绘制等
 
  T get value;
 
 }

    AnimationController:动画的管理类,继承自 Animation<double>。默认情况下在给定的时间范围内线性生成从0.0到1.0的值。

    AnimationController对象需要传递一个vsync参数,它接收一个TickerProvider类型的对象,主要职责是创建Ticker。Flutter应用在启动时会绑定一个SchedulerBinding,可以给每一次屏幕刷新添加回调,Ticker就是通过SchedulerBinding来添加屏幕刷新的回调,当屏幕刷新时,会通知到绑定的Ticker回调。假如动画的UI不在当前屏幕,比如锁屏时,锁屏后屏幕停止刷新,不会通知SchedulerBinding,Ticker也就不会触发,这样就能够防止屏幕外的动画消耗不必要的资源。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class AnimationController extends Animation<double>
 
 with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
 
 /// value:动画的初始值,默认是lowerBound
 
 /// duration:动画执行的时长
 
 /// lowerBound:动画的最小值,默认值为0.0
 
 /// upperBound:动画的最大值,默认值为1.0
 
 /// vsync:可以通过 `with SingleTickerProviderStateMixin` 传入StatefulWidget对象
 
 AnimationController({
 
 double value,
 
 this.duration,
 
 this.lowerBound = 0.0,
 
 this.upperBound = 1.0,
 
 @required TickerProvider vsync,
 
 }) {
 
 _ticker = vsync.createTicker(_tick);
 
 }
 
 
 Ticker _ticker;
 
 
 /// Ticker的回调,每次屏幕刷新都会回调
 
 void _tick(Duration elapsed) {
 
 notifyListeners();
 
 }
 
 
 /// 开始播放动画
 
 TickerFuture forward({ double from })
 
 
 /// 反向播放动画
 
 TickerFuture reverse({ double from })
 
 
 /// 设置动画重复执行
 
 TickerFuture repeat({ double min, double max, bool reverse = false, Duration period })
 
 
 /// 释放动画资源
 
 void dispose()
 
}

    CurvedAnimation:非线性动画类,继承自 Animation<double>。CurvedAnimation可以使用curve属性指定曲线函数Curve,类似Android动画的插值器,Flutter中已经实现了许多常用的曲线,在Curves类中可以找到,比如Curves.linear、Curves.decelerate、Curves.ease。也可以继承Curve类重写 transform() 方法来实现自定义的曲线函数。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class CurvedAnimation extends Animation<double>
 
 with AnimationWithParentMixin<double> {
 
 /// parent:指定AnimationController对象
 
 /// curve:指定动画的曲线函数
 
 CurvedAnimation({
 
 @required this.parent,
 
 @required this.curve,
 
 })
 
}
 
 
abstract class Curve {
 
 /// 计算动画执行中`t`点的插值,可以自定义曲线函数
 
 double transform(double t)
 
}

    Tween:补间值的生成类,继承自 Animatable<T>。

    由于AnimationController的值范围默认为0.0到1.0,如果需要不同的范围或数据类型,可以使用Tween指定动画值的范围。Tween不仅能返回double类型的值,还有IntTween、ColorTween、SizeTween等各种返回不同数据类型的子类。
    使用Tween对象需要调用 animate() 方法,传入AnimationController对象,该方法会返回一个Animation,这样就可以获取到动画的插值了。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Tween<T extends dynamic> extends Animatable<T> {
 
 /// begin:动画的起始值
 
 /// end:动画的结束值
 
 Tween({ this.begin, this.end });
 
 
 /// 可以把double类型的动画插值转换成任何类型的值
 
 T transform(double t)
 
 
 /// parent:传入AnimationController对象
 
 /// 返回Animation对象,使用Animation.value获取动画当前的插值
 
 Animation<T> animate(Animation<double> parent)
 
}

    AnimatedBuilder:用于构建动画的Widget,将动画和要执行动画的Widget关联起来,继承关系为AnimatedBuilder → AnimatedWidget → StatefulWidget。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class AnimatedBuilder extends AnimatedWidget {
 
 const AnimatedBuilder({
 
 @required Listenable animation,
 
 @required this.builder,
 
 });
 
 
 /// typedef TransitionBuilder = Widget Function(BuildContext context, Widget child);
 
 /// builder是一个函数,返回Widget对象
 
 final TransitionBuilder builder;
 
 
 @override
 
 Widget build(BuildContext context) {
 
 return builder(context, child);
 
 }
 
}
 
 
abstract class AnimatedWidget extends StatefulWidget {
 
 const AnimatedWidget({
 
 @required this.listenable,
 
 });
 
 
 @protected
 
 Widget build(BuildContext context);
 
 
 @override
 
 _AnimatedState createState() => _AnimatedState();
 
}
 
 
class _AnimatedState extends State<AnimatedWidget> {
 
 @override
 
 void initState() {
 
 super.initState();
 
 widget.listenable.addListener(_handleChange);
 
 }
 
 
 @override
 
 void dispose() {
 
 widget.listenable.removeListener(_handleChange);
 
 super.dispose();
 
 }
 
 
 void _handleChange() {
 
 setState(() { });
 
 }
 
 
 @override
 
 Widget build(BuildContext context) => widget.build(context);
 
}

分析上面列出的源码,AnimatedWidget是一个StatefulWidget。当AnimatedWidget关联的_AnimatedState初始化时,会注册动画的监听函数_handleChange,_handleChange监听函数中又调用了setState()方法,即动画插值每次改变时都会调用build()方法。_AnimatedState.build()方法中又调用了AnimatedWidget.build()方法,在AnimatedBuilder中实现了AnimatedWidget.build()方法:调用属性builder生成Widget,最终实现了动画与Widget的绑定。

加载动画的实现

了解了Flutter的动画后,再结合之前对加载动画流程的分析,加载动画可分成三个阶段,我们可以依赖Tween类,指定值的范围从0.0到3.0变化,当然也可以只使用AnimationController,指定lowerBound和upperBound的值分别为0.0和3.0。这里之所以不使用CurvedAnimation,是因为加载动画的圆弧是线性变化的,不存在加速减速,没有必要使用。

大圆弧能够实现了,我们再来看内部的小圆弧,仔细观察会发现小圆弧的变化规律与大圆弧完全一致,只不过小圆弧的起始位置在x轴负方向,与大圆弧正好相差180度,也就是π弧度。在绘制大圆弧的同时,可以很轻松的计算出小圆弧的起点的角度(即大圆弧起点的角度+π弧度)。

至此整个动画的实现思路就清晰了:

  1. 自定义加载动画的Widget,继承自CustomPaint类。
  2. 使用AnimationController、Tween创建动画,动画的值范围从0.0到3.0线性变化,并且设置动画重复执行。动画插值每递增1.0代表动画执行的一个阶段。
  3. 继承CustomPainter类,实现paint()方法绘制圆弧。根据动画的插值判断当前属于动画的哪个阶段,再计算出圆弧的起点、扫过的角度,绘制出两个圆弧。

下面是实现加载动画的关键代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import 'dart:math';
 
import 'package:flutter/material.dart';
 
 
class WubaLoadingWidget extends StatefulWidget {
 
 @override
 
 _WubaLoadingWidgetState createState() => _WubaLoadingWidgetState();
 
}
 
 
class _WubaLoadingWidgetState extends State<WubaLoadingWidget>
 
 with SingleTickerProviderStateMixin {
 
 AnimationController _animationController;
 
 Animation<double> _animation;
 
 
 @override
 
 void initState() {
 
 super.initState();
 
 _animationController = new AnimationController(
 
  // 可以指定lowerBound、upperBound,使用AnimationController对象
 
  // lowerBound: 0.0,
 
  // upperBound: 3.0,
 
  vsync: this,
 
  duration: const Duration(milliseconds: 1500),
 
 );
 
 _animation = Tween(begin: 0.0, end: 3.0)
 
  .animate(_animationController);
 
 _animationController.forward(); // 执行动画
 
 _animationController.repeat(); // 设置动画循环执行
 
 }
 
 
 @override
 
 void dispose() {
 
 // 调用dispose()方法释放动画资源
 
 _animationController.dispose();
 
 super.dispose();
 
 }
 
 
 @override
 
 Widget build(BuildContext context) {
 
 return AnimatedBuilder(
 
  animation: _animationController,
 
  builder: (BuildContext context, Widget child) {
 
  return Container(
 
   child: CustomPaint(
 
   painter: _LoadingPaint(
 
    value: _animation.value,
 
   ),
 
   ),
 
  );
 
  },
 
 );
 
 }
 
}
 
 
class _LoadingPaint extends CustomPainter {
 
 final double value;
 
 final Paint _outerPaint; // 大圆弧的Paint
 
 final Paint _innerPaint; // 小圆弧的Paint
 
 
 _LoadingPaint({
 
 this.value,
 
 });
 
 
 @override
 
 void paint(Canvas canvas, Size size) {
 
 double startAngle = 0;
 
 double sweepAngle = 0;
 
 // 动画的第一阶段:圆弧起点为0度,终点的角度递增
 
 if (value <= 1.0) {
 
  startAngle = 0;
 
  sweepAngle = value * pi;
 
 }
 
 // 动画的第二阶段:圆弧扫过的弧度为π弧度(180度),起点、终点一起顺时针旋转,一共旋转π弧度
 
 else if (value <= 2.0) {
 
  startAngle = (value - 1) * pi;
 
  sweepAngle = pi;
 
 }
 
 // 动画的第三阶段:圆弧的终点不变,起点从x轴负方向开始顺时针旋转,直到起点也到达x轴正方向
 
 else {
 
  startAngle = pi + (value - 2) * pi;
 
  sweepAngle = (3 - value) * pi;
 
 }
 
 // 绘制外圈的大圆弧
 
 canvas.drawArc(outerRect, startAngle, sweepAngle, false, _outerPaint);
 
 // 绘制内圈的小圆弧
 
 canvas.drawArc(innerRect, startAngle + pi, sweepAngle, false, _innerPaint);
 
 }
 
 
 @override
 
 bool shouldRepaint(CustomPainter oldDelegate) {
 
 return true;
 
 }
 
}

总结

Flutter的Canvas、Paint与Android的API非常类似,基本的思路也一致,对于Android同学比较容易掌握。

Flutter中动画的实现相较于Android逻辑更加清晰简单,方便易用。AnimatedBuilder类巧妙的将UI与动画整合在一起,把UI和动画职责分离,这种思路值得学习。Flutter中的动画还有路由过渡动画、Hero动画、切换动画组件AnimatedSwitcher等,有需要的同学可以查找相关资料。

如果大家需要定制一些个性化的加载动画,推荐一个GitHub的开源项目:flutter_spinkit,这个插件提供了很多种常用的加载动画效果。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对服务器之家的支持。

原文链接:https://mp.weixin.qq.com/s?__biz=MzI2NzI4MTEwNA==&mid=2247486001&idx=1&sn=43054249f7b8a9da4416f394fc8ef6ea

延伸 · 阅读

精彩推荐