[翻譯] Flutter 中的動畫 - 簡易指南 - 教程

很是感謝 Didier Boelens 贊成我將它的一些文章翻譯爲中文發表,這是其中一篇。git

本文經過一個實例詳細講解了 Flutter 中動畫的原理。github

原文的代碼塊有行號,對修改的代碼有黃色背景。在這裏不能對代碼添加行號和背景顏色,因此,爲方便閱讀有些代碼塊用了截圖。安全

原文 連接bash

 

Flutter中的動畫功能強大並且使用起來很是簡單。 經過一個具體的例子,您將學習如何構建本身的動畫所需的一切。markdown

難度:中等session

今天咱們沒法想象沒有任何動畫的移動應用。 當您從一個頁面移動到另外一個頁面時,點擊一個按鈕(或InkWell)… 就有一個動畫。 動畫無處不在。app

Flutter使動畫效果很是容易實現。 用很是簡單的話來講,這篇文章討論了這個主題,早些時候這些事只能留給專家,爲了使這篇論文具備吸引力,我採起了用 Flutter 逐步實現如下斷頭臺效果菜單,這個動畫是由 Vitaly Rubtsov 在 Dribble 上發佈的。less

original

本文的第一部分講解了動畫的理論和主要概念。 第二部分專門用於動畫的實現,就如上面的動圖所顯示的那樣。ide

動畫的 3 個支柱

要有 動畫 效果,須要存在如下3個元素:佈局

  • 一個 Ticker (斷續器)

  • 一個 動做(Animation)

  • 一個 動做控制器 (AnimationController)

斷續器 (Ticker)

簡單來講,Ticker 是一個幾乎定時發送信號的類(大約每秒60次)。 想一想你的手錶每秒鐘都會嘀嗒一聲。 在每一個滴答處,Ticker 調用回調方法,該方法具備自第一個滴答開始後的持續時間。

重要

即便在不一樣時間啓動,全部的 ticher 也將 始終同步。 這對於同步動畫動做很是有用

動畫

動畫只不過是一個能夠在動畫的生命週期內改變的值(特定類型)。 這個值在動畫時間內的變化方式能夠是線性的(如1,2,3,4,5 …),也能夠更復雜(參見後面的曲線)。

動畫控制器

AnimationController 是一個控制(啓動,中止,重複......)動畫(或幾個動畫)的類。 換句話說,它使用速度(=每秒的值變化率)使動畫值在特定持續時間內從一個低的邊界值(lowerBound) 變爲 一個高的邊界值(upperBound)。

AnimationController 類

此類可控制動畫。 爲了更精確,我更願說「在一個場景」,由於咱們稍後會看到,幾個不一樣的動畫能夠由同一個控制器控制......

所以,使用此 AnimationController 類,咱們能夠:

  • 向前播放一個場景,反轉

  • 中止一個場景

  • 將場景設置爲某個值

  • 定義場景的邊界值(lowerBound,upperBound)

如下僞代碼顯示了此類的各類不一樣的初始化參數:

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
);
複製代碼

大多數時候,初始化 AnimationController 時,value,lowerBound,upperBound和debugLabel都沒有被提到。

如何將 AnimationController 綁定到 Ticker?

爲了起做用,須要將 AnimationController 綁定到 Ticker

一般地,您將生成一個 Ticker,連接到 Stateful Widget的一個實例。

  • 第2行

    告訴 Flutter 你想要一個 新的 單個 Ticker,連接到 MyStateWidget 的這個實例

  • 第8-10行

    控制器初始化。 場景 的總持續時間設置爲 1000 毫秒並綁定到 Ticker(vsync:this)。 默認的參數是:lowerBound = 0.0和upperBound = 1.0

  • 第16行

    很是重要,您須要在銷燬 MyStateWidget 實例時釋放控制器。

TickerProviderStateMixin 或 SingleTickerProviderStateMixin?

若是您有多個 AnimationController 實例而且想要各自具備不一樣的 Ticker,請將 TickerProviderStateMixin 替換爲 SingleTickerProviderStateMixin

OK,我將控制器綁定到 Ticker 可是它有什麼幫助?

多虧了每秒約60次的tick,AnimationController在給定的持續時間內線性生成從 lowerBound 到 upperBound 的值。

