本文由雲+社群發表
目的
寫了幾個Flutter的demo,但是對Flutter的自定義view和動畫都不太瞭解,看到一個類似效果在android的實現,就嘗試用Flutter做一下。同時也是學習Flutter的自定義view和動畫相關的知識。
效果
效果動圖
在藍色區域點選,會產品水波紋動畫。
宛如水珠落在池塘,雨滴落在青青草地~
思路
動畫很簡單,雖然有多個雨滴,不過每次點選都是重複的動畫,所以只用管一個雨滴動畫是怎麼實現的,其他的都是重複。
單獨來看一個雨滴動畫,其實就是一個圓圈慢慢的變大同時慢慢的變淺,最後消失。
所以我們封裝一套上述的動畫邏輯,然後在使用者每次點選時生成一個相應的動畫即可。
實現
自定義view
首先我們要解決的是自定義view的問題,我們知道Flutter中的一起UI皆Flutter,但是不同於android中的View會直接提供一個draw方法讓你做自由的繪製操作。在Flutter中,除了StatefuleWidget等申明瞭支援繼承的類外,其他的都是不建議繼承重寫的。如要要做一個新的Widget,官方建議是通過組合Widget來實現。
當然對於我們這裡這種需要自己做繪製操作的,就不是組合可以解決的了,這種情況下,Flutter提供了CustomPainter
類,這個類提供了paint方法,可以通過重寫該方法,實現對canvas的繪製。然後作為CustomPaint
的引數,控制該Widget的展示樣式。
這裡由於主要的繪製是水紋,要實現多個重複動畫,所以具體的繪製邏輯封裝了起來
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();
});
}
// ...
}
複製程式碼
水紋圈的繪製
每一個水紋的動畫都是一樣的,所以統一封裝了起來。
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重新整理原理。
後面會梳理一下這類原理知識,否則還是有點擔憂複雜動畫按這種寫法是否會卡頓。
此文已由作者授權騰訊雲+社群釋出