如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)-創(chuàng)新互聯(lián)

這篇文章主要為大家展示了“如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)”,內(nèi)容簡(jiǎn)而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)”這篇文章吧。

創(chuàng)新互聯(lián)建站主打移動(dòng)網(wǎng)站、網(wǎng)站設(shè)計(jì)、成都網(wǎng)站設(shè)計(jì)、網(wǎng)站改版、網(wǎng)絡(luò)推廣、網(wǎng)站維護(hù)、主機(jī)域名、等互聯(lián)網(wǎng)信息服務(wù),為各行業(yè)提供服務(wù)。在技術(shù)實(shí)力的保障下,我們?yōu)榭蛻?hù)承諾穩(wěn)定,放心的服務(wù),根據(jù)網(wǎng)站的內(nèi)容與功能再?zèng)Q定采用什么樣的設(shè)計(jì)。最后,要實(shí)現(xiàn)符合網(wǎng)站需求的內(nèi)容、功能與設(shè)計(jì),我們還會(huì)規(guī)劃穩(wěn)定安全的技術(shù)方案做保障。

動(dòng)畫(huà)中的三大核心

為了能夠?qū)崿F(xiàn)動(dòng)畫(huà)效果,必須提供下面的三個(gè)元素:

  • Ticker

  • Animation

  • AnimationController

下面對(duì)這幾個(gè)元素進(jìn)行一下簡(jiǎn)單的介紹,更詳細(xì)的在后面說(shuō)明。

Ticker

簡(jiǎn)單來(lái)說(shuō),Ticker 這個(gè)類(lèi)會(huì)在常規(guī)的一個(gè)時(shí)間區(qū)間里(大約每秒 60 次),發(fā)送一個(gè)信號(hào),把這想象成你得手表,每秒都會(huì)滴答滴答的轉(zhuǎn)。

當(dāng) Ticker 啟動(dòng)之后,自從第一個(gè) tick 到來(lái)開(kāi)始,每個(gè)到的 tick 都會(huì)回調(diào) Ticker 的 callback 方法。

重要提示
盡管所有的 ticker 可能是在不同的時(shí)間里啟動(dòng)的,但是它們總是以同步的方式執(zhí)行,這對(duì)于一些同步動(dòng)畫(huà)是很有用的。

Animation

Animation 其實(shí)沒(méi)有什么特別的,只不過(guò)是一個(gè)可以隨著動(dòng)畫(huà)的生命周期改變的一個(gè)值(有特定的類(lèi)型),值隨著動(dòng)畫(huà)時(shí)間的變化而變化的方式可以是線(xiàn)性的(例如1、2、3、4、5...),也可以更為復(fù)雜(參考后面的“Curves 曲線(xiàn)”)。

AnimationController

AnimationController 是一個(gè)可以控制一個(gè)或多個(gè)動(dòng)畫(huà)(開(kāi)始,結(jié)束,重復(fù))的控制器。換句話(huà)說(shuō),它讓上面說(shuō)的 Animation 值在一個(gè)指定的時(shí)間內(nèi),根據(jù)一個(gè)速度從一個(gè)最小值變化到大。

AnimationController 類(lèi)介紹

此類(lèi)可控制動(dòng)畫(huà)。為了更加精確,我寧愿說(shuō)“ 控制一個(gè)場(chǎng)景”,因?yàn)樯院笪覀儗⒖吹?,幾個(gè)不同的動(dòng)畫(huà)可以由同一個(gè)控制器來(lái)控制……

因此,使用這個(gè)AnimationController類(lèi),我們可以:

  • 開(kāi)始一個(gè)子動(dòng)畫(huà),正向或者反向播放

  • 停止一個(gè)子動(dòng)畫(huà)

  • 為子動(dòng)畫(huà)設(shè)置一個(gè)具體的值

  • 定義動(dòng)畫(huà)值的邊界

以下偽代碼可以展示這個(gè)類(lèi)里面的不同的初始化參數(shù)

AnimationController controller = new AnimationController(
    value:      // the current value of the animation, usually 0.0 (= default)
    lowerBound: // the lowest value of the animation, usually 0.0 (= default)
    upperBound: // the highest value of the animation, usually 1.0 (= default)
    duration:   // the total duration of the whole animation (scene)
    vsync:      // the ticker provider
    debugLabel: // a label to be used to identify the controller
            // during debug session);
復(fù)制代碼

在大多數(shù)情況下,初始化 AnimationController 時(shí)不會(huì)設(shè)計(jì)到 value,lowerBound,upperBound和debugLabel。

如何將 AnimationController 綁定到 Ticker 上

為了讓動(dòng)畫(huà)正常工作,必須將 AnimationController 綁定到 Ticker 上。

通常情況下,你可以生成一個(gè) Ticker 綁定到一個(gè) StatefulWidget 實(shí)例上。

class _MyStateWidget extends State<MyStateWidget>        with SingleTickerProviderStateMixin {
    AnimationController _controller;    @override
    void initState(){      super.initState();
      _controller = new AnimationController(
        duration: const Duration(milliseconds: 1000), 
        vsync: this,
      );
    }    @override
    void dispose(){
      _controller.dispose();      super.dispose();
    }
    ...
}
復(fù)制代碼
  • 第 2 行 這行代碼告訴 Flutter ,你想要一個(gè)單 Ticker,這個(gè) Ticker 鏈接到了 MyStateWidget 實(shí)例上。

  • 8-10行

控制器的初始化。場(chǎng)景(子動(dòng)畫(huà))的總持續(xù)時(shí)間設(shè)置為1000毫秒,并綁定到了 Ticker(vsync:this)。

隱式參數(shù)為:lowerBound = 0.0 和 upperBound = 1.0

  • 16行

非常重要,當(dāng) MyStateWidget 這個(gè)頁(yè)面的實(shí)例銷(xiāo)毀時(shí),您需要釋放 controller。

TickerProviderStateMixin 還是 SingleTickerProviderStateMixin?

如果你有幾個(gè)Animation Controller情況下,你想有不同的 Ticker, 只需要將 SingleTickerProviderStateMixin 替換為 TickerProviderStateMixin。

好的,我已經(jīng)將控制器綁定到了 Ticker 上,但是它是工作的?

正是由于 ticker,每秒鐘將會(huì)產(chǎn)生大約 60 個(gè) tick,AnimationController 將根據(jù) tick 在給定的時(shí)間里,線(xiàn)性的產(chǎn)生在最小值和大值之間的值。

在這1000毫秒內(nèi)產(chǎn)生的值的示例如下:

如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)

image.png

我們看到值在1000毫秒內(nèi)從0.0(lowerBound)到1.0(upperBound)變化。生成了51個(gè)不同的值。

讓我們擴(kuò)展代碼以查看如何使用它。

class _MyStateWidget extends State<MyStateWidget>        with SingleTickerProviderStateMixin {
    AnimationController _controller;    @override
    void initState(){      super.initState();
      _controller = new AnimationController(
        duration: const Duration(milliseconds: 1000), 
        vsync: this,
      );
      _controller.addListener((){
          setState((){});
      });
      _controller.forward();
    }    @override
    void dispose(){
      _controller.dispose();      super.dispose();
    }    @override
    Widget build(BuildContext context){        final int percent = (_controller.value * 100.0).round();        return new Scaffold(
            body: new Container(
                child: new Center(
                    child: new Text('$percent%'),
                ),
            ),
        );
    }
}
復(fù)制代碼
  • 12 行 此行告訴控制器,每次其值更改時(shí),我們都需要重建Widget(通過(guò)setState())

  • 第15行

Widget初始化完成后,我們告訴控制器開(kāi)始計(jì)數(shù)(forward() -> 從lowerBound到upperBound)

  • 26行

我們檢索控制器的值(_controller.value),并且在此示例中,此值的范圍是0.0到1.0(也就是 0% 到 100%),我們得到此百分比的整數(shù)表達(dá)式,將其顯示在頁(yè)面的中心。

動(dòng)畫(huà)的概念

如我們所見(jiàn), controller 可以以線(xiàn)性的方式返回彼此不同的小數(shù)值。

有的時(shí)候我們可能還有其他的需求如:

  • 使用其他類(lèi)型的值,例如Offset,int …

  • 使用范圍不是從0.0到1.0

  • 考慮線(xiàn)性變化以外的其他變化類(lèi)型以產(chǎn)生一些效果

使用其他值類(lèi)型

為了能夠使用其他值類(lèi)型,Animation 類(lèi)使用模板。

換句話(huà)說(shuō),您可以定義:

Animation<int> integerVariation;
Animation<double> decimalVariation;
Animation<Offset> offsetVariation;
復(fù)制代碼
使用不同的數(shù)值范圍

有時(shí),我們希望使用一個(gè)不同的范圍,而不是0.0和1.0。

為了定義這樣的范圍,我們將使用 Tween 類(lèi)。

為了說(shuō)明這一點(diǎn),讓我們考慮一個(gè)情況,您希望角度從0到π/ 2 變化的情況。

Animation<double> angleAnimation = new Tween(begin: 0.0, end: pi/2);
復(fù)制代碼
變化類(lèi)型

如前所述,將默認(rèn)值從 lowerBound 變化到 upperBound 的默認(rèn)方式是線(xiàn)性的,controller 就是這么控制的。

如果要使角度從0到π/ 2 弧度線(xiàn)性變化,請(qǐng)將 Animation 綁定到AnimationController:

Animation<double> angleAnimation = new Tween(begin: 0.0, end: pi/2).animate(_controller);
復(fù)制代碼

當(dāng)您開(kāi)始動(dòng)畫(huà)(通過(guò)_controller.forward())時(shí),angleAnimation.value 將使用 _controller.value 來(lái)獲取 范圍[0.0; π/ 2] 中的值。

下圖顯示了這種線(xiàn)性變化(π/ 2 = 1.57)

如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)

image.png

使用Flutter預(yù)定義的曲線(xiàn)變化

Flutter 提供了一組預(yù)定義的 Curved 變化,如下:

如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)

image.png

要使用這些曲線(xiàn)效果:

Animation<double> angleAnimation = new Tween(begin: 0.0, end: pi/2).animate(    new CurvedAnimation(
        parent: _controller,
        curve:  Curves.ease,
        reverseCurve: Curves.easeOut
    ));
復(fù)制代碼

這將產(chǎn)生值[0; π/ 2] 之間的值:

  • 當(dāng)正向播放動(dòng)畫(huà),數(shù)值從 0 到 π/2 ,會(huì)使用 Curves.ease 效果

  • 當(dāng)反向播放動(dòng)畫(huà),數(shù)值從 π/2 到 0,會(huì)使用 Curves.easeOut 效果

控制動(dòng)畫(huà)

該AnimationController 類(lèi)可以讓你通過(guò) API 來(lái)控制動(dòng)畫(huà)。(以下是最常用的API):

  • _controller.forward({兩個(gè)區(qū)間的值})

要求控制器開(kāi)始生成 lowerBound- > upperBound中的值

from 的可選參數(shù)可用于強(qiáng)制控制器從lowerBound之外的另一個(gè)值開(kāi)始“ 計(jì)數(shù) ”

  • _controller.reverse({兩個(gè)區(qū)間的值})

要求控制器開(kāi)始生成 upperBound- > lowerBound中的值

from的可選參數(shù)可用于強(qiáng)制控制器從“ upperBound ”之外的另一個(gè)值開(kāi)始“ 計(jì)數(shù) ”

  • _controller.stop({bool cancelled:true})

停止運(yùn)行動(dòng)畫(huà)

  • _controller.reset()

將動(dòng)畫(huà)重置為從 LowerBound 開(kāi)始

  • _controller.animateTo(double target, { Duration duration, Curve curve: Curves.linear })

將動(dòng)畫(huà)的當(dāng)前值改變到目標(biāo)值。

  • _controller.repeat({double min,double max,Duration period})

開(kāi)始以正向運(yùn)行動(dòng)畫(huà),并在動(dòng)畫(huà)完成后重新啟動(dòng)動(dòng)畫(huà)。如果定義了 min 或者 max ,將限制動(dòng)畫(huà)的重復(fù)執(zhí)行次數(shù)。

安全起見(jiàn)

由于動(dòng)畫(huà)可能會(huì)意外停止(例如關(guān)閉屏幕),因此在使用以下API之一時(shí),添加“ .orCancel ” 更為安全:

__controller.forward().orCancel;
復(fù)制代碼

這個(gè)小技巧,可以保證,在 _controller 釋放之前,如果 Ticker 取消了,將不會(huì)導(dǎo)致異常。

場(chǎng)景的概念

官方文檔中不存在“ 場(chǎng)景 ”一詞,但就我個(gè)人而言,我發(fā)現(xiàn)它更接近現(xiàn)實(shí)。我來(lái)解釋一下。

如我所說(shuō),一個(gè) AnimationController 管理一個(gè)Animation。但是,我們可能將“ 動(dòng)畫(huà) ” 一詞理解為一系列需要依次播放或重疊播放的子動(dòng)畫(huà)。將子動(dòng)畫(huà)組合在一起,這就是我所說(shuō)的“ 場(chǎng)景 ”。

考慮以下情況,其中動(dòng)畫(huà)的整個(gè)持續(xù)時(shí)間為10秒,我們希望達(dá)到的效果是:

  • 在開(kāi)始的2秒內(nèi),有一個(gè)球從屏幕的左側(cè)移動(dòng)到屏幕的中間

  • 然后,同一個(gè)球需要3秒鐘才能從屏幕中心移動(dòng)到屏幕頂部中心

  • 最終,球需要5秒鐘才能消失。 正如您最可能已經(jīng)想到的那樣,我們必須考慮3種不同的動(dòng)畫(huà):

////// Definition of the _controller with a whole duration of 10 seconds///AnimationController _controller = new AnimationController(
    duration: const Duration(seconds: 10), 
    vsync: this);////// First animation that moves the ball from the left to the center///Animation<Offset> moveLeftToCenter = new Tween(
    begin: new Offset(0.0, screenHeight /2), 
    end: new Offset(screenWidth /2, screenHeight /2)
).animate(_controller);////// Second animation that moves the ball from the center to the top///Animation<Offset> moveCenterToTop = new Tween(
    begin: new Offset(screenWidth /2, screenHeight /2), 
    end: new Offset(screenWidth /2, 0.0)
).animate(_controller);////// Third animation that will be used to change the opacity of the ball to make it disappear///Animation<double> disappear = new Tween(
    begin: 1.0, 
    end: 0.0).animate(_controller);
復(fù)制代碼

現(xiàn)在的問(wèn)題是,我們?nèi)绾捂溄樱ɑ蚓幣牛┳觿?dòng)畫(huà)?

Interval

組合動(dòng)畫(huà)可以通過(guò) Interval 這個(gè)類(lèi)來(lái)實(shí)現(xiàn)。但是,那什么是 Interval?

可能和我們腦子里首先想到的不一樣, Interval 和時(shí)間沒(méi)有關(guān)系,而是一組值的范圍。

如果考慮使用 _controller,則必須記住,它會(huì)使值從 lowerBound 到 upperBound 變化。

通常,這兩個(gè)值基本定義為 lowerBound = 0.0 和 upperBound = 1.0,這使動(dòng)畫(huà)計(jì)算更容易,因?yàn)閇0.0-> 1.0]只是從0%到100%的變化。因此,如果一個(gè)場(chǎng)景的總持續(xù)時(shí)間為10秒,則最有可能在5秒后,相應(yīng)的_controller.value將非常接近0.5(= 50%)。

如果將3個(gè)不同的動(dòng)畫(huà)放在一個(gè)時(shí)間軸上,則可以獲得如下示意圖:

如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)

image.png

如果現(xiàn)在考慮值的間隔,則對(duì)于3個(gè)動(dòng)畫(huà)中的每個(gè)動(dòng)畫(huà),我們將得到:

  • moveLeftToCenter

持續(xù)時(shí)間:2秒,從0秒開(kāi)始,以2秒結(jié)束=>范圍= [0; 2] =>百分比:從整個(gè)場(chǎng)景的0%到20%=> [0.0; 0.20]

  • moveCenterToTop

持續(xù)時(shí)間:3秒,開(kāi)始于2秒,結(jié)束于5秒=>范圍= [2; 5] =>百分比:從整個(gè)場(chǎng)景的20%到50%=> [0.20; 0.50]

  • disappear

持續(xù)時(shí)間:5秒,開(kāi)始于5秒,結(jié)束于10秒=>范圍= [5; 10] =>百分比:從整個(gè)場(chǎng)景的50%到100%=> [0.50; 1.0]

現(xiàn)在我們有了這些百分比,我們得到每個(gè)動(dòng)畫(huà)的定義,如下:

////// Definition of the _controller with a whole duration of 10 seconds///AnimationController _controller = new AnimationController(
    duration: const Duration(seconds: 10), 
    vsync: this);////// First animation that moves the ball from the left to the center///Animation<Offset> moveLeftToCenter = new Tween(
    begin: new Offset(0.0, screenHeight /2), 
    end: new Offset(screenWidth /2, screenHeight /2)
    ).animate(            new CurvedAnimation(
                parent: _controller,
                curve:  new Interval(                    0.0,                    0.20,
                    curve: Curves.linear,
                ),
            ),
        );////// Second animation that moves the ball from the center to the top///Animation<Offset> moveCenterToTop = new Tween(
    begin: new Offset(screenWidth /2, screenHeight /2), 
    end: new Offset(screenWidth /2, 0.0)
    ).animate(            new CurvedAnimation(
                parent: _controller,
                curve:  new Interval(                    0.20,                    0.50,
                    curve: Curves.linear,
                ),
            ),
        );////// Third animation that will be used to change the opacity of the ball to make it disappear///Animation<double> disappear = new Tween(begin: 1.0, end: 0.0)
        .animate(            new CurvedAnimation(
                parent: _controller,
                curve:  new Interval(                    0.50,                    1.0,
                    curve: Curves.linear,
                ),
            ),
        );
復(fù)制代碼

這就是定義場(chǎng)景(或一系列動(dòng)畫(huà))所需的全部設(shè)置。當(dāng)然,沒(méi)有什么可以阻止您重疊子動(dòng)畫(huà)…

響應(yīng)動(dòng)畫(huà)狀態(tài)

有時(shí),獲取動(dòng)畫(huà)(或場(chǎng)景)的狀態(tài)很方便。

動(dòng)畫(huà)可能具有4種不同的狀態(tài):

  • dismissed:動(dòng)畫(huà)在開(kāi)始后停止(或尚未開(kāi)始)

  • forward:動(dòng)畫(huà)從頭到尾運(yùn)行

  • reverse:動(dòng)畫(huà)反向播放

  • completed:動(dòng)畫(huà)在播放后停止

要獲得此狀態(tài),我們需要通過(guò)以下方式監(jiān)聽(tīng)動(dòng)畫(huà)狀態(tài)的變化:

   myAnimation.addStatusListener((AnimationStatus status){       switch(status){           case AnimationStatus.dismissed:
               ...               break;           case AnimationStatus.forward:
               ...               break;           case AnimationStatus.reverse:
               ...               break;           case AnimationStatus.completed:
               ...               break;
       }
   });
復(fù)制代碼

狀態(tài)應(yīng)用的典型示例就是狀態(tài)的切換。例如,動(dòng)畫(huà)完成后,我們要反轉(zhuǎn)它,如:

  myAnimation.addStatusListener((AnimationStatus status){      switch(status){          ///
          /// When the animation is at the beginning, we force the animation to play
          ///
          case AnimationStatus.dismissed:
              _controller.forward();              break;          ///
          /// When the animation is at the end, we force the animation to reverse
          ///
          case AnimationStatus.completed:
              _controller.reverse();              break;
      }
  });
復(fù)制代碼

理論已經(jīng)足夠了,現(xiàn)在我們開(kāi)始實(shí)戰(zhàn)

我在文章開(kāi)頭提到了一個(gè)動(dòng)畫(huà),現(xiàn)在我準(zhǔn)備開(kāi)始實(shí)現(xiàn)它,名字就叫“guillotine(斷頭臺(tái))”

動(dòng)畫(huà)分析及程序初始化

未來(lái)能夠?qū)崿F(xiàn)“斬頭臺(tái)”效果,我們需要考慮一下幾個(gè)方面:

  • 頁(yè)面內(nèi)容本身

  • 當(dāng)我們點(diǎn)擊菜單圖標(biāo)時(shí),菜單欄會(huì)旋轉(zhuǎn)

  • 旋轉(zhuǎn)時(shí),菜單會(huì)覆蓋頁(yè)面內(nèi)容并填充整個(gè)視口

  • 一旦菜單是完全可見(jiàn),我們?cè)俅吸c(diǎn)擊圖標(biāo),菜單旋轉(zhuǎn)出來(lái),以便回到原來(lái)的位置和尺寸

從這些觀察中,我們可以立即得出結(jié)論,我們沒(méi)有使用帶有AppBar的普通Scaffold(因?yàn)楹笳呤枪潭ǖ模?/p>

我們需要使用 2 層 Stack:

  • 頁(yè)面內(nèi)容(下層)

  • 菜單(上層)

程序的基本框架基本出來(lái)了:

class MyPage extends StatefulWidget {    @override
    _MyPageState createState() => new _MyPageState();
}class _MyPageState extends State<MyPage>{  @override
  Widget build(BuildContext context){      return SafeArea(
        top: false,
        bottom: false,
        child: new Container(
          child: new Stack(
            alignment: Alignment.topLeft,
            children: <Widget>[              new Page(),              new GuillotineMenu(),
            ],
          ),
        ),
      );
  }
}class Page extends StatelessWidget {    @override
    Widget build(BuildContext context){        return new Container(
            padding: const EdgeInsets.only(top: 90.0),
            color: Color(0xff222222),
        );
    }
}class GuillotineMenu extends StatefulWidget {    @override
    _GuillotineMenuState createState() => new _GuillotineMenuState();
}class _GuillotineMenuState extends State<GuillotineMenu> {    @overrride
    Widget build(BuildContext context){        return new Container(
            color: Color(0xff333333),
        );
    }
}
復(fù)制代碼

這些代碼的運(yùn)行結(jié)果為黑屏,僅顯示覆蓋整個(gè)視口的GuillotineMenu。

菜單效果分析

如果你看了上面的示例,可以看到菜單完全打開(kāi)時(shí),它完全覆蓋了視口。打開(kāi)后,只有可見(jiàn)的AppBa。

而如果最初旋轉(zhuǎn) GuillotineMenu 并在按下菜單按鈕時(shí)將其旋轉(zhuǎn)π/ 2,將會(huì)怎樣呢,如下圖所示這樣嗎?

如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)

image.png

然后,我們可以按以下方式重寫(xiě)_GuillotineMenuState類(lèi):(這里不在解釋如何布局,這不是重點(diǎn))

class _GuillotineMenuState extends State<GuillotineMenu> {   double rotationAngle = 0.0;    @override
    Widget build(BuildContext context){
        MediaQueryData mediaQueryData = MediaQuery.of(context);        double screenWidth = mediaQueryData.size.width;        double screenHeight = mediaQueryData.size.height;        return new Transform.rotate(
                angle: rotationAngle,
                origin: new Offset(24.0, 56.0),
                alignment: Alignment.topLeft,
                child: Material(
                    color: Colors.transparent,
                    child: Container(
                    width: screenWidth,
                    height: screenHeight,
                    color: Color(0xFF333333),
                    child: new Stack(
                        children: <Widget>[
                            _buildMenuTitle(),
                            _buildMenuIcon(),
                            _buildMenuContent(),
                        ],
                    ),
                ),
            ),
        );
    }    ///
    /// Menu Title
    ///
    Widget _buildMenuTitle(){        return new Positioned(
            top: 32.0,
            left: 40.0,
            width: screenWidth,
            height: 24.0,
            child: new Transform.rotate(
                alignment: Alignment.topLeft,
                origin: Offset.zero,
                angle: pi / 2.0,
                child: new Center(
                child: new Container(
                    width: double.infinity,
                    height: double.infinity,
                    child: new Opacity(
                    opacity: 1.0,
                    child: new Text('ACTIVITY',
                        textAlign: TextAlign.center,
                        style: new TextStyle(
                            color: Colors.white,
                            fontSize: 20.0,
                            fontWeight: FontWeight.bold,
                            letterSpacing: 2.0,
                        )),
                    ),
                ),
            )),
        );
    }    ///
    /// Menu Icon
    /// 
    Widget _buildMenuIcon(){        return new Positioned(
            top: 32.0,
            left: 4.0,
            child: new IconButton(
                icon: const Icon(
                    Icons.menu,
                    color: Colors.white,
                ),
                onPressed: (){},
            ),
        );
    }    ///
    /// Menu content
    ///
    Widget _buildMenuContent(){        final List<Map> _menus = <Map>[
            {            "icon": Icons.person,            "title": "profile",            "color": Colors.white,
            },
            {            "icon": Icons.view_agenda,            "title": "feed",            "color": Colors.white,
            },
            {            "icon": Icons.swap_calls,            "title": "activity",            "color": Colors.cyan,
            },
            {            "icon": Icons.settings,            "title": "settings",            "color": Colors.white,
            },
        ];        return new Padding(
            padding: const EdgeInsets.only(left: 64.0, top: 96.0),
            child: new Container(
                width: double.infinity,
                height: double.infinity,
                child: new Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: _menus.map((menuItem) {                        return new ListTile(
                            leading: new Icon(
                            menuItem["icon"],
                            color: menuItem["color"],
                            ),
                            title: new Text(
                            menuItem["title"],
                            style: new TextStyle(
                                color: menuItem["color"],
                                fontSize: 24.0),
                            ),
                        );
                    }).toList(),
                ),
            ),
        );
    }
}
復(fù)制代碼
  • 10-13行

這些線(xiàn)定義了斷頭臺(tái)菜單圍繞旋轉(zhuǎn)中心(菜單圖標(biāo)的位置)的旋轉(zhuǎn)

現(xiàn)在,此代碼的結(jié)果將顯示一個(gè)未旋轉(zhuǎn)的菜單屏幕(因?yàn)閞otationAngle = 0.0),該屏幕顯示了垂直的標(biāo)題。

接下來(lái)使 menu 顯示動(dòng)畫(huà)

如果更新 rotationAngle 的值(在-π/ 2和0之間),您將看到菜單旋轉(zhuǎn)了相應(yīng)的角度。

如前所述,我們需要

  • 一個(gè)SingleTickerProviderStateMixin,因?yàn)槲覀冎挥?個(gè)場(chǎng)景

  • 一個(gè)AnimationController

  • 一個(gè)動(dòng)畫(huà) 有一個(gè)角度變化

代碼如下所示:

class _GuillotineMenuState extends State<GuillotineMenu>    with SingleTickerProviderStateMixin {
    AnimationController animationControllerMenu;
    Animation<double> animationMenu;    ///
    /// Menu Icon, onPress() handling
    ///
    _handleMenuOpenClose(){
        animationControllerMenu.forward();
    }    @override
    void initState(){        super.initState();    ///
        /// Initialization of the animation controller
        ///
        animationControllerMenu = new AnimationController(
            duration: const Duration(milliseconds: 1000), 
            vsync: this
        )..addListener((){
            setState((){});
        });    ///
        /// Initialization of the menu appearance animation
        ///
        _rotationAnimation = new Tween(
            begin: -pi/2.0, 
            end: 0.0
        ).animate(animationControllerMenu);
    }    @override
    void dispose(){
        animationControllerMenu.dispose();        super.dispose();
    }    @override
    Widget build(BuildContext context){
        MediaQueryData mediaQueryData = MediaQuery.of(context);        double screenWidth = mediaQueryData.size.width;        double screenHeight = mediaQueryData.size.height;        double angle = animationMenu.value;        return new Transform.rotate(
            angle: angle,
            origin: new Offset(24.0, 56.0),
            alignment: Alignment.topLeft,
            child: Material(
                color: Colors.transparent,
                child: Container(
                    width: screenWidth,
                    height: screenHeight,
                    color: Color(0xFF333333),
                    child: new Stack(
                        children: <Widget>[
                            _buildMenuTitle(),
                            _buildMenuIcon(),
                            _buildMenuContent(),
                        ],
                    ),
                ),
            ),
        );
    }
    ...    ///
    /// Menu Icon
    /// 
    Widget _buildMenuIcon(){        return new Positioned(
            top: 32.0,
            left: 4.0,
            child: new IconButton(
                icon: const Icon(
                    Icons.menu,
                    color: Colors.white,
                ),
                onPressed: _handleMenuOpenClose,
            ),
        );
    }
    ...
}
復(fù)制代碼

現(xiàn)在,當(dāng)我們按下菜單按鈕時(shí),菜單會(huì)打開(kāi),但再次按下按鈕時(shí)菜單不會(huì)關(guān)閉。這是 AnimationStatus 要完成的事情。

讓我們添加一個(gè)監(jiān)聽(tīng)器,并基于 AnimationStatus 決定是向前還是向后運(yùn)行動(dòng)畫(huà)。

////// Menu animation status///enum _GuillotineAnimationStatus { closed, open, animating }class _GuillotineMenuState extends State<GuillotineMenu>    with SingleTickerProviderStateMixin {
    AnimationController animationControllerMenu;
    Animation<double> animationMenu;
    _GuillotineAnimationStatus menuAnimationStatus = _GuillotineAnimationStatus.closed;
    _handleMenuOpenClose(){        if (menuAnimationStatus == _GuillotineAnimationStatus.closed){
            animationControllerMenu.forward().orCancel;
        } else if (menuAnimationStatus == _GuillotineAnimationStatus.open) {
            animationControllerMenu.reverse().orCancel;
        }
    }    @override
    void initState(){        super.initState();    ///
        /// Initialization of the animation controller
        ///
        animationControllerMenu = new AnimationController(
            duration: const Duration(milliseconds: 1000), 
            vsync: this
        )..addListener((){
            setState((){});
        })..addStatusListener((AnimationStatus status) {            if (status == AnimationStatus.completed) {        ///
        /// When the animation is at the end, the menu is open
        ///
              menuAnimationStatus = _GuillotineAnimationStatus.open;
            } else if (status == AnimationStatus.dismissed) {        ///
        /// When the animation is at the beginning, the menu is closed
        ///
              menuAnimationStatus = _GuillotineAnimationStatus.closed;
            } else {        ///
        /// Otherwise the animation is running
        ///
              menuAnimationStatus = _GuillotineAnimationStatus.animating;
            }
          });
    ...
    }
...
}
復(fù)制代碼

現(xiàn)在菜單可以按預(yù)期方式打開(kāi)或關(guān)閉,但是前面的演示向我們展示了一個(gè)打開(kāi)/關(guān)閉的動(dòng)畫(huà),該懂哈不是線(xiàn)性的,看起來(lái)有一個(gè)反復(fù)的回彈效果。接下來(lái)讓我們添加此效果。

為此,我將選擇以下2種效果:

  • 菜單打開(kāi)時(shí)用 bounceOut

  • 菜單關(guān)閉時(shí)用 bouncIn

如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)

image.png

如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)

image.png

class _GuillotineMenuState extends State<GuillotineMenu>    with SingleTickerProviderStateMixin {
...    @override
    void initState(){
    ...    ///
    /// Initialization of the menu appearance animation
    /// 
    animationMenu = new Tween(
        begin: -pi / 2.0, 
        end: 0.0
    ).animate(new CurvedAnimation(
        parent: animationControllerMenu,
        curve: Curves.bounceOut,
        reverseCurve: Curves.bounceIn,
    ));
    }
...
}
復(fù)制代碼

在此實(shí)現(xiàn)中仍有一些細(xì)節(jié)沒(méi)有實(shí)現(xiàn):打開(kāi)菜單時(shí)標(biāo)題消失,而關(guān)閉菜單時(shí)顯示標(biāo)題。這是一個(gè)面朝上/朝外的效果,也要作為動(dòng)畫(huà)處理。讓我們添加它。

class _GuillotineMenuState extends State<GuillotineMenu>    with SingleTickerProviderStateMixin {
  AnimationController animationControllerMenu;
  Animation<double> animationMenu;
  Animation<double> animationTitleFadeInOut;
  _GuillotineAnimationStatus menuAnimationStatus;
...  @override
  void initState(){
    ...    ///
    /// Initialization of the menu title fade out/in animation
    /// 
    animationTitleFadeInOut = new Tween(
        begin: 1.0, 
        end: 0.0
    ).animate(new CurvedAnimation(
        parent: animationControllerMenu,
        curve: new Interval(            0.0,            0.5,
            curve: Curves.ease,
        ),
    ));
  }
...  ///
  /// Menu Title
  ///
  Widget _buildMenuTitle(){    return new Positioned(
      top: 32.0,
      left: 40.0,
      width: screenWidth,
      height: 24.0,
      child: new Transform.rotate(
        alignment: Alignment.topLeft,
        origin: Offset.zero,
        angle: pi / 2.0,
        child: new Center(
          child: new Container(
            width: double.infinity,
            height: double.infinity,
              child: new Opacity(
                opacity: animationTitleFadeInOut.value,
                child: new Text('ACTIVITY',
                    textAlign: TextAlign.center,
                    style: new TextStyle(
                        color: Colors.white,
                        fontSize: 20.0,
                        fontWeight: FontWeight.bold,
                        letterSpacing: 2.0,
                    )),
                ),
            ),
        )),
    );
  }
...
}
復(fù)制代碼

最終的效果基本如下:

如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)

以上是“如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)-成都網(wǎng)站建設(shè)公司行業(yè)資訊頻道!

分享標(biāo)題:如何實(shí)現(xiàn)Flutter動(dòng)畫(huà)-創(chuàng)新互聯(lián)
分享路徑:http://muchs.cn/article32/djiosc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供手機(jī)網(wǎng)站建設(shè)、Google、全網(wǎng)營(yíng)銷(xiāo)推廣、靜態(tài)網(wǎng)站、網(wǎng)站導(dǎo)航、虛擬主機(jī)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

h5響應(yīng)式網(wǎng)站建設(shè)