flutter通過widget組合的方式實現“陰晴雨雪”

安卓小哥發表於2019-07-24

開頭

在flutter中,我們可以通過 AnimationController 及各種 Animation 搭配使用的方式去實現 Widget 的動畫。

實現的方式也非常方便,通過flutter內建好的模版程式碼,在你建立的dart檔案中輸入 sta 即可建立出基本的動畫模版類。

那麼,我們可以通過這樣的Widget組合方式,實現出怎樣的動畫呢?

image

接下來,我們就以上面的動畫為例子,講一講Widget強大的組合性!

Widget 組合

由簡到難,我們依次開始組合出上面的效果。

晴天動畫是最簡單的,就是一個太陽360度不停旋轉的效果

首先,通過模版程式碼 sta 建立出一個 WeatherSunny 類,初始化的 controlleranimation 分別如下


  AnimationController _controller;
  Animation _animation;

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 60),
    );
    _animation = Tween(begin: 0.0, end: 1.0).animate(_controller);
    ...
    }
複製程式碼

為了達到太陽不停旋轉的效果,我們需要把動畫設定成迴圈的,所以需要監聽它的狀態

  @override
  void initState() {
    ...
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reset();
        _controller.forward();
      }
    });
    _controller.forward();
    super.initState();
  }
複製程式碼

由於動畫需要進行Widget的重新整理,所以我們通常需要進行下面的操作:

    _controller.addListener((){
      setState(() {});
    });
複製程式碼

但是對於複雜度不高的動畫,我們可以使用 AnimatedBuilder 去降低程式碼行數,所以在這裡上面的監聽重新整理就沒有必要了

然後是將 Animation 應用在 Widget 上

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (ctx, child) {
        return Container(
          decoration: BoxDecoration(border: Border.all()),
          child: Transform.rotate(
            angle: pi * 2 * _animation.value * 5,
            child: child,
          ),
        );
      },
      child: Icon(
        Icons.wb_sunny,
        size: widget.sunnySize,
        color: widget.sunColor,
      ),
    );
  }
複製程式碼

這裡的太陽其實就是flutter預設提供的Icon,我們讓它每60s旋轉 360 * 5 的度數,也就是每60s 轉5圈。

到這裡也許有同學會問,為什麼不將 Duration 設定成12s,旋轉度數設定成 360 ,效果不是一樣嗎?

效果確實一樣,不過靈活度是不一樣的,等你實際操作一遍就可以體會到了。

image

晴天動畫非常簡單,實際上就是 旋轉動畫 + Icon 的組合

那麼陰天動畫如何實現呢,應該很多同學已經知道了,就是 晴天動畫 + Stack 的組合

首先我們將之前的 WeatherSunny 封裝好,讓它可以從外部傳入某些引數

WeatherSunny({
    this.sunnySize = 100,
    this.sunColor = Colors.orange,
    ...
  })
複製程式碼

然後我們建立一個 WeatherCloudy 去實現陰天動畫,這裡的陰天動畫不需要額外的動畫操作,所以不用將其建立成 StatefulWidget

  @override
  Widget build(BuildContext context) {
    ...
    return Container(
      width: width,
      height: height,
      child: Stack(
        children: <Widget>[
          Positioned(
            left: sunOrigin.dx + cloudSize / 6,
            top: sunOrigin.dy - cloudSize / 6,
            child: WeatherSunny(
              sunnySize: sunSize,
              sunColor: sunColor,
            ),
          ),
          Positioned(
            left: cloudOrigin.dx,
            top: cloudOrigin.dy,
            child: Icon(
              Icons.cloud,
              size: cloudSize,
              color: cloudColor,
            ),
          ),
        ],
      ),
    );
  }
複製程式碼

上面省去了很多細節程式碼,可以看到陰天的動畫就是通過 Stack 組合 晴天動畫 與另外一個 雲朵Icon,只不過我們需要計算各個物件的相對座標

image

落雨的動畫稍微要複雜一些,因為雨點的生成都是隨機的,所以需要使用到 Random()

在實現之前可以先思考一下,雨點是用什麼去實現的?

也許有小夥伴早就知道了,就是通過 Container 去實現的雨點

Container(
          width: randomWidth,
          height: randomHeight,
          decoration: BoxDecoration(
              borderRadius: BorderRadius.all(Radius.circular(randomWidth / 2)),
              gradient: LinearGradient(
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                  colors: [
                    Colors.white, Theme.of(context).primaryColor,
                  ])),
        )
複製程式碼

Container可以實現的效果很豐富,冒充雨點也是不在話下

接下來,就是如何展示出這麼多的雨點。

顯然,是通過 Stack + N個Position 的結合方式

我們可以建立出隨機數量的 Container 雨點展示,然後在 Position 中設定他們的隨機座標

      //雨滴隨機大小
      final randomWidth = Random().nextDouble() * width / 50 + 1;
      final randomHeight = Random().nextDouble() * height / 10;
      //雨滴隨機座標
      double randomL = Random().nextDouble() * width - randomWidth;
      double randomT = Random().nextDouble() * height + randomHeight;
複製程式碼

不過又有一個問題來了,如何實現雨滴動畫無限向下移動呢?

首先肯定是需要讓動畫無限迴圈的

        _controller.reset();
        _controller.forward();
複製程式碼

讓雨滴移動通過 Transform.translate 即可

 Transform.translate(
              offset: Offset(
                0,
                _animation.value * widget.droppingHeight,
              ),
              child: child,
            ),
          );
複製程式碼

實際上的動畫應該上這個樣子

image

所以還剩下一個問題,如何保證雨滴不出邊界?

這裡就需要用到另一個控制元件 ClipRect

通過 ClipRectclipper 屬性,我們可以對顯示區域進行限制,接下來自定義一個 CustomClipper

class CustomRect extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) {
    Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);
    return rect;
  }

  @override
  bool shouldReclip(CustomRect oldClipper) {
    return false;
  }
}
複製程式碼

這樣,我們就可以把顯示內容限制在 rect 的範圍內

大概的程式碼如下

  Widget build(BuildContext context) {
    final children =
        getDroppingWidget(widget.droppingHeight, widget.droppingWidth, context);

    return Container(
      width: widget.droppingWidth,
      height: widget.droppingHeight,
      decoration: BoxDecoration(border: Border.all()),
      child: AnimatedBuilder(
        animation: _animation,
        builder: (ctx, child) {
          return ClipRect(
            clipper: CustomRect(),
            child: Transform.translate(
              offset: Offset(
                0,
                _animation.value * widget.droppingHeight,
              ),
              child: child,
            ),
          );
        },
        child: Stack(
          children: [
            Transform.translate(
              offset: Offset(0, -widget.droppingHeight),
              child: Stack(
                children: children,
              ),
            ),
            Stack(
              children: children,
            ),
          ],
        ),
      ),
    );
  }
複製程式碼

image

下雪的動畫與下雨的動畫是一樣的,只是將實現 雨滴 的Widget替換為 飄雪 的Widget

Container(
          width: width,
          height: width,
          decoration: BoxDecoration(
              shape: BoxShape.circle,
              gradient: LinearGradient(
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                  colors: [
                    Colors.white,
                    Theme.of(context).primaryColor,
                  ])),
        );
複製程式碼

image

最後還有 雨雪 + 雲 的動畫,具體實現方式與 晴 + 雲 的效果是差不多的,只是需要進行位置的計算有所不同

那麼,通過 widget 組合實現一些動畫效果就到此為止,可以看到在flutter 中 萬物基於widget 絕非空口無憑,

附錄

demo地址如下:

【weather_animation_demo】

(ps:demo中我將控制元件進行了封裝,可以很方便的呼叫,本來是打算寫成一個dart package的,後來覺得效果比較簡單,還是用作學習素材最為合適!

封裝後,通過 droppingType 引數來控制下降的是與還是雪,通過 droppingLevel 引數控制雨雪的數量。 也可以通過 droppingWidget 引數來自定義下落的控制元件。 )

相關文章