『Flutter-繪製篇』實現炫酷的雨雪特效

前言

前不久,利用週末時間學習並完成一個簡單的 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 下載進行查看更多效果。最後再看看大雨下的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章