在這1000毫秒內生成的值的示例:

咱們看到值在1000毫秒內從0.0(lowerBound)變到1.0(upperBound)。 生成了51個不一樣的值。

讓咱們打開代碼以瞭解如何使用它。

  • 第12行

    這行告訴控制器每次它的值改變時,咱們須要從新構建 Widget(經過 setState()

  • 第15行

    Widget 初始化完成後,咱們告訴控制器開始計數(forward() ->lowerBoundupperBound

  • 第26行

    咱們恢復控制器的值( _controller.value ),而且,在這個例子中,這個值的範圍是0.0到1.0(0%到100%),咱們獲得這個百分比的整數表達式,顯示在屏幕中心。

動畫的概念

正如咱們剛剛看到的那樣,控制器返回一系列 十進制值,這些值以 線性 方式變化。 有時,咱們但願:

  • 使用其餘 類型 的值,例如 Offsetint ...

  • 使用不一樣於 0.0 到 1.0 的值範圍

  • 考慮除線性以外的其餘 變化 類型以產生一些效果

使用其餘類型的值

爲了可以使用其餘值類型,Animation 類使用 泛型

換句話說,您能夠定義:

Animation<int> integerVariation;
Animation<double> decimalVariation;
Animation<Offset> offsetVariation;
複製代碼

使用不一樣的值變化範圍

有時,咱們但願在不一樣於 0.0 和 1.0 的 兩個值之間進行變化。

爲了定義這樣的範圍,咱們將使用 Tween 類。

爲了說明這一點,讓咱們考慮一下你但願角度從 0 到 π/2 弧度變化的狀況。

Animation<double> angleAnimation = new Tween(begin: 0.0, end: pi/2);
複製代碼

變化類型

如前所述,將值從 lowerBound 變爲 upperBound 的默認方式是 線性 的,這是控制器的工做方式。

若是要使角度在 0 到 π/2 弧度之間線性變化,請將 Animation 綁定到 AnimationController

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

當你啓動動畫時(經過 _controller.forward() ),angleAnimation.value 將使用 _controller.value 的值來插入 [0.0; π/2] 的範圍。

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

使用 flutter 預約義的 變化曲線

Flutter 提供了一組預約義的變化曲線,列表顯示以下:

使用這些曲線:

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

這建立了一個值的變化[0; π/2],這個變化使用一下曲線:

  • Curves.ease 當動畫從 0.0 -> π/2 (向前)

  • Curves.easeOut 當動畫從 π/2 -> 0.0 (反轉)

控制動畫

AnimationController 是容許您經過 API 控制動畫的類。(這是最經常使用的 API):

  • _controller.forward({ double from })

    要求控制器開始 lowerBound - > upperBound 的值的變化。可選參數 from 可用於強制控制器從另外一個值開始「計數」而不是 lowerBound

  • _controller.reverse({ double from })

    要求控制器開始 upperBound - > lowerBound 的值變化。可選參數 from 可用於強制控制器從 upperBound 之外的另外一個值開始「計數」

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

    中止運行動畫

  • _controller.reset()

    將動畫重置爲 lowerBound

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

    將動畫從其當前值驅動到目標值

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

    開始向前執行動畫,並在完成時重複執行動畫。 若是定義了 min 和 max 值,則 min 和 max 限制重複發生的次數。

讓咱們安全……

因爲動畫可能會意外中止(例如屏幕被退出),所以在使用這些 API 時,添加 「.orCancel」 會更安全:

__controller.forward().orCancel;
複製代碼

因爲這個小技巧,若是在銷燬 _controller 以前取消 Ticker,不會拋出任何異常。

場景的概念

官方文檔中不存在 「場景(scene)」 這個詞,但就我的而言,我發現它更接近現實。 讓我解釋。

正如我所說,一個 AnimationController 管理動畫。 可是,咱們可能會將 「動畫(Animation)」 這個詞理解爲一系列須要按順序或重疊播放的子動畫。關於 如何將子動畫連接在一塊兒的定義,我就稱之爲「場景」。

考慮如下狀況,其中動畫的整個持續時間爲10秒,咱們但願:

  • 前2秒,球從左側移動到屏幕中間

  • 而後,同一個球須要3秒鐘從屏幕的中心移動到頂部中心

  • 最後,球須要5秒淡出。

正如您可能已經想象的那樣,咱們必須考慮 3 種不一樣的動畫:

///
/// _controller 定義,整個持續時間爲10秒
///
AnimationController _controller = new AnimationController(
	duration: const Duration(seconds: 10), 
	vsync: this
);

///
/// 第一個動畫,將球從左側移動到中心
///
Animation<Offset> moveLeftToCenter = new Tween(
	begin: new Offset(0.0, screenHeight /2), 
	end: new Offset(screenWidth /2, screenHeight /2)
).animate(_controller);

///
/// 第二個動畫,將球從中心移動到頂部
///
Animation<Offset> moveCenterToTop = new Tween(
	begin: new Offset(screenWidth /2, screenHeight /2), 
	end: new Offset(screenWidth /2, 0.0)
).animate(_controller);

///
/// 第三個動畫,改變球的不透明度,使其消失
///
Animation<double> disappear = new Tween(
	begin: 1.0, 
	end: 0.0
).animate(_controller);
複製代碼

如今問題是,咱們如何連接(或編排)子動畫?

間隔(Interval)的概念

答案是經過使用 Interval 類來給出的。 但什麼是 間隔(Interval)

可能與咱們腦殼裏冒出的一個想法相反,一個 間隔時間間隔 無關,而與 一系列值 有關。

若是你考慮 _controller,你必須記住 它是使一個值從 lowerBound 變爲 upperBound

一般,這兩個值分別保持在 lowerBound = 0.0upperBound = 1.0,這使事情更容易考慮,由於 [0.0 -> 1.0] 只不過是從0%到100%的變化。 所以,若是一個場景的總持續時間是10秒,那麼在5秒以後,相應的_controller.value 將很是接近0.5(= 50%)。

若是咱們在時間軸上放置這 3 個不一樣的動畫,咱們會獲得:

若是咱們如今考慮值的區間,對於3個動畫中的每個,咱們獲得:

  • 從左移動到中心

    持續時間:2秒,從0秒開始,以2秒結束=>範圍= [0; 2] =>百分比:從整個場景的0%到20%=> [0.0; 0.20]

  • 從中心移動到頂部

    持續時間:3秒,從2秒開始,在第 5 秒結束=>範圍= [2; 5] =>百分比:從整個場景的20%到50%=> [0.20;0.50]

  • 消失

    持續時間:5秒,從5秒開始,以10秒結束=>範圍= [5; 10] =>百分比:從整個場景的50%到100%=> [0.50; 1.0]

如今咱們有這些百分比,咱們能夠更新每一個動畫的定義,以下所示:

///
/// _controller 定義,整個持續時間爲10秒
///
AnimationController _controller = new AnimationController(
	duration: const Duration(seconds: 10), 
	vsync: this
);

///
/// 第一個動畫,將球從左側移動到中心
///
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,
                ),
            ),
        );

