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

芙蓉秋江發表於2019-03-14

非常感謝 Didier Boelens 同意我將它的一些文章翻譯為中文發表,這是其中一篇。

本文通過一個例項詳細講解了 Flutter 中動畫的原理。

原文的程式碼塊有行號,對修改的程式碼有黃色背景。在這裡不能對程式碼新增行號和背景顏色,所以,為方便閱讀有些程式碼塊用了截圖。

原文 連結

 

Flutter中的動畫功能強大而且使用起來非常簡單。 通過一個具體的例子,您將學習如何構建自己的動畫所需的一切。

難度:中等

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

Flutter使動畫效果非常容易實現。 用非常簡單的話來說,這篇文章討論了這個主題,早些時候這些事只能留給專家,為了使這篇論文具有吸引力,我採取了用 Flutter 逐步實現以下斷頭臺效果選單,這個動畫是由 Vitaly Rubtsov 在 Dribble 上釋出的。

original

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

動畫的 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的一個例項。

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

  • 第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毫秒內生成的值的示例:

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

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

讓我們開啟程式碼以瞭解如何使用它。

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

  • 第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 預定義的 變化曲線

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 個不同的動畫,我們會得到:

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

如果我們現在考慮值的區間,對於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,如下圖所示?

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

然後我們可以按如下方式重寫 _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 選單關閉時

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

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

在這個實現中仍然有一些遺漏的東西......開啟選單時標題消失,關閉選單時又顯示出來了。 這是一個 淡出/淡入 效果,也可以作為動畫處理。 我們加上吧。

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

結果

這是我獲得的結果,它與原版非常接近,不是嗎?

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

原始碼

本文的完整原始碼可以在 GitHub 上找到。

小結

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

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

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

相關文章