基于Flutter實現(xiàn)風車加載組件的制作
前言
Flutter 官方提供了諸如 CircularProgressIndicator和 LinearProgressIndicator兩種常見的加載指示組件,但是說實話,實在太普通,比如下面這個CircularProgressIndicator。

正好我們介紹到了動畫環(huán)節(jié),那我們自己來一個有趣的加載指示組件吧。創(chuàng)意送哪來呢,冥思苦想中腦海里突然就響起了一首歌:
大風車吱呀吱喲喲地轉,這里的風景呀真好看! 天好看,地好看
沒錯,這就是當時風靡全中國的放學檔,兒童必看節(jié)目《大風車》的主題曲。

嗯,我們就自己來個風車動畫加載組件吧,最終完成效果如下,支持尺寸和旋轉速度的設定。

接口定義
遵循接口先行的習慣,我們先設計對外的接口。對于一個動畫加載組件,我們需要支持兩個屬性:
- 尺寸:可以由調用者決定尺寸的大小,以便應用在不同的場合。由于風車加載是個正方形,我們定義參數(shù)名為
size,類型為double。 - 速度:風車是旋轉的,需要支持旋轉速度調節(jié),以便滿足應用用戶群體的偏好。我們定義參數(shù)名為
speed,單位是轉/秒,即一秒旋轉多少圈,類型也是double。 - 旋轉方向:可以控制順時針還是逆時針方向旋轉。參數(shù)名為
direction,為枚舉。枚舉名稱為RotationDirection,有兩個枚舉值,分別是clockwise和antiClockwise。
其實還可以支持顏色設置,不過看了一下,大部分風車是4個葉片,顏色為藍黃紅綠組合,這里我們就直接在組件內部固定顏色了,這樣也可以簡化調用者的使用。 然后是定義組件名稱,我們依據(jù)英文意思,將組件命名為 WindmillIndicator。
實現(xiàn)思路
風車繪制
關鍵是繪制風車,根據(jù)給定的尺寸繪制完風車后,再讓它按設定的速度旋轉起來就好了。繪制風車的關鍵點又在于繪制葉片,繪制一個葉片后,其他三個葉片依次旋轉90度就可以了。我們來看一下葉片的繪制。葉片示意圖如下:

葉片整體在一個給定尺寸的正方形框內,由三條線組成:
- 紅色線:弧線,我們設定起點在底邊X 軸方向1/3寬度處,終點是左側邊 Y 軸方向1/3高度處,圓弧半徑為邊長的一半。
- 綠色線:弧線,起點為紅色線的終點,終點為右上角頂點,圓弧半徑為邊長。
- 藍色線,連接綠色線的終點和紅色線的起點,以達到閉合。
有了葉片,其他的就是依次旋轉90度了,繪制完后的示意圖如下所示:

