『Flutter-繪制篇』實現(xiàn)炫酷的雨雪特效

前言

前不久,利用周末時間學(xué)習(xí)并完成一個簡單的 Flutter 項目 - 簡悅天氣簡約不簡單,豐富不復(fù)雜,這是一款簡約風(fēng)格的 flutter 天氣項目,提供實時、多日、24 小時、臺風(fēng)路徑以及生活指數(shù)等服務(wù),支持定位、刪除、搜索等操作。

下圖為主頁效果:

圖1

開始

項目中很多自定義 widget,今天的主角是 背景層,不同的天氣氣象有不一樣的呈現(xiàn)效果,一共實現(xiàn)了 12 種類別,其中有 晴、多云、陰天、小中大雨、小中大雪、霧、霾、浮塵,而背景層又分為三層:

  • 背景顏色層。從上到下的漸變效果
  • 云層。只有一種圖片,對其位移、數(shù)量、染色做不同變化達(dá)到不同效果
  • 雨雪層。為雨雪天氣單獨做了動畫,很炫酷。

好,真正的主角就是這個雨雪層,為了更好的預(yù)覽效果,在關(guān)于頁面有上角添加切換天氣類型的入口,實時查看不同氣象下不同的背景效果。如下圖,為雨雪的最終效果(gif 效果看起來會失真,請下載 apk 自行體驗):

圖5
圖6

不得不說,如此復(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)行查看更多效果。最后再看看大雨下的效果。

大雨特效
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容