最近在做一個天氣模塊的時候,風力需要顯示一個旋轉的風車,實現效果如下:
需求分析
我們可以把上面的效果拆分爲兩個部分實現:
1、畫一個風車的FanWidget
2、旋轉動畫
一、風車Widget實現
風車Widget 效果如下:
這裏又可以把它拆分爲如下三部分實現:
- 3片扇葉
- 中間的圓點
- 圓柱
圓點和圓柱都比較好實現,最主要還是三片扇葉的實現。
扇葉的實現思路是:先在原點(0,0)畫一個扇葉,然後在旋轉複製兩個扇葉。
至於爲什麼要在原點畫扇葉?
因爲旋轉是以原點(0,0)爲旋轉的中心點的。
1、扇葉的實現
首先在原點x軸上畫一片扇葉:
@override
void paint(Canvas canvas, Size size) async {
r = width / 2;
var fanPath = Path();
var paint = Paint()
..strokeWidth = 1
..style = PaintingStyle.fill
..color = color;
var bgPaint = Paint()..color = Colors.yellow;
canvas.drawRect(Rect.fromLTRB(0, 0, width, height), bgPaint);
///扇葉的寬度
double fanWidth = height / 3;
///留2個寬度放圓點
fanPath.moveTo(2, 0);
fanPath.quadraticBezierTo(fanWidth / 4, -4, fanWidth / 2, -2);
fanPath.lineTo(fanWidth, 0);
fanPath.lineTo(fanWidth / 2, 2);
fanPath.quadraticBezierTo(fanWidth / 4, 4, 2, 0);
fanPath.close();
canvas.drawPath(fanPath, paint);
}
效果如下:
黃色的背景是這個自定義widget的寬高背景。
然後在旋轉複製第二個和第三個扇葉:
///1角度 = radians弧度
double radians = pi / 180;
///第二個扇葉
canvas.save();
canvas.rotate(radians * 120);
canvas.drawPath(fanPath, paint);
canvas.restore();
///第三個扇葉
canvas.save();
canvas.rotate(radians * 240);
canvas.drawPath(fanPath, paint);
canvas.restore();
由於canvas
旋轉的是弧度,而360角度等於2π弧度
,所以可以得到每1角度的弧度值radians
///1角度 = radians弧度
double radians = pi / 180;
pi
是math方法中π
的值。
每次旋轉之前需要先調用canvas.save();
保存之前的操作,
旋轉完成後,調用canvas.restore();
來合併旋轉的路徑。
旋轉後效果如下:
這個時候我們只需要對Canvas進行平移操作,就可以了。
///半徑
double r;
r = width / 2;
///初始時旋轉的原點在(0,0),平移原點到圓心
canvas.translate(r, fanWidth);
效果如下:
需要注意的是:平移是在畫扇葉之前進行的。
畫圓點:
///中間圓點
canvas.drawCircle(Offset(r, fanWidth), 2, paint);
畫圓柱:
///圓柱
var pillarPath = Path();
pillarPath.moveTo(r + 1, fanWidth + 3);
pillarPath.lineTo(r + 4, height - 2);
pillarPath.quadraticBezierTo(r, height, r - 4, height - 2);
pillarPath.lineTo(r - 1, fanWidth + 3);
pillarPath.lineTo(r + 1, fanWidth + 3);
pillarPath.close();
canvas.drawPath(pillarPath, paint);
畫圓點和圓柱是在canvas平移之前進行的。
效果圖如下:
二、旋轉動畫
因爲是一個單獨的Widget,所以在使用動畫的時候我們也不需要考慮性能之類的東西了。直接使用setState
來刷新就行了。
在動畫結束的時候,再次啓動動畫就行了。
class _PageState extends State<FanWidget> with SingleTickerProviderStateMixin {
///當前動畫的進度0~360
double progress = 0;
AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(seconds: 6))
..addListener(() {
setState(() {
progress = _animationController.value * 360;
});
})
..addStatusListener((AnimationStatus status) {
///動畫結束後啓動動畫
if (status == AnimationStatus.completed) {
_animationController.reset();
_animationController.forward();
}
});
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return Container(
child: _FanWidget(
width: widget.width,
height: widget.height,
progress: progress,
color: widget.color,
),
);
}
@override
void dispose() {
_animationController?.dispose();
super.dispose();
}
}
///風車widget
class FanWidget extends StatefulWidget {
final double width;
final double height;
final Color color;
FanWidget({this.width, this.height, this.color = Colors.white});
@override
State<StatefulWidget> createState() {
return _PageState();
}
}
在_PageState
中,直接使用AnimationController
啓動一個值爲0-1
的動畫,時間爲6秒。並在addStatusListener
中監聽動畫執行的狀態,當動畫執行完畢後,直接調用forward
再次執行動畫是沒有效果的,要先調用reset
將當前狀態重置,然後在執行forward
啓動動畫。
關於動畫的狀態等其他相關點,可以看:
progress
的賦值如下:
progress = _animationController.value * 360;
由於_animationController.value
的值爲0~1,所以progress
的值爲0~360,可以用來表示第一個扇葉旋轉的角度。
因爲另外兩個扇葉都是基於第一個扇葉分別進行120°旋轉和240°旋轉得到的。所以我們只需要旋轉第一個扇葉就行了。
即在FanWidget
中,我們還需要添加一個旋轉的操作:
///旋轉
canvas.rotate(radians * progress);
旋轉的操作放在平移之後,畫扇葉之前進行。
這樣我們通過addListener
監聽動畫執行進度,然後賦值progress
,調用setState
刷新進度,從而實現旋轉的動畫。
總結
- 旋轉時需要注意旋轉的中心點是在
原點
處(0,0) - 旋轉的角度是
弧度
restore
之前,要進行save
操作
完整代碼:
https://github.com/Zhengyi66/FlutterDemo/blob/master/lib/widget/fan_widget.dart