///
/// 第二個動畫,將球從中心移動到頂部
///
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,
                ),
            ),
        );

///
/// 第三個動畫,改變球的不透明度,使其消失
///
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,
                ),
            ),
        );
複製代碼

這就是定義場景(或一系列動畫)所需的所有內容。 固然,沒有什麼能阻止你重疊子動畫……

響應動畫狀態

有時,知道動畫(或場景)的狀態頗有用。

動畫有4種不一樣的狀態:

  • 擱置(dismissed):動畫在開始處中止(或還沒有開始)

  • 向前(forward):動畫從開始到結束

  • 反向(reverse):動畫反向運行,從結束到開始

  • 已完成(completed):動畫在結束時中止

要得到這些狀態,咱們須要經過如下方式監聽動畫狀態改變:

myAnimation.addStatusListener((AnimationStatus status){
        switch(status){
            case AnimationStatus.dismissed:
                ...
                break;

            case AnimationStatus.forward:
                ...
                break;

            case AnimationStatus.reverse:
                ...
                break;

            case AnimationStatus.completed:
                ...
                break;
        }
    });
複製代碼

一個典型的用法是,若是此狀態是往復切換。 例如,一旦動畫完成,咱們想要反轉它。 爲達到這個效果:

myAnimation.addStatusListener((AnimationStatus status){
        switch(status){
            ///
            /// 當動畫開始時,咱們強制播放動畫
            ///
            case AnimationStatus.dismissed:
                _controller.forward();
                break;

            ///
            /// 當動畫結束時,咱們強制動畫反轉執行
            ///
            case AnimationStatus.completed:
                _controller.reverse();
                break;
        }
    });