旋轉效果
我們把每一個葉片作為獨立的組件,按照設定的速度,更改旋轉角度即可,只要4個葉片的旋轉增量角度同時保持一致,風車的形狀就能夠一致保持,這樣就有風車旋轉的效果了。
代碼實現(xiàn)
WindmillIndicator定義
WindmillIndicator 需要使用 Animation 和 AnimationController 來控制動畫,因此是一個 StatefulWidget。根據(jù)我們上面的接口定義,得到WindmillIndicator的定義如下:
class WindmillIndicator extends StatefulWidget {
final size;
// 旋轉速度,默認:1轉/秒
final double speed;
final direction;
WindmillIndicator({Key? key,
this.size = 50.0,
this.speed = 1.0,
this.direction = RotationDirection.clockwise,
})
: assert(speed > 0),
assert(size > 0),
super(key: key);
@override
_WindmillIndicatorState createState() => _WindmillIndicatorState();
}這里使用了 assert 來防止參數(shù)錯誤,比如 speed 不能是負數(shù)和0(因為后面計算旋轉速度需要將 speed 當除數(shù)來計算動畫周期),同時 size 不可以小于0。
旋轉速度設定
我們使用 Tween<double>設定Animation 的值的范圍,begin和 end 為0和1.0,然后每個葉片在構建的時候旋轉角度都加上2π 弧度乘以 Animation 對象的值,這樣一個周期下來就是旋轉了一圈。然后是 AnimationController 來控制具體的選擇速度,實際的時間使用毫秒數(shù),用1000 / speed 得到的就是旋轉一圈需要的毫秒數(shù)。這樣即能夠設定旋轉速度為 speed。代碼如下所示:
class _WindmillIndicatorState extends State<WindmillIndicator>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
int milliseconds = 1000 ~/ widget.speed;
controller = AnimationController(
duration: Duration(milliseconds: milliseconds), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(controller)
..addListener(() {
setState(() {});
});
controller.repeat();
}
@override
Widget build(BuildContext context) {
return AnimatedWindmill(
animation: animation,
size: widget.size,
direction: widget.direction,
);
}
@override
void dispose() {
if (controller.status != AnimationStatus.completed &&
controller.status != AnimationStatus.dismissed) {
controller.stop();
}
controller.dispose();
super.dispose();
}這里在initState 里設置好參數(shù)之后就調用了controller.repeat(),以使得動畫重復進行。在 build 方法里,我們構建了一個AnimatedWindmill對象,將 Animation 對象和 size 傳給了它。AnimatedWindmill是風車的繪制和動畫組件承載類。
風車葉片繪制
風車葉片代碼定義如下:
class WindmillWing extends StatelessWidget {
final double size;
final Color color;
final double angle;
const WindmillWing(
{Key? key, required this.size, required this.color, required this.angle});
@override
Widget build(BuildContext context) {
return Container(
transformAlignment: Alignment.bottomCenter,
transform: Matrix4.translationValues(0, -size / 2, 0)..rotateZ(angle),
child: ClipPath(
child: Container(
width: size,
height: size,
alignment: Alignment.center,
color: color,
),
clipper: WindwillClipPath(),
),
);
}
}共接收三個參數(shù):
- size:即矩形框的邊長;
- color:葉片填充顏色;
- angle:葉片旋轉角度。
實際葉片旋轉時參照底部中心位置(bottomCenter)旋轉(不同位置的效果不一樣,感興趣的可以拉取代碼修改試試)。這里有兩個額外的注意點:
transform參數(shù)我們首先往 Y 軸做了 size / 2的平移,這是因為旋轉后風車整體位置會偏下size / 2,因此上移補償,保證風車的位置在中心。
實際葉片的形狀是對 Container 進行裁剪得來的,這里使用了 ClipPath 類。ClipPath 支持使用自定義的CustomClipper<Path>裁剪類最子元素的邊界進行裁剪。我們定義了WindwillClipPath類來實現(xiàn)我們說的風車葉片外觀裁剪,也就是把正方形裁剪為風車葉片形狀。WindwillClipPath的代碼如下,在重載的 getClip方法中將我們所說的葉片繪制路徑返回即可。
class WindwillClipPath extends CustomClipper<Path> {
@override
Path getClip(Size size) {
var path = Path()
..moveTo(size.width / 3, size.height)
..arcToPoint(
Offset(0, size.height * 2 / 3),
radius: Radius.circular(size.width / 2),
)
..arcToPoint(
Offset(size.width, 0),
radius: Radius.circular(size.width),
)
..lineTo(size.width / 3, size.height);
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return false;
}
}風車組件
有了風車葉片組件,風車組件構建就簡單多了(這也是拆分子組件的好處之一)。我們將風車組件繼承 AnimatedWidget,然后使用 Stack 組件將4個葉片組合起來,每個葉片給定不同的顏色和旋轉角度即可。而旋轉角度是由葉片的初始角度加上Animation對象控制的旋轉角度共同確定的。然后控制順時針還是逆時針根據(jù)枚舉值控制角度是增加還是減少就可以了,風車組件的代碼如下:
class AnimatedWindmill extends AnimatedWidget {
final size;
final direction;
AnimatedWindmill(
{Key? key,
required Animation<double> animation,
required this.direction,
this.size = 50.0,
}) : super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
final rotationAngle = direction == RotationDirection.clockwise
? 2 * pi * animation.value
: -2 * pi * animation.value;
return Stack(
alignment: Alignment.topCenter,
children: [
WindmillWing(
size: size,
color: Colors.blue,
angle: 0 + rotationAngle,
),
WindmillWing(
size: size,
color: Colors.yellow,
angle: pi / 2 + rotationAngle,
),
WindmillWing(
size: size,
color: Colors.green,
angle: pi + rotationAngle,
),
WindmillWing(
size: size,
color: Colors.red,
angle: -pi / 2 + rotationAngle,
),
],
);
}
}運行效果
我們分別看運行速度為0.5和1的效果,實測感覺速度太快或太慢體驗都一般,比較舒適的速度在0.3-0.8之間,當然你想晃暈用戶的可以更快些。


