您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關怎么使用Flutter實現58同城中的加載動畫的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
使用Flutter實現58同城中加載動畫的過程,先看一下加載動畫的效果:
動畫效果乍看比較復雜,難以看出端倪,其實我們可以先調慢動畫的速度,這樣能夠比較清晰地分析出動畫的流程。
動畫的流程
動畫由兩個圓弧的動效組成,兩個圓弧的起始點角度和掃過的弧度隨著時間規律變化。仔細觀察會發現,兩個圓弧的動效其實是一樣的,只不過起始位置是不一樣的。我們先看下外部大圓弧的運動規律。
大圓弧從x軸正方向開始運動,按照動畫的運動規律,可以將動畫分為三個階段:
第一階段:圓弧起點的在x軸正方向,終點的角度x軸正方向開始向下逐漸增大,直到終點到達y軸負方向位置,最終圓弧掃過的角度為180度。
第二階段:圓弧掃過的角度保持在180度,起點和終點一起順時針旋轉,直到旋轉180度后終點到達x軸正方向。
第三階段:圓弧的終點保持在x軸正方向,起點順時針旋轉,直到起點也到達x軸正方向,此時完成一個完整的動畫。接下來繼續重復動畫的第一階段,組成一個連貫的動畫。
分析完動畫的流程,思路就很清晰了,我們按照動畫流程把動畫拆分成三部分,通過對圓弧的起點、終點和掃過角度的變換,組合成一個完整的動畫,然后不斷地重復,最后就變成了一個加載中的動畫效果。
接下來開始寫代碼實現。
由于動畫是由一個圓弧不斷變化組成的,如果使用Android,我們很自然的想到可以使用Canvas來進行圓弧的繪制,然后根據時間的變化不停地重新繪制圓弧,從而實現動畫效果。那么在Flutter中是否也存在Canvas呢,答案是肯定的,Flutter和Android一樣,也存在Canvas。
Flutter中的Canvas
Flutter中使用 CustomPainter 類在Canvas上進行繪制,該類包含一個 paint() 方法,該方法提供了一個Canvas對象,可以用來繪制各種圖形。
abstract class CustomPainter extends Listenable { void paint(Canvas canvas, Size size); }
不過在Flutter中一切皆是Widget,而承載Canvas功能的Widget是 CustomPaint 類。 CustomPaint 包含一個painter屬性,用來指定進行繪制的 CustomPainter,源碼如下:
class CustomPaint extends SingleChildRenderObjectWidget { const CustomPaint({ Key key, this.painter, }); final CustomPainter painter; }
Flutter中的Canvas和Android類似,提供了一系列的API用來繪制點、線、圓形、正方形等,而且API很類似,對比一下Flutter與Android中Canvas的常見API(具體的參數列表請參考文檔和源碼,篇幅有限不再一一列出):
Android | Flutter | |
---|---|---|
點 | drawPoint() drawPoints() | drawPoints() |
線 | drawLine() drawLines() | drawLine() |
圓 | drawCircle() | drawCircle() |
橢圓 | drawOval() | drawOval() |
圓弧 | drawArc() | drawArc() |
矩形 | drawRect() | drawRect() |
Path | drawPath() | drawPath() |
圖片 | drawBitmap() | drawImage() |
文字 | drawText() | drawParagraph() |
變換 | save() restore() | save() restore() |
要繪制動畫中的圓弧,應該使用 drawArc() 方法來實現,這里需要注意的是drawArc()方法的參數:startAngle和sweepAngle的單位是弧度(180度等于π弧度)。
具體來看一下 Canvas.drawArc() 方法的參數列表:
/// rect: 圓弧四周范圍所形成的矩形,在本篇中圓弧為圓形,可以使用Rect.fromCircle()確定圓弧的范圍 /// startAngle: 圓弧起始點的角度,x軸正方向為0度,按順時針遞增,y軸負方向為90度,以此類推 /// sweepAngle: 圓弧掃過的角度,即圓弧終點所在的角度為startAngle + sweepAngle /// useCenter: 如果為true,圓弧兩端會與圓心相連,形成一個扇形,本篇中應為false /// paint: 畫筆,下文中會進行簡單介紹 void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
在Canvas的一系列方法中會發現一個熟悉的名稱:Paint,與Android類似,Flutter中的Paint類也是用來描述畫筆的。
Paint類
Paint類位于 dart.ui 庫中,Paint類保存了畫筆的顏色、粗細、是否抗鋸齒、著色器等屬性。
下面簡單的介紹下幾個常用的屬性:
Paint paint = Paint() ..color = Color(0xFFFF552E) ..strokeWidth = 2.0 ..style = PaintingStyle.stroke ..isAntiAlias = true ..shader = LinearGradient(colors: []).createShader(rect) ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.bevel;
屬性說明:
color:Color類型,設置畫筆的顏色。
strokeWidth:double類型,設置畫筆的粗細。
style:PaintingStyle枚舉類型,設置畫筆的樣式, PaintingStyle.stroke 為描邊, PaintingStyle.fill 為填充。
isAntiAlias:bool類型,設置是否抗鋸齒,true為開啟抗鋸齒。
shader:Shader類型,著色器,一般用來繪制漸變效果,可以使用 LinearGradient、 RadialGradient、 SweepGradient 等。
strokeCap:StrokeCap枚舉類型,設置線條兩端點的樣式, StrokeCap.butt 為無(默認值), StrokeCap.round 為圓形, StrokeCap.square 為方形。
strokeJoin:StrokeJoin枚舉類型,設置線條交匯處的樣式, StrokeJoin.miter 為銳角, StrokeJoin.round 為圓弧, StrokeJoin.bevel 為斜角,可以參考下圖方便理解:
熟悉了Canvas和Paint的使用之后,就能夠繪制出加載動畫的圓弧了。當然,只是繪制出圓弧并沒有什么用,主要是怎么讓圓弧動起來。
Flutter中的動畫 想要讓圓弧動起來,我們需要使用到Flutter的動畫。下面先來介紹下Flutter中動畫的實現。 Flutter中的動畫相關的類主要有以下幾個: Animation:動畫的核心類,是一個抽象類。用來生成動畫執行過程中的插值,輸出的結果可以是線性或曲線的,Animation對象與UI渲染沒有任何關系。 abstract class Animation<T> extends Listenable implements ValueListenable<T> { /// 添加動畫狀態的監聽 void addStatusListener(AnimationStatusListener listener); /// 移除動畫狀態的監聽 void removeStatusListener(AnimationStatusListener listener); /// 獲取當前動畫的狀態 AnimationStatus get status; /// 獲取當前動畫的插值,執行動畫時需要根據該值進行UI繪制等 T get value; }
AnimationController:動畫的管理類,繼承自 Animation<double>。默認情況下在給定的時間范圍內線性生成從0.0到1.0的值。
AnimationController對象需要傳遞一個vsync參數,它接收一個TickerProvider類型的對象,主要職責是創建Ticker。Flutter應用在啟動時會綁定一個SchedulerBinding,可以給每一次屏幕刷新添加回調,Ticker就是通過SchedulerBinding來添加屏幕刷新的回調,當屏幕刷新時,會通知到綁定的Ticker回調。假如動畫的UI不在當前屏幕,比如鎖屏時,鎖屏后屏幕停止刷新,不會通知SchedulerBinding,Ticker也就不會觸發,這樣就能夠防止屏幕外的動畫消耗不必要的資源。
class AnimationController extends Animation<double> with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { /// value:動畫的初始值,默認是lowerBound /// duration:動畫執行的時長 /// lowerBound:動畫的最小值,默認值為0.0 /// upperBound:動畫的最大值,默認值為1.0 /// vsync:可以通過 `with SingleTickerProviderStateMixin` 傳入StatefulWidget對象 AnimationController({ double value, this.duration, this.lowerBound = 0.0, this.upperBound = 1.0, @required TickerProvider vsync, }) { _ticker = vsync.createTicker(_tick); } Ticker _ticker; /// Ticker的回調,每次屏幕刷新都會回調 void _tick(Duration elapsed) { notifyListeners(); } /// 開始播放動畫 TickerFuture forward({ double from }) /// 反向播放動畫 TickerFuture reverse({ double from }) /// 設置動畫重復執行 TickerFuture repeat({ double min, double max, bool reverse = false, Duration period }) /// 釋放動畫資源 void dispose() }
CurvedAnimation:非線性動畫類,繼承自 Animation<double>。CurvedAnimation可以使用curve屬性指定曲線函數Curve,類似Android動畫的插值器,Flutter中已經實現了許多常用的曲線,在Curves類中可以找到,比如Curves.linear、Curves.decelerate、Curves.ease。也可以繼承Curve類重寫 transform() 方法來實現自定義的曲線函數。
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> { /// parent:指定AnimationController對象 /// curve:指定動畫的曲線函數 CurvedAnimation({ @required this.parent, @required this.curve, }) } abstract class Curve { /// 計算動畫執行中`t`點的插值,可以自定義曲線函數 double transform(double t) }
Tween:補間值的生成類,繼承自 Animatable<T>。
由于AnimationController的值范圍默認為0.0到1.0,如果需要不同的范圍或數據類型,可以使用Tween指定動畫值的范圍。Tween不僅能返回double類型的值,還有IntTween、ColorTween、SizeTween等各種返回不同數據類型的子類。
使用Tween對象需要調用 animate() 方法,傳入AnimationController對象,該方法會返回一個Animation,這樣就可以獲取到動畫的插值了。
class Tween<T extends dynamic> extends Animatable<T> { /// begin:動畫的起始值 /// end:動畫的結束值 Tween({ this.begin, this.end }); /// 可以把double類型的動畫插值轉換成任何類型的值 T transform(double t) /// parent:傳入AnimationController對象 /// 返回Animation對象,使用Animation.value獲取動畫當前的插值 Animation<T> animate(Animation<double> parent) }
AnimatedBuilder:用于構建動畫的Widget,將動畫和要執行動畫的Widget關聯起來,繼承關系為AnimatedBuilder → AnimatedWidget → StatefulWidget。
class AnimatedBuilder extends AnimatedWidget { const AnimatedBuilder({ @required Listenable animation, @required this.builder, }); /// typedef TransitionBuilder = Widget Function(BuildContext context, Widget child); /// builder是一個函數,返回Widget對象 final TransitionBuilder builder; @override Widget build(BuildContext context) { return builder(context, child); } } abstract class AnimatedWidget extends StatefulWidget { const AnimatedWidget({ @required this.listenable, }); @protected Widget build(BuildContext context); @override _AnimatedState createState() => _AnimatedState(); } class _AnimatedState extends State<AnimatedWidget> { @override void initState() { super.initState(); widget.listenable.addListener(_handleChange); } @override void dispose() { widget.listenable.removeListener(_handleChange); super.dispose(); } void _handleChange() { setState(() { }); } @override Widget build(BuildContext context) => widget.build(context); }
分析上面列出的源碼,AnimatedWidget是一個StatefulWidget。當AnimatedWidget關聯的_AnimatedState初始化時,會注冊動畫的監聽函數_handleChange,_handleChange監聽函數中又調用了setState()方法,即動畫插值每次改變時都會調用build()方法。_AnimatedState.build()方法中又調用了AnimatedWidget.build()方法,在AnimatedBuilder中實現了AnimatedWidget.build()方法:調用屬性builder生成Widget,最終實現了動畫與Widget的綁定。
加載動畫的實現
了解了Flutter的動畫后,再結合之前對加載動畫流程的分析,加載動畫可分成三個階段,我們可以依賴Tween類,指定值的范圍從0.0到3.0變化,當然也可以只使用AnimationController,指定lowerBound和upperBound的值分別為0.0和3.0。這里之所以不使用CurvedAnimation,是因為加載動畫的圓弧是線性變化的,不存在加速減速,沒有必要使用。
大圓弧能夠實現了,我們再來看內部的小圓弧,仔細觀察會發現小圓弧的變化規律與大圓弧完全一致,只不過小圓弧的起始位置在x軸負方向,與大圓弧正好相差180度,也就是π弧度。在繪制大圓弧的同時,可以很輕松的計算出小圓弧的起點的角度(即大圓弧起點的角度+π弧度)。
至此整個動畫的實現思路就清晰了:
自定義加載動畫的Widget,繼承自CustomPaint類。
使用AnimationController、Tween創建動畫,動畫的值范圍從0.0到3.0線性變化,并且設置動畫重復執行。動畫插值每遞增1.0代表動畫執行的一個階段。
繼承CustomPainter類,實現paint()方法繪制圓弧。根據動畫的插值判斷當前屬于動畫的哪個階段,再計算出圓弧的起點、掃過的角度,繪制出兩個圓弧。
下面是實現加載動畫的關鍵代碼:
import 'dart:math'; import 'package:flutter/material.dart'; class WubaLoadingWidget extends StatefulWidget { @override _WubaLoadingWidgetState createState() => _WubaLoadingWidgetState(); } class _WubaLoadingWidgetState extends State<WubaLoadingWidget> with SingleTickerProviderStateMixin { AnimationController _animationController; Animation<double> _animation; @override void initState() { super.initState(); _animationController = new AnimationController( // 可以指定lowerBound、upperBound,使用AnimationController對象 // lowerBound: 0.0, // upperBound: 3.0, vsync: this, duration: const Duration(milliseconds: 1500), ); _animation = Tween(begin: 0.0, end: 3.0) .animate(_animationController); _animationController.forward(); // 執行動畫 _animationController.repeat(); // 設置動畫循環執行 } @override void dispose() { // 調用dispose()方法釋放動畫資源 _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animationController, builder: (BuildContext context, Widget child) { return Container( child: CustomPaint( painter: _LoadingPaint( value: _animation.value, ), ), ); }, ); } } class _LoadingPaint extends CustomPainter { final double value; final Paint _outerPaint; // 大圓弧的Paint final Paint _innerPaint; // 小圓弧的Paint _LoadingPaint({ this.value, }); @override void paint(Canvas canvas, Size size) { double startAngle = 0; double sweepAngle = 0; // 動畫的第一階段:圓弧起點為0度,終點的角度遞增 if (value <= 1.0) { startAngle = 0; sweepAngle = value * pi; } // 動畫的第二階段:圓弧掃過的弧度為π弧度(180度),起點、終點一起順時針旋轉,一共旋轉π弧度 else if (value <= 2.0) { startAngle = (value - 1) * pi; sweepAngle = pi; } // 動畫的第三階段:圓弧的終點不變,起點從x軸負方向開始順時針旋轉,直到起點也到達x軸正方向 else { startAngle = pi + (value - 2) * pi; sweepAngle = (3 - value) * pi; } // 繪制外圈的大圓弧 canvas.drawArc(outerRect, startAngle, sweepAngle, false, _outerPaint); // 繪制內圈的小圓弧 canvas.drawArc(innerRect, startAngle + pi, sweepAngle, false, _innerPaint); } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } }
總結
Flutter的Canvas、Paint與Android的API非常類似,基本的思路也一致,對于Android同學比較容易掌握。
Flutter中動畫的實現相較于Android邏輯更加清晰簡單,方便易用。AnimatedBuilder類巧妙的將UI與動畫整合在一起,把UI和動畫職責分離,這種思路值得學習。Flutter中的動畫還有路由過渡動畫、Hero動畫、切換動畫組件AnimatedSwitcher等,有需要的同學可以查找相關資料。
感謝各位的閱讀!關于“怎么使用Flutter實現58同城中的加載動畫”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。