前言
前不久,利用週末時間學習並完成一個簡單的 Flutter 項目 - 簡悅天氣,簡約不簡單,豐富不復雜,這是一款簡約風格的 flutter 天氣項目,提供實時、多日、24 小時、颱風路徑以及生活指數等服務,支持定位、刪除、搜索等操作。
下圖爲主頁效果:
開始
項目中很多自定義 widget,今天的主角是 背景層,不同的天氣氣象有不一樣的呈現效果,一共實現了 12 種類別,其中有 晴、多雲、陰天、小中大雨、小中大雪、霧、霾、浮塵,而背景層又分爲三層:
- 背景顏色層。從上到下的漸變效果
- 雲層。只有一種圖片,對其位移、數量、染色做不同變化達到不同效果
- 雨雪層。爲雨雪天氣單獨做了動畫,很炫酷。
好,真正的主角就是這個雨雪層,爲了更好的預覽效果,在關於頁面有上角添加切換天氣類型的入口,實時查看不同氣象下不同的背景效果。如下圖,爲雨雪的最終效果(gif 效果看起來會失真,請下載 apk 自行體驗):
不得不說,如此複雜的動畫(複雜並不是指多難實現,而是不停的繪製很多圖片下),Flutter 還能有不錯的性能表現,媲美原生效果。
效果實現
這裏不贅述繪製和動畫相關知識,網上已經有很多文章介紹,本篇只針對項目中用到的實現方式和相關知識進行講述,具有一定的侷限性,適合簡單的繪製動畫邏輯。
創建繪製類
因爲 Flutter 處處是 widget,自定義 View 需要用到的是 CustomPaint,而成員變量中需要傳入實現 CustomPainter 的類,那咱們先創建此類。
class RainSnowPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
有沒有很熟悉,看到了 Canvas 的類,自然也有 Paint 類,有了畫筆和畫板,剩下就好辦了。
構造雨雪對象
對需要實現的效果進行分析,首先雨雪效果是由一張圖片不同屬性拼接而成,每個雨滴和雪花落實在屏幕上,必須有 x,y 的座標屬性。爲了營造遠近的效果,需要加上 scale 值,由於更加還原真實的視覺效果,雨滴的遠近,必然速度上和清晰度上會有差異,因此加上 speed 和 alpha 屬性,再加上其他計算用的屬性,最後類的聲明如下:
class RainSnowParams {
double x;
double y;
double speed;
double scale;
double width;
double height;
double alpha;
WeatherType weatherType;
RainSnowParams(this.width, this.height, this.weatherType);
}
屬性初始化
有了屬性後,接下來就是對屬性進行賦值,爲了保證效果更加的還原,所有屬性既要有規則,又要隨機。怎麼解釋規則性和隨機性都要同時擁有,就拿雨速而言,小雨相對於大雨,雨的速度稍慢,但是不能很慢,並且每滴雨滴的速度不一樣,這就致使小雨的速度必然在一個區間下隨機,同樣雪也一樣。
初始化又分成兩步,第一次的初始化和雨滴下落結束後的數據重置,實際上兩者的區別只在於 y。第一次初始化 y 在屏幕高度中隨機放置,而雨滴下落結束後,y 值置爲0。那麼就可以把重置邏輯封裝統一的方法。
void reset() {
double initScale = 0.1;
double gapScale = 0.2;
double initSpeed = 40;
double gapSpeed = 40;
if (weatherType == WeatherType.lightRainy) {
initScale = 1.05;
gapScale = 0.1;
initSpeed = 15;
gapSpeed = 10;
} else if(){
...// 其他雨雪情況
}
double random = Random().nextDouble();
this.scale = initScale + gapScale * random;
this.speed = initSpeed + gapSpeed * (1 - random);
this.alpha = 0.1 + 0.9 * random;
x = Random().nextInt(width * 1.2 ~/ scale).toDouble() - width * 0.1 ~/ scale;
}
其中 init 代表這初始值,gap 代表浮動值,這兩個根據雨雪量大小而做不同區分。通過 Random().nextDouble()
獲取隨機 [0.0, 1.0]
的值,random * gap + init
就是最終的值。
x 的屬性控制在 [-0.1*width, 1.1 width]
的區間內隨機,y 值上面已提到。
雪相對有雨有個不同,雨是垂直下落,而雪是隨風搖擺,那爲了營造這種感覺,此時就需要藉助 sin
函數。
if (WeatherUtil.isSnow(_state.widget.weatherType)) {
double offsetX = sin(params.y / (300 + 50 * params.alpha)) * (1 + 0.5 * params.alpha);
params.x += offsetX;
}
開始繪製
終於到了最重要的步驟 繪製,但他並不是最難的,有了前面創建好的屬性和對其初始化,剩下就只是調用 api 進行繪製即可。不過再此之前好像漏了什麼沒說,沒錯,就是 動畫,一個無限循環的動畫。
Flutter 中創建動畫也很簡單,需要在動畫監聽中,判斷如果動畫結束則重新繼續執行即可。
1. 在 initState 函數中初始化 controller, animation 和 listener
_controller =
AnimationController(duration: Duration(minutes: 1), vsync: this);
CurvedAnimation(parent: _controller, curve: Curves.linear);
_controller.addListener(() {
setState(() {});
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.repeat();
}
});
_controller.forward();
2. 在 dispose 函數中釋放掉動畫資源
@override
void dispose() {
_controller.dispose();
super.dispose();
}
在初始化是便讓他執行並一直執行知道頁面銷燬,有了動畫後,開始進行繪製,雨雪的繪製邏輯基本相似,只不過圖片源不一樣。
別看屏幕上有很多雨滴,其實只用了一直圖片,通過控制 alpha、speed和scale 的屬性來隨機展現不同的形態。還有,根據氣象大中小雨類型的區分,會直接落實到雨滴數量和雨滴形態上的變化,營造出多樣的差異。
void drawRain(Canvas canvas, Size size) {
weatherPrint(
"開始繪製雨層 image:${_state._images?.length}, rains:${_state._rainSnows?.length}");
if (_state._images != null && _state._images.length > 1) {
ui.Image image = _state._images[0];
if (_state._rainSnows != null && _state._rainSnows.isNotEmpty) {
_state._rainSnows.forEach((element) {
move(element);
ui.Offset offset = ui.Offset(element.x, element.y);
canvas.save();
canvas.scale(element.scale, element.scale);
var identity = ColorFilter.matrix(<double>[
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, element.alpha, 0,
]);
_paint.colorFilter = identity;
canvas.drawImage(image, offset, _paint);
canvas.restore();
});
}
}
}
這裏繪製邏輯只用到了 drawImage 的方法,參數分別爲圖片、位置和畫筆,不像 Android 提供了 paint.setAlpha() 的方法控制圖片的透明值,這裏需要通過 colorFilter 修改矩陣中對應的值來控制 alpha。
move() 函數用於控制雨滴在運動過程中 x和y 值的不斷變化。
void move(RainSnowParams params) {
params.y = params.y + params.speed;
if (WeatherUtil.isSnow(_state.widget.weatherType)) {
double offsetX = sin(params.y / (300 + 50 * params.alpha)) * (1 + 0.5 * params.alpha);
params.x += offsetX;
}
if (params.y > 800 / params.scale) {
params.y = 0;
if (WeatherUtil.isRainy(_state.widget.weatherType) &&
_state._images.isNotEmpty &&
_state._images[0] != null) {
params.y = -_state._images[0].height.toDouble();
}
params.reset();
}
}
該方法每次重繪時都會調用,即 y += speed
根據 speed 不斷修改 y 的屬性,因爲雪的特殊性,x 會通過 sin 函數運算後得出。以及當雨滴超過屏幕需要重新歸位並重新初始化。
到此, 雨雪的繪製和動畫邏輯已經講述結束,是不是很簡單,但是效果上還是相當酷炫的,感興趣的可以到 SimplicityWeather 下載進行查看更多效果。最後再看看大雨下的效果。