源碼已提交至:動畫相關源碼,想用在項目的可以直接把WindmillIndicator的實現(xiàn)源文件windmill_indicator.dart拷貝到自己的項目里使用。
總結
本篇實現(xiàn)了風車旋轉的加載指示動畫效果,通過這樣的效果可以提升用戶體驗,尤其是兒童類的應用,絕對是體驗加分的動效。從 Flutter學習方面來說,重點是三個知識:
Animation、AnimationController 和 AnimatedWidget的應用;
Matrix4控制Container 的平移和旋轉的使用;
使用 ClipPath 和自定義CustomClipper<Path> 對組件形狀進行裁剪,這個在很多場景會用到,比如那些特殊形狀的組件。
以上就是基于Flutter實現(xiàn)風車加載組件的制作的詳細內容,更多關于Flutter風車加載組件的資料請關注腳本之家其它相關文章!
相關文章
Android文本框搜索和清空效果實現(xiàn)代碼及簡要概述
在工作過程中可能會遇到這樣一個效果:文本框輸入為空時顯示輸入的圖標;不為空時顯示清空的圖標,此時點擊清空圖標能清空文本框內輸入文字,感興趣的你可以了解下哦,或許對你學習android有所幫助2013-02-02
android編程實現(xiàn)添加文本內容到sqlite表中的方法
這篇文章主要介紹了android編程實現(xiàn)添加文本內容到sqlite表中的方法,結合實例較為詳細的分析了Android針對txt文本文件的讀取及SQL數(shù)據(jù)庫操作的相關技巧,需要的朋友可以參考下2015-11-11
Android中l(wèi)istview嵌套scrollveiw沖突的解決方法
這篇文章主要為大家詳細介紹了Android中l(wèi)istview嵌套scrollveiw沖突的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01
Android應用借助LinearLayout實現(xiàn)垂直水平居中布局
這篇文章主要介紹了Android應用借助LinearLayout實現(xiàn)垂直水平居中布局的方法,文中列舉了LinearLayout線性布局下居中相關的幾個重要參數(shù),需要的朋友可以參考下2016-04-04
Android中gson、jsonobject解析JSON的方法詳解
JSON即JavaScript Object Natation, 它是一種輕量級的數(shù)據(jù)交換格式, 與XML一樣, 是廣泛被采用的客戶端和服務端交互的解決方案.接下來由腳本之家小編給大家介紹Android中gson、jsonobject解析JSON的方法,感興趣的朋友一起學習吧2016-02-02
詳解Android_性能優(yōu)化之ViewPager加載成百上千高清大圖oom解決方案
這篇文章主要介紹了詳解Android_性能優(yōu)化之ViewPager加載成百上千高清大圖oom解決方案,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2016-12-12

