欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

如何使用Flutter實(shí)現(xiàn)58同城中的加載動(dòng)畫詳解

 更新時(shí)間:2019年10月14日 08:32:43   作者:吳振  
這篇文章主要給大家介紹了關(guān)于如何使用Flutter實(shí)現(xiàn)58同城中加載動(dòng)畫詳?shù)南嚓P(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Flutter具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

前言

在應(yīng)用中執(zhí)行耗時(shí)操作時(shí),為了避免界面長(zhǎng)時(shí)間等待造成假死的現(xiàn)象,往往會(huì)添加一個(gè)加載中的動(dòng)畫來(lái)提醒用戶,在58同城中也不例外,而且我們并沒(méi)有使用系統(tǒng)默認(rèn)的加載動(dòng)畫,而是制作了一個(gè)具有58特色的加載動(dòng)畫。

在本篇文章中,給大家分享下筆者使用Flutter實(shí)現(xiàn)58同城中加載動(dòng)畫的過(guò)程。先看一下加載動(dòng)畫的效果:

動(dòng)畫效果乍看比較復(fù)雜,難以看出端倪,其實(shí)我們可以先調(diào)慢動(dòng)畫的速度,這樣能夠比較清晰地分析出動(dòng)畫的流程。

動(dòng)畫的流程

動(dòng)畫由兩個(gè)圓弧的動(dòng)效組成,兩個(gè)圓弧的起始點(diǎn)角度和掃過(guò)的弧度隨著時(shí)間規(guī)律變化。仔細(xì)觀察會(huì)發(fā)現(xiàn),兩個(gè)圓弧的動(dòng)效其實(shí)是一樣的,只不過(guò)起始位置是不一樣的。我們先看下外部大圓弧的運(yùn)動(dòng)規(guī)律。

大圓弧從x軸正方向開(kāi)始運(yùn)動(dòng),按照動(dòng)畫的運(yùn)動(dòng)規(guī)律,可以將動(dòng)畫分為三個(gè)階段:

第一階段:圓弧起點(diǎn)的在x軸正方向,終點(diǎn)的角度x軸正方向開(kāi)始向下逐漸增大,直到終點(diǎn)到達(dá)y軸負(fù)方向位置,最終圓弧掃過(guò)的角度為180度。

第二階段:圓弧掃過(guò)的角度保持在180度,起點(diǎn)和終點(diǎn)一起順時(shí)針旋轉(zhuǎn),直到旋轉(zhuǎn)180度后終點(diǎn)到達(dá)x軸正方向。

第三階段:圓弧的終點(diǎn)保持在x軸正方向,起點(diǎn)順時(shí)針旋轉(zhuǎn),直到起點(diǎn)也到達(dá)x軸正方向,此時(shí)完成一個(gè)完整的動(dòng)畫。接下來(lái)繼續(xù)重復(fù)動(dòng)畫的第一階段,組成一個(gè)連貫的動(dòng)畫。

分析完動(dòng)畫的流程,思路就很清晰了,我們按照動(dòng)畫流程把動(dòng)畫拆分成三部分,通過(guò)對(duì)圓弧的起點(diǎn)、終點(diǎn)和掃過(guò)角度的變換,組合成一個(gè)完整的動(dòng)畫,然后不斷地重復(fù),最后就變成了一個(gè)加載中的動(dòng)畫效果。

接下來(lái)開(kāi)始寫代碼實(shí)現(xiàn)。

由于動(dòng)畫是由一個(gè)圓弧不斷變化組成的,如果使用Android,我們很自然的想到可以使用Canvas來(lái)進(jìn)行圓弧的繪制,然后根據(jù)時(shí)間的變化不停地重新繪制圓弧,從而實(shí)現(xiàn)動(dòng)畫效果。那么在Flutter中是否也存在Canvas呢,答案是肯定的,F(xiàn)lutter和Android一樣,也存在Canvas。

Flutter中的Canvas

Flutter中使用 CustomPainter 類在Canvas上進(jìn)行繪制,該類包含一個(gè) paint() 方法,該方法提供了一個(gè)Canvas對(duì)象,可以用來(lái)繪制各種圖形。

 abstract class CustomPainter extends Listenable {

 void paint(Canvas canvas, Size size);

 }

不過(guò)在Flutter中一切皆是Widget,而承載Canvas功能的Widget是 CustomPaint 類。 CustomPaint 包含一個(gè)painter屬性,用來(lái)指定進(jìn)行繪制的 CustomPainter,源碼如下:

 class CustomPaint extends SingleChildRenderObjectWidget {

 const CustomPaint({

 Key key,

 this.painter,

 });

 final CustomPainter painter;

 }

Flutter中的Canvas和Android類似,提供了一系列的API用來(lái)繪制點(diǎn)、線、圓形、正方形等,而且API很類似,對(duì)比一下Flutter與Android中Canvas的常見(jiàn)API(具體的參數(shù)列表請(qǐng)參考文檔和源碼,篇幅有限不再一一列出):


Android Flutter
點(diǎn)

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()


 

要繪制動(dòng)畫中的圓弧,應(yīng)該使用 drawArc() 方法來(lái)實(shí)現(xiàn),這里需要注意的是drawArc()方法的參數(shù):startAngle和sweepAngle的單位是弧度(180度等于π弧度)。

具體來(lái)看一下 Canvas.drawArc() 方法的參數(shù)列表:

 /// rect: 圓弧四周范圍所形成的矩形,在本篇中圓弧為圓形,可以使用Rect.fromCircle()確定圓弧的范圍

 /// startAngle: 圓弧起始點(diǎn)的角度,x軸正方向?yàn)?度,按順時(shí)針遞增,y軸負(fù)方向?yàn)?0度,以此類推

 /// sweepAngle: 圓弧掃過(guò)的角度,即圓弧終點(diǎn)所在的角度為startAngle + sweepAngle

 /// useCenter: 如果為true,圓弧兩端會(huì)與圓心相連,形成一個(gè)扇形,本篇中應(yīng)為false

 /// paint: 畫筆,下文中會(huì)進(jìn)行簡(jiǎn)單介紹

 void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)

