原文連接:medium.com/@felixblasc…git
本文主要介紹如何使用 simple_animations 實現漂亮的動畫效果。Demo 可見 :gsy_flutter_demo
這篇文章將會介紹一個頗有意思的動畫效果,它能讓 Flutter 的頁面顯得更加友好,同時本文也將展現如何使用 simple_animations 庫,在 Flutter 上輕鬆地實現以下圖所示的動畫效果。github
動畫所須要展現的效果是:由平滑過渡的漸變背景組成,而且在文字下面會有多個波從右向左滑動。canvas
接下來首先從背景漸變開始介紹,在 Flutter 中內置的 BoxDecoration
就支持使用 LinearGradient
來實現漸變效果,代碼以下所示:bash
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [color1, color2])),
);
複製代碼
因此咱們只須要在這個基礎上去設置動畫便可,這裏直接使用 simple_animations 來實現效果, 在 simple_animations 中咱們可使用這兩個對象來實現效果:less
MultiTrackTween
(動畫處理對象,一次安排多個補間動畫的屬性)ControlledAnimation
(一個很是簡單的基於補間動畫的控件對象)關於波形的實如今後面的文章中會介紹,這裏先背景漸變的代碼:ide
class AnimatedBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
final tween = MultiTrackTween([
Track("color1").add(Duration(seconds: 3),
ColorTween(begin: Color(0xffD38312), end: Colors.lightBlue.shade900)),
Track("color2").add(Duration(seconds: 3),
ColorTween(begin: Color(0xffA83279), end: Colors.blue.shade600))
]);
return ControlledAnimation(
playback: Playback.MIRROR,
tween: tween,
duration: tween.duration,
builder: (context, animation) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [animation["color1"], animation["color2"]])),
);
},
);
}
}
複製代碼
如上代碼所示,在這裏這裏咱們僅僅定義兩個的 Track
的顏色 color1 和 color2,並將它設置到 ControlledAnimation
中,最後在 LinearGradient
使用這兩個顏色。函數
是否是看起來很簡單,這裏甚至都沒有看到任何 StatefulWidget
或 AnimationController
,你能夠將這段代碼做爲模板,並使用更復雜的顏色過渡對其進行擴展。學習
如今咱們有了一個漸變背景的動畫,接着能夠添加一些新的動畫效果來完善效果,以下圖所示是咱們想要實現的最終效果:動畫
實際上,這是三個波型相互重疊的效果,這裏咱們須要確保它們彼此獨立,以便於最終能夠產生波紋的疊加效果。ui
所以,咱們須要定義一個 WaveAnimation
具有如下屬性的小控件:
speed
:控制波浪動畫的持續時間;height
:設置波浪做用的區域;offset
:x軸的偏移,以給出不一樣的波形「起始位置」;接下來咱們先要討論一個數學的問題,要如何實現一個週期性的循環弧形動畫效果呢?答案只有一個:三角函數。
首先咱們須要爲動畫設置一個 0.0 到 2 * pi 之間的值,並將該值放入到正弦函數中,接着咱們在三個位置上採樣 y 的數值大小:左側、中間和末端。這樣從左到右就覆蓋了一個 pi 大小的間隔,所以咱們始終能夠看到一個完整的正弦波的一半。
咱們爲何要採樣三個位置?由於咱們會根據這三個位置畫一段 Path,以下圖所示提供了這個可視化效果:
咱們從左上角開始(紫色),並在右上角(紅色)添加一個二次貝塞爾函數鏈接過去,而後咱們能夠經過指定一個「控制點」(綠色)來實現這個變化,最後利用了 Flutter 的 Canvas
路徑繪製的方法繪製出一個 Path
。
而後咱們讓紅色的點往橙色移動,以後讓紫色的點往黃色點之後,最後咱們只須要把這個 Path 路徑給鏈接起來就能夠了。
當你只僅關注紫色、綠色和紅色點的時候,就能夠看到咱們的採樣後的路徑是一個正弦波的效果。
這個二次貝塞爾函數乍一看彷佛有些詭異,可是它只是想畫一條從紫色到紅色的直線(藍色)。它和綠點之間的距離越長,線條就會受到某種相似重力因素的影響獲得灰色的形狀,最終的結果是線逐漸彎曲。
class AnimatedWave extends StatelessWidget {
final double height;
final double speed;
final double offset;
AnimatedWave({this.height, this.speed, this.offset = 0.0});
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return Container(
height: height,
width: constraints.biggest.width,
child: ControlledAnimation(
playback: Playback.LOOP,
duration: Duration(milliseconds: (5000 / speed).round()),
tween: Tween(begin: 0.0, end: 2 * pi),
builder: (context, value) {
return CustomPaint(
foregroundPainter: CurvePainter(value + offset),
);
}),
);
});
}
}
class CurvePainter extends CustomPainter {
final double value;
CurvePainter(this.value);
@override
void paint(Canvas canvas, Size size) {
final white = Paint()..color = Colors.white.withAlpha(60);
final path = Path();
final y1 = sin(value);
final y2 = sin(value + pi / 2);
final y3 = sin(value + pi);
final startPointY = size.height * (0.5 + 0.4 * y1);
final controlPointY = size.height * (0.5 + 0.4 * y2);
final endPointY = size.height * (0.5 + 0.4 * y3);
path.moveTo(size.width * 0, startPointY);
path.quadraticBezierTo(
size.width * 0.5, controlPointY, size.width, endPointY);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, white);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
複製代碼
對於這個控件,咱們使用了一個 LayoutBuilder
來檢查可用的寬度再進行繪製,而後使用 ControlledAnimation
和 Playback.LOOP
實現從 0.0 到 2 * pi 的簡單補間數據,以後能夠將當前動畫值傳遞到 CustomPainter
的 Canvas
進行動畫繪製。
最終這個 CustomPainter
能夠實現咱們想要的波形路徑,但須要注意的是,咱們使用的波形與不透明度須要一層層減少,這樣多個波始重疊才能始終可見。
是否是用至關少的代碼就實現了很炫酷的動畫?
最後以下代碼所示,咱們使用 Stack
將控件堆疊在一塊兒。
class FancyBackgroundApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned.fill(child: AnimatedBackground()),
onBottom(AnimatedWave(
height: 180,
speed: 1.0,
)),
onBottom(AnimatedWave(
height: 120,
speed: 0.9,
offset: pi,
)),
onBottom(AnimatedWave(
height: 220,
speed: 1.2,
offset: pi / 2,
)),
Positioned.fill(child: CenteredText()),
],
);
}
onBottom(Widget child) => Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: child,
),
);
}
複製代碼