複製代碼

理論已經足夠了,讓咱們如今實踐!

既然已經介紹了理論,那麼如今是時候實踐了……

正如我在本文開頭所提到的,我如今將經過實現一個名爲 「斷頭臺」 的動畫來實現動畫的概念。

分析動畫和初始骨架

爲了得到這種 斷頭臺 效應,咱們首先須要考慮:

  • 頁面內容自己

  • 菜單欄,當咱們點擊 菜單(或漢堡包)圖標時旋轉

  • 旋轉 進來 時,菜單會重疊頁面內容並填充整個屏幕窗口

  • 一旦菜單徹底可見,咱們再次點擊菜單圖標,菜單就會旋轉 出去,以便回到原來的位置和尺寸

從這些觀察中,咱們能夠當即推斷出咱們不能使用帶有 AppBar 的普通 Scaffold (由於後者是固定的)。

咱們相反地會使用兩層的 Stack

  • 頁面內容(下層)

  • 菜單(上層)

讓咱們首先構建這個骨架:

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),
        );
    }
}
複製代碼

這段代碼的效果是一個黑屏,只顯示了 GuillotineMenu,覆蓋了整個視口。

分析菜單自己

若是你仔細看視頻,能夠看到當菜單徹底打開時,它徹底覆蓋了屏幕。 當它剛剛打開時,只能看到像 AppBar 這樣的東西。

沒有什麼能阻止咱們以不一樣的方式看待事物……若是 GuillotineMenu 最初會被旋轉,當咱們點擊菜單按鈕時,咱們將其旋轉π/ 2,以下圖所示?

而後咱們能夠按以下方式重寫 _GuillotineMenuState 類:(沒有給出關於建立佈局方法的解釋,由於這不是本文的目的)

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(),
				),
			),
		);
	}
}
複製代碼
  • 第10-13行

    這幾行定義斷頭臺菜單圍繞一個旋轉中心旋轉,(菜單圖標的位置)

如今這段代碼的結果給出了一個未旋轉的菜單屏幕(由於 rotationAngle = 0.0),它顯示了垂直顯示的標題。

爲菜單添加動畫效果

若是更新 rotationAngle 的值(在 -π/2 和 0 之間),您將看到按相應角度旋轉的菜單。

讓咱們作一些動畫

如前所述,咱們須要

  • 一個 SingleTickerProviderStateMixin,由於咱們只有一個場景

  • 一個 AnimationController

  • 具備角度變化的動畫

代碼變成這樣了:

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,
			),
		);
	}
	...
}
複製代碼

OK,當咱們點擊菜單按鈕時,菜單會打開,可是當咱們再次按下按鈕時不會關閉。 這就是AnimationStatus 的做用。

讓咱們添加一個監聽器,並根據 AnimationStatus 決定是向前仍是向後運行動畫。

///
/// 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;
            }
          });

	...
    }
...
}
複製代碼

菜單如今按預期打開或關閉,但視頻向咱們顯示了一個開放/關閉動做,它不是線性的,看起來像一個彈跳效果。 讓咱們添加這個效果。

爲此,我將選擇如下兩種效果:

  • bounceOut 菜單打開時

  • bounceIn 菜單關閉時

在這個實現中仍然有一些遺漏的東西......打開菜單時標題消失,關閉菜單時又顯示出來了。 這是一個 淡出/淡入 效果,也能夠做爲動畫處理。 咱們加上吧。

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,
					)),
				),
			),
		)),
	);
  }
...
}
複製代碼

結果

這是我得到的結果,它與原版很是接近,不是嗎?

源代碼

本文的完整源代碼能夠在 GitHub 上找到。

小結

就像你看到的這樣,構建動畫很是簡單,甚至是複雜的動畫。

我但願這篇很長的文章成功地揭開了 Flutter 動畫的神祕面紗。

請繼續關注下一篇文章,順祝編碼愉快。

相關文章
相關標籤/搜索