在Canvas的一系列方法中會(huì)發(fā)現(xiàn)一個(gè)熟悉的名稱:Paint,與Android類似,F(xiàn)lutter中的Paint類也是用來(lái)描述畫筆的。

Paint類

Paint類位于 dart.ui 庫(kù)中,Paint類保存了畫筆的顏色、粗細(xì)、是否抗鋸齒、著色器等屬性。

下面簡(jiǎn)單的介紹下幾個(gè)常用的屬性:

 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;

屬性說(shuō)明:

  • color:Color類型,設(shè)置畫筆的顏色。
  • strokeWidth:double類型,設(shè)置畫筆的粗細(xì)。
  • style:PaintingStyle枚舉類型,設(shè)置畫筆的樣式, PaintingStyle.stroke 為描邊, PaintingStyle.fill 為填充。
  • isAntiAlias:bool類型,設(shè)置是否抗鋸齒,true為開(kāi)啟抗鋸齒。
  • shader:Shader類型,著色器,一般用來(lái)繪制漸變效果,可以使用 LinearGradient、 RadialGradient、 SweepGradient 等。
  • strokeCap:StrokeCap枚舉類型,設(shè)置線條兩端點(diǎn)的樣式, StrokeCap.butt 為無(wú)(默認(rèn)值), StrokeCap.round 為圓形, StrokeCap.square 為方形。
  • strokeJoin:StrokeJoin枚舉類型,設(shè)置線條交匯處的樣式, StrokeJoin.miter 為銳角, StrokeJoin.round 為圓弧, StrokeJoin.bevel 為斜角,可以參考下圖方便理解:

熟悉了Canvas和Paint的使用之后,就能夠繪制出加載動(dòng)畫的圓弧了。當(dāng)然,只是繪制出圓弧并沒(méi)有什么用,主要是怎么讓圓弧動(dòng)起來(lái)。

Flutter中的動(dòng)畫

想要讓圓弧動(dòng)起來(lái),我們需要使用到Flutter的動(dòng)畫。下面先來(lái)介紹下Flutter中動(dòng)畫的實(shí)現(xiàn)。

Flutter中的動(dòng)畫相關(guān)的類主要有以下幾個(gè):

 Animation:動(dòng)畫的核心類,是一個(gè)抽象類。用來(lái)生成動(dòng)畫執(zhí)行過(guò)程中的插值,輸出的結(jié)果可以是線性或曲線的,Animation對(duì)象與UI渲染沒(méi)有任何關(guān)系。

 abstract class Animation<T> extends Listenable implements ValueListenable<T> {

  /// 添加動(dòng)畫狀態(tài)的監(jiān)聽(tīng)

  void addStatusListener(AnimationStatusListener listener);


  /// 移除動(dòng)畫狀態(tài)的監(jiān)聽(tīng)

  void removeStatusListener(AnimationStatusListener listener);


  /// 獲取當(dāng)前動(dòng)畫的狀態(tài)

  AnimationStatus get status;


  /// 獲取當(dāng)前動(dòng)畫的插值,執(zhí)行動(dòng)畫時(shí)需要根據(jù)該值進(jìn)行UI繪制等

  T get value;

 }

    AnimationController:動(dòng)畫的管理類,繼承自 Animation<double>。默認(rèn)情況下在給定的時(shí)間范圍內(nèi)線性生成從0.0到1.0的值。

    AnimationController對(duì)象需要傳遞一個(gè)vsync參數(shù),它接收一個(gè)TickerProvider類型的對(duì)象,主要職責(zé)是創(chuàng)建Ticker。Flutter應(yīng)用在啟動(dòng)時(shí)會(huì)綁定一個(gè)SchedulerBinding,可以給每一次屏幕刷新添加回調(diào),Ticker就是通過(guò)SchedulerBinding來(lái)添加屏幕刷新的回調(diào),當(dāng)屏幕刷新時(shí),會(huì)通知到綁定的Ticker回調(diào)。假如動(dòng)畫的UI不在當(dāng)前屏幕,比如鎖屏?xí)r,鎖屏后屏幕停止刷新,不會(huì)通知SchedulerBinding,Ticker也就不會(huì)觸發(fā),這樣就能夠防止屏幕外的動(dòng)畫消耗不必要的資源。

 class AnimationController extends Animation<double>

  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {

  /// value:動(dòng)畫的初始值,默認(rèn)是lowerBound

  /// duration:動(dòng)畫執(zhí)行的時(shí)長(zhǎng)

  /// lowerBound:動(dòng)畫的最小值,默認(rèn)值為0.0

  /// upperBound:動(dòng)畫的最大值,默認(rèn)值為1.0

  /// vsync:可以通過(guò) `with SingleTickerProviderStateMixin` 傳入StatefulWidget對(duì)象

  AnimationController({

  double value,

  this.duration,

  this.lowerBound = 0.0,

  this.upperBound = 1.0,

  @required TickerProvider vsync,

  }) {

  _ticker = vsync.createTicker(_tick);

  }


  Ticker _ticker;


  /// Ticker的回調(diào),每次屏幕刷新都會(huì)回調(diào)

  void _tick(Duration elapsed) {

  notifyListeners();

  }


  /// 開(kāi)始播放動(dòng)畫

  TickerFuture forward({ double from })


  /// 反向播放動(dòng)畫

  TickerFuture reverse({ double from })


  /// 設(shè)置動(dòng)畫重復(fù)執(zhí)行

  TickerFuture repeat({ double min, double max, bool reverse = false, Duration period })


  /// 釋放動(dòng)畫資源

  void dispose()

 }

    CurvedAnimation:非線性動(dòng)畫類,繼承自 Animation<double>。CurvedAnimation可以使用curve屬性指定曲線函數(shù)Curve,類似Android動(dòng)畫的插值器,F(xiàn)lutter中已經(jīng)實(shí)現(xiàn)了許多常用的曲線,在Curves類中可以找到,比如Curves.linear、Curves.decelerate、Curves.ease。也可以繼承Curve類重寫 transform() 方法來(lái)實(shí)現(xiàn)自定義的曲線函數(shù)。

 class CurvedAnimation extends Animation<double>

  with AnimationWithParentMixin<double> {

  /// parent:指定AnimationController對(duì)象

  /// curve:指定動(dòng)畫的曲線函數(shù)

  CurvedAnimation({

  @required this.parent,

  @required this.curve,

  })

 }


 abstract class Curve {

  /// 計(jì)算動(dòng)畫執(zhí)行中`t`點(diǎn)的插值,可以自定義曲線函數(shù)

  double transform(double t)

 }

    Tween:補(bǔ)間值的生成類,繼承自 Animatable<T>。

    由于AnimationController的值范圍默認(rèn)為0.0到1.0,如果需要不同的范圍或數(shù)據(jù)類型,可以使用Tween指定動(dòng)畫值的范圍。Tween不僅能返回double類型的值,還有IntTween、ColorTween、SizeTween等各種返回不同數(shù)據(jù)類型的子類。
    使用Tween對(duì)象需要調(diào)用 animate() 方法,傳入AnimationController對(duì)象,該方法會(huì)返回一個(gè)Animation,這樣就可以獲取到動(dòng)畫的插值了。

 class Tween<T extends dynamic> extends Animatable<T> {

  /// begin:動(dòng)畫的起始值

  /// end:動(dòng)畫的結(jié)束值

  Tween({ this.begin, this.end });


  /// 可以把double類型的動(dòng)畫插值轉(zhuǎn)換成任何類型的值

  T transform(double t)


  /// parent:傳入AnimationController對(duì)象

  /// 返回Animation對(duì)象,使用Animation.value獲取動(dòng)畫當(dāng)前的插值

  Animation<T> animate(Animation<double> parent)

 }

    AnimatedBuilder:用于構(gòu)建動(dòng)畫的Widget,將動(dòng)畫和要執(zhí)行動(dòng)畫的Widget關(guān)聯(lián)起來(lái),繼承關(guān)系為AnimatedBuilder → AnimatedWidget → StatefulWidget。

 class AnimatedBuilder extends AnimatedWidget {

  const AnimatedBuilder({

  @required Listenable animation,

  @required this.builder,

  });


  /// typedef TransitionBuilder = Widget Function(BuildContext context, Widget child);

  /// builder是一個(gè)函數(shù),返回Widget對(duì)象

  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是一個(gè)StatefulWidget。當(dāng)AnimatedWidget關(guān)聯(lián)的_AnimatedState初始化時(shí),會(huì)注冊(cè)動(dòng)畫的監(jiān)聽(tīng)函數(shù)_handleChange,_handleChange監(jiān)聽(tīng)函數(shù)中又調(diào)用了setState()方法,即動(dòng)畫插值每次改變時(shí)都會(huì)調(diào)用build()方法。_AnimatedState.build()方法中又調(diào)用了AnimatedWidget.build()方法,在AnimatedBuilder中實(shí)現(xiàn)了AnimatedWidget.build()方法:調(diào)用屬性builder生成Widget,最終實(shí)現(xiàn)了動(dòng)畫與Widget的綁定。

加載動(dòng)畫的實(shí)現(xiàn)

了解了Flutter的動(dòng)畫后,再結(jié)合之前對(duì)加載動(dòng)畫流程的分析,加載動(dòng)畫可分成三個(gè)階段,我們可以依賴Tween類,指定值的范圍從0.0到3.0變化,當(dāng)然也可以只使用AnimationController,指定lowerBound和upperBound的值分別為0.0和3.0。這里之所以不使用CurvedAnimation,是因?yàn)榧虞d動(dòng)畫的圓弧是線性變化的,不存在加速減速,沒(méi)有必要使用。

大圓弧能夠?qū)崿F(xiàn)了,我們?cè)賮?lái)看內(nèi)部的小圓弧,仔細(xì)觀察會(huì)發(fā)現(xiàn)小圓弧的變化規(guī)律與大圓弧完全一致,只不過(guò)小圓弧的起始位置在x軸負(fù)方向,與大圓弧正好相差180度,也就是π弧度。在繪制大圓弧的同時(shí),可以很輕松的計(jì)算出小圓弧的起點(diǎn)的角度(即大圓弧起點(diǎn)的角度+π弧度)。

至此整個(gè)動(dòng)畫的實(shí)現(xiàn)思路就清晰了:

  1. 自定義加載動(dòng)畫的Widget,繼承自CustomPaint類。
  2. 使用AnimationController、Tween創(chuàng)建動(dòng)畫,動(dòng)畫的值范圍從0.0到3.0線性變化,并且設(shè)置動(dòng)畫重復(fù)執(zhí)行。動(dòng)畫插值每遞增1.0代表動(dòng)畫執(zhí)行的一個(gè)階段。
  3. 繼承CustomPainter類,實(shí)現(xiàn)paint()方法繪制圓弧。根據(jù)動(dòng)畫的插值判斷當(dāng)前屬于動(dòng)畫的哪個(gè)階段,再計(jì)算出圓弧的起點(diǎn)、掃過(guò)的角度,繪制出兩個(gè)圓弧。

下面是實(shí)現(xiàn)加載動(dòng)畫的關(guān)鍵代碼:

 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對(duì)象

   // 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(); // 執(zhí)行動(dòng)畫

  _animationController.repeat(); // 設(shè)置動(dòng)畫循環(huán)執(zhí)行

  }


  @override

  void dispose() {

  // 調(diào)用dispose()方法釋放動(dòng)畫資源

  _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;

  // 動(dòng)畫的第一階段:圓弧起點(diǎn)為0度,終點(diǎn)的角度遞增

  if (value <= 1.0) {

   startAngle = 0;

   sweepAngle = value * pi;

  }

  // 動(dòng)畫的第二階段:圓弧掃過(guò)的弧度為π弧度(180度),起點(diǎn)、終點(diǎn)一起順時(shí)針旋轉(zhuǎn),一共旋轉(zhuǎn)π弧度

  else if (value <= 2.0) {

   startAngle = (value - 1) * pi;

   sweepAngle = pi;

  }

  // 動(dòng)畫的第三階段:圓弧的終點(diǎn)不變,起點(diǎn)從x軸負(fù)方向開(kāi)始順時(shí)針旋轉(zhuǎn),直到起點(diǎn)也到達(dá)x軸正方向

  else {

   startAngle = pi + (value - 2) * pi;

   sweepAngle = (3 - value) * pi;

  }

  // 繪制外圈的大圓弧

  canvas.drawArc(outerRect, startAngle, sweepAngle, false, _outerPaint);

  // 繪制內(nèi)圈的小圓弧

  canvas.drawArc(innerRect, startAngle + pi, sweepAngle, false, _innerPaint);

  }


  @override

  bool shouldRepaint(CustomPainter oldDelegate) {

  return true;

  }

 }

總結(jié)

Flutter的Canvas、Paint與Android的API非常類似,基本的思路也一致,對(duì)于Android同學(xué)比較容易掌握。

Flutter中動(dòng)畫的實(shí)現(xiàn)相較于Android邏輯更加清晰簡(jiǎn)單,方便易用。AnimatedBuilder類巧妙的將UI與動(dòng)畫整合在一起,把UI和動(dòng)畫職責(zé)分離,這種思路值得學(xué)習(xí)。Flutter中的動(dòng)畫還有路由過(guò)渡動(dòng)畫、Hero動(dòng)畫、切換動(dòng)畫組件AnimatedSwitcher等,有需要的同學(xué)可以查找相關(guān)資料。

如果大家需要定制一些個(gè)性化的加載動(dòng)畫,推薦一個(gè)GitHub的開(kāi)源項(xiàng)目:flutter_spinkit,這個(gè)插件提供了很多種常用的加載動(dòng)畫效果。

好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。

相關(guān)文章

最新評(píng)論