前言
前不久,利用周末時間學(xué)習(xí)并完成一個簡單的 Flutter 項目 - 簡悅天氣,簡約不簡單,豐富不復(fù)雜,這是一款簡約風(fēng)格的 flutter 天氣項目,提供實時、多日、24 小時、臺風(fēng)路徑以及生活指數(shù)等服務(wù),支持定位、刪除、搜索等操作。
下圖為主頁效果:
開始
項目中很多自定義 widget,今天的主角是 背景層,不同的天氣氣象有不一樣的呈現(xiàn)效果,一共實現(xiàn)了 12 種類別,其中有 晴、多云、陰天、小中大雨、小中大雪、霧、霾、浮塵,而背景層又分為三層:
- 背景顏色層。從上到下的漸變效果
- 云層。只有一種圖片,對其位移、數(shù)量、染色做不同變化達(dá)到不同效果
- 雨雪層。為雨雪天氣單獨做了動畫,很炫酷。
好,真正的主角就是這個雨雪層,為了更好的預(yù)覽效果,在關(guān)于頁面有上角添加切換天氣類型的入口,實時查看不同氣象下不同的背景效果。如下圖,為雨雪的最終效果(gif 效果看起來會失真,請下載 apk 自行體驗):
不得不說,如此復(fù)雜的動畫(復(fù)雜并不是指多難實現(xiàn),而是不停的繪制很多圖片下),F(xiàn)lutter 還能有不錯的性能表現(xiàn),媲美原生效果。
效果實現(xiàn)
這里不贅述繪制和動畫相關(guān)知識,網(wǎng)上已經(jīng)有很多文章介紹,本篇只針對項目中用到的實現(xiàn)方式和相關(guān)知識進(jìn)行講述,具有一定的局限性,適合簡單的繪制動畫邏輯。
創(chuàng)建繪制類
因為 Flutter 處處是 widget,自定義 View 需要用到的是 CustomPaint,而成員變量中需要傳入實現(xiàn) CustomPainter 的類,那咱們先創(chuàng)建此類。
class RainSnowPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
有沒有很熟悉,看到了 Canvas 的類,自然也有 Paint 類,有了畫筆和畫板,剩下就好辦了。
構(gòu)造雨雪對象
對需要實現(xiàn)的效果進(jìn)行分析,首先雨雪效果是由一張圖片不同屬性拼接而成,每個雨滴和雪花落實在屏幕上,必須有 x,y 的坐標(biāo)屬性。為了營造遠(yuǎn)近的效果,需要加上 scale 值,由于更加還原真實的視覺效果,雨滴的遠(yuǎn)近,必然速度上和清晰度上會有差異,因此加上 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);
}
屬性初始化
有了屬性后,接下來就是對屬性進(jìn)行賦值,為了保證效果更加的還原,所有屬性既要有規(guī)則,又要隨機(jī)。怎么解釋規(guī)則性和隨機(jī)性都要同時擁有,就拿雨速而言,小雨相對于大雨,雨的速度稍慢,但是不能很慢,并且每滴雨滴的速度不一樣,這就致使小雨的速度必然在一個區(qū)間下隨機(jī),同樣雪也一樣。
初始化又分成兩步,第一次的初始化和雨滴下落結(jié)束后的數(shù)據(jù)重置,實際上兩者的區(qū)別只在于 y。第一次初始化 y 在屏幕高度中隨機(jī)放置,而雨滴下落結(jié)束后,y 值置為0。那么就可以把重置邏輯封裝統(tǒng)一的方法。
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 代表浮動值,這兩個根據(jù)雨雪量大小而做不同區(qū)分。通過 Random().nextDouble()獲取隨機(jī) [0.0, 1.0] 的值,random * gap + init 就是最終的值。
x 的屬性控制在 [-0.1*width, 1.1 width]的區(qū)間內(nèi)隨機(jī),y 值上面已提到。
雪相對有雨有個不同,雨是垂直下落,而雪是隨風(fēng)搖擺,那為了營造這種感覺,此時就需要借助 sin 函數(shù)。
if (WeatherUtil.isSnow(_state.widget.weatherType)) {
double offsetX = sin(params.y / (300 + 50 * params.alpha)) * (1 + 0.5 * params.alpha);
params.x += offsetX;
}
開始繪制
終于到了最重要的步驟 繪制,但他并不是最難的,有了前面創(chuàng)建好的屬性和對其初始化,剩下就只是調(diào)用 api 進(jìn)行繪制即可。不過再此之前好像漏了什么沒說,沒錯,就是 動畫,一個無限循環(huán)的動畫。
Flutter 中創(chuàng)建動畫也很簡單,需要在動畫監(jiān)聽中,判斷如果動畫結(jié)束則重新繼續(xù)執(zhí)行即可。
1. 在 initState 函數(shù)中初始化 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 函數(shù)中釋放掉動畫資源
@override
void dispose() {
_controller.dispose();
super.dispose();
}
在初始化是便讓他執(zhí)行并一直執(zhí)行知道頁面銷毀,有了動畫后,開始進(jìn)行繪制,雨雪的繪制邏輯基本相似,只不過圖片源不一樣。
別看屏幕上有很多雨滴,其實只用了一直圖片,通過控制 alpha、speed和scale 的屬性來隨機(jī)展現(xiàn)不同的形態(tài)。還有,根據(jù)氣象大中小雨類型的區(qū)分,會直接落實到雨滴數(shù)量和雨滴形態(tài)上的變化,營造出多樣的差異。
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 的方法,參數(shù)分別為圖片、位置和畫筆,不像 Android 提供了 paint.setAlpha() 的方法控制圖片的透明值,這里需要通過 colorFilter 修改矩陣中對應(yīng)的值來控制 alpha。
move() 函數(shù)用于控制雨滴在運動過程中 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();
}
}
該方法每次重繪時都會調(diào)用,即 y += speed 根據(jù) speed 不斷修改 y 的屬性,因為雪的特殊性,x 會通過 sin 函數(shù)運算后得出。以及當(dāng)雨滴超過屏幕需要重新歸位并重新初始化。
到此, 雨雪的繪制和動畫邏輯已經(jīng)講述結(jié)束,是不是很簡單,但是效果上還是相當(dāng)酷炫的,感興趣的可以到 SimplicityWeather 下載進(jìn)行查看更多效果。最后再看看大雨下的效果。