使用Flutter實現58同城中的加載動畫


在應用中執行耗時操作時,為了避免界面長時間等待造成假死的現象,往往會添加一個加載中的動畫來提醒用戶,在58同城中也不例外,而且我們並沒有使用系統默認的加載動畫,而是制作了一個具有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的綁定。

PPT模板下載大全https://www.wode007.com

加載動畫的實現

了解了Flutter的動畫后,再結合之前對加載動畫流程的分析,加載動畫可分成三個階段,我們可以依賴Tween類,指定值的范圍從0.0到3.0變化,當然也可以只使用AnimationController,指定lowerBound和upperBound的值分別為0.0和3.0。這里之所以不使用CurvedAnimation,是因為加載動畫的圓弧是線性變化的,不存在加速減速,沒有必要使用。

大圓弧能夠實現了,我們再來看內部的小圓弧,仔細觀察會發現小圓弧的變化規律與大圓弧完全一致,只不過小圓弧的起始位置在x軸負方向,與大圓弧正好相差180度,也就是π弧度。在繪制大圓弧的同時,可以很輕松的計算出小圓弧的起點的角度(即大圓弧起點的角度+π弧度)。

至此整個動畫的實現思路就清晰了:

  1. 自定義加載動畫的Widget,繼承自CustomPaint類。

  2. 使用AnimationController、Tween創建動畫,動畫的值范圍從0.0到3.0線性變化,並且設置動畫重復執行。動畫插值每遞增1.0代表動畫執行的一個階段。

  3. 繼承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等,有需要的同學可以查找相關資料。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM