本文由雲+社區發表
寫了幾個Flutter的demo,可是對Flutter的自定義view和動畫都不太瞭解,看到一個相似效果在android的實現,就嘗試用Flutter作一下。同時也是學習Flutter的自定義view和動畫相關的知識。android
效果動圖canvas
在藍色區域點擊,會產品水波紋動畫。ide
宛如水珠落在池塘,雨滴落在青青草地~
動畫很簡單,雖然有多個雨滴,不過每次點擊都是重複的動畫,因此只用管一個雨滴動畫是怎麼實現的,其餘的都是重複。工具
單獨來看一個雨滴動畫,其實就是一個圓圈慢慢的變大同時慢慢的變淺,最後消失。性能
因此咱們封裝一套上述的動畫邏輯,而後在用戶每次點擊時生成一個相應的動畫便可。學習
首先咱們要解決的是自定義view的問題,咱們知道Flutter中的一塊兒UI皆Flutter,可是不一樣於android中的View會直接提供一個draw方法讓你作自由的繪製操做。在Flutter中,除了StatefuleWidget等申明瞭支持繼承的類外,其餘的都是不建議繼承重寫的。如要要作一個新的Widget,官方建議是經過組合Widget來實現。動畫
固然對於咱們這裏這種須要本身作繪製操做的,就不是組合能夠解決的了,這種狀況下,Flutter提供了CustomPainter
類,這個類提供了paint方法,能夠經過重寫該方法,實現對canvas的繪製。而後做爲CustomPaint
的參數,控制該Widget的展現樣式。this
這裏因爲主要的繪製是水紋,要實現多個重複動畫,因此具體的繪製邏輯封裝了起來spa
class RainDrop extends CustomPainter { RainDrop(this.rainList); List<RainDropDrawer> rainList = List(); // 雨點列表 Paint _paint = new Paint()..style = PaintingStyle.stroke; // 配置畫筆 @override void paint(Canvas canvas, Size size) { rainList.forEach((item) { item.drawRainDrop(canvas, _paint); // 實際的繪製邏輯 }); rainList.removeWhere((item) { // 移出無效對象 return !item.isValid(); }); } // ... }
每個水紋的動畫都是同樣的,因此統一封裝了起來。code
class RainDropDrawer { static const double MAX_RADIUS = 30; double posX; double posY; double radius = 5; RainDropDrawer(this.posX, this.posY); // (2) drawRainDrop(Canvas canvas, Paint paint) { // (1) double opt = (MAX_RADIUS - radius) / MAX_RADIUS; // (3) paint.color = Color.fromRGBO(0, 0, 0, opt); canvas.drawCircle(Offset(posX, posY), radius, paint); // (4) radius += 0.5; } bool isValid() { // (5) return radius < MAX_RADIUS; } }
註釋(1)處,上文提到的CustomPainter
會把canvas傳過來,在這裏完成單個水紋的繪製工做。
註釋(2)處,每一個水紋圈須要肯定的是位置,只要位置就好了,大小是隨着時間均勻擴大的,給默認起始值就行。
註釋(3)處,透明度是隨着半徑擴大而逐漸透明的,這裏簡單的作了線性的映射。
註釋(4)處,繪製水紋圈,而後讓水紋半徑自增,實現每次繪製擴大的效果。
註釋(5)處,給定失效的條件。超過必定半徑這個水紋就消失了。
Flutter中提供了不少的動畫實現,這裏用到的是AnimationController。
其實AnimationController在這裏就是提供了一個回調,每次收到vsync信號時回調作一次更新。
_animation = new AnimationController( // 由於是repeat的,這裏的duration其實不care duration: const Duration(milliseconds: 200), vsync: this) ..addListener(() { if (_rainList.isEmpty) { //(1) _animation.stop(); } setState(() {}); });
這裏的動畫是經過repeat啓動的,因此不用太關心duration,由於只要不手動關閉其實是會一直回調的。
vsync設置的是當前的widget,提供了一個ticker,會定時回調。而後在回調中setState
讓當前widget更新UI。
註釋(1)處是動畫中止的條件判斷,當每次點擊往_rainList
中加一個對象,每一個對象繪製會判斷大小是否有效,若是無效會被從列表中移出,當列表中沒有元素時就中止動畫。
上述基本實現了多個雨滴的展現和動畫,而後咱們要來實現對用戶點擊的響應。
Flutter提供了GestureDetector
這個widget來作手勢識別。因此咱們只須要用這個widget wrap住咱們的自定義view,而後實現對應的手勢監聽方法便可。
GestureDetector( onTapUp: (TapUpDetails tapUp) { RenderBox getBox = context.findRenderObject(); var localOffset = getBox.globalToLocal(tapUp.globalPosition); // (1) var rainDrop = RainDropDrawer(localOffset.dx, localOffset.dy); _rainList.add(rainDrop); _animation.repeat(); // (2) }, child: CustomPaint( painter: RainDrop(_rainList), ), ),
這裏咱們關注用戶輕點後擡起的手勢,這個監聽的方法會傳入TapUpDetails
參數,這個參數含有擡起的位置參數,可是須要注意的是,這個座標是全屏幕的座標,而繪製的座標是widget內的座標,因此咱們須要將這個座標轉換爲咱們widget內的座標系,Flutter提供了這樣的一個工具方法,參考註釋(1)
處的實現便可。
完成了座標換算,就能夠構建一個「雨點」對象,添加到List裏面。而後在註釋(2)
處啓動動畫,就能夠看到咱們文章開頭的動畫效果啦~
Flutter的動畫實現起來真的很簡單,提供一個差值回調,而後不停的更新便可。不過這裏暫時沒有考慮性能等問題,對setState
這個方法感受仍是很黑盒,不太懂Flutter具體的UI刷新原理。
後面會梳理一下這類原理知識,不然仍是有點擔心複雜動畫按這種寫法是否會卡頓。
此文已由做者受權騰訊雲+社區發佈