Flutter入門篇(三)— 如何實現登入動畫效果
在上一篇的時候,我們講解了怎麼做一個登入介面,但是之後呢?完全是草草結尾的感覺嘛,這不,接下來就是給大家詳細說說,這個登入裡面不得鳥的故事。先來看一個登入的過程~~

登入失敗
分析
可能上面的 gif
圖不是很真切,這上面展示了兩個功能:
- 顏色變換的閃屏頁面
- 動畫效果的登入頁面
有沒有感覺這樣的登入好像還不錯呢,哈哈哈,接下來就詳細分析一下這其中的玄機~~
路由
一般我們的頁面跳轉都會涉及到路由,路由就是從一個頁面跳轉到另一個頁面的過程,就比如Android中的 Activity
或IOS中的 ViewController
的跳轉。
在Flutter中所以的路由都使用 Navigator
來進行管理的,換句話說它就是讓這些本來相對獨立的個體形成一個完美的整體。那麼 Navigator
是直接管理的就是頁面嗎?當然不是,實際上它管理是 Route
物件,而且提供了管理堆疊的相關方法,比如:
- Navigator.push (入棧)
- Navigator.pop (出棧)
雖然能夠直接建立一個 navigator
,但是呢,一般不建議這樣直接使用,我們常常通過 WidgetsApp
或者 MaterialApp
去建立。還記得 第一篇 的時候,就跟大家提過,Flutter提供了許多widgets,可幫助您構建遵循 Material Design
的應用程式。Material應用程式以MaterialApp widget開始, 該widget在應用程式的根部建立了一些有用的widget,其中包括一個 Navigator
, 它管理由字串標識的Widget棧(即頁面路由棧)。Navigator可以讓您的應用程式在頁面之間的平滑的過渡。 所以我們的應用啟動一般這樣寫:
void main() { runApp(MaterialApp(home: MyAppHome())); }
那麼, home
所指向的頁面也就是我們棧中最底層的路由,那 MaterialApp
到底是怎麼建立這個底層路由的呢?它遵循以下幾個原則:
const MaterialApp({ Key key, this.navigatorKey, this.home, this.routes = const <String, WidgetBuilder>{}, this.initialRoute, this.onGenerateRoute, this.onUnknownRoute, //省略無關程式碼 ... })
home routes onGenerateRoute onUnknownRoute
所以說如果要建立 Navigator
,那麼以上四個必須有一個被使用。
MaterialPageRoute
一般我們可以使用 MaterialPageRoute
去進行路由:
Navigator.push(context, MaterialPageRoute<void>( builder: (BuildContext context) { return Scaffold( appBar: AppBar(title: Text('My Page')), body: Center( child: FlatButton( child: Text('POP'), onPressed: () { Navigator.pop(context); }, ), ), ); }, ));
這種方式的就很明顯了,它是使用一種 build
的方式去入棧(或者出棧)。如上可以看出,當我點選 POP
按鈕的時候又可以將這個頁面進行出棧,又可以回到我們的 home
頁面。但是,通常我們不這麼去返回上一個頁面,在 上一章 的時候就使用 Scaffold
的 AppBar
中可以直接新增一個返回,究其根本這個返回最終也是呼叫的這個
Navigator.pop(context);
當我們需要在返回的時候帶一個返回值的時候,可以像如下的方式進行使用,那麼這個時候就不能使用 Scaffold
的 AppBar
中的返回了,因為它是不會返回任何結果的。
Navigator.pop(context, true);
pushNamed
上面是通過一個動態的方式去進行路由,我們也可以使用一種靜態的方式去路由,那就是 pushNamed
,從字面意思就是通過頁面的名字進行路由的,那麼這個名字是從哪裡來的呢?這就需要使用我們上面在 MaterialApp
中的 routes
路由表了。
class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primaryColor: Color(0xFFFF786E), primaryColorLight: Color(0xFFFF978F), accentColor: Color(0xFFFFFFFF) ), home: Start(), debugShowCheckedModeBanner: false, routes:{ "scaffold_page":(context)=>ScaffoldTest(), "snack_page":(context)=> SnackTest(), "login_page":(context)=> LoginPage(), "start_page":(context)=> Start(), } ); } }
Navigator.pushNamed(context, "snack_page");
那麼,我們能不能攜帶引數呢?當然是可以的咯
void _showBerlinWeather() { Navigator.pushNamed( context, '/weather', arguments: <String, String>{ 'city': 'Berlin', 'country': 'Germany', }, ); }
也能攜帶一個自定義的物件進行遨遊~~
class WeatherRouteArguments { WeatherRouteArguments({ this.city, this.country }); final String city; final String country; bool get isGermanCapital { return country == 'Germany' && city == 'Berlin'; } } void _showWeather() { Navigator.pushNamed( context, '/weather', arguments: WeatherRouteArguments(city: 'Berlin', country: 'Germany'), ); }
當然還有一些其他的方式:
- pushReplacementNamed 和 pushReplacement 替換當前頁面
- popAndPushNamed 當前頁面出棧,入棧新的頁面
- pushNamedAndRemoveUntil 和 pushAndRemoveUntil 入棧新頁面並關閉之前的所有頁面
動畫
在前面 gif
圖中我們可以看到在閃屏頁在不同的時間顏色有不同的變化(圖片模糊,效果不明顯),還有點選登入的時候,按鈕的樣子也有變化,那麼這個是怎麼實現的呢?當然是我們的動畫了~~
AnimationController
AnimationController
用來控制一個動畫的正向播放、反向播放和停止動畫等操作。在預設情況下 AnimationController
是按照線性進行動畫播放的。
需要注意的是在使用 AnimationController
的時候需要結合 TickerProvider
,因為只有在 TickerProvider
下才能配置 AnimationController
中的構造引數 vsync
。 TickerProvider
是一個抽象類,所以我們一般使用它的實現類 TickerProviderStateMixin
和 SingleTickerProviderStateMixin
。
那麼,這兩種方式有什麼不同呢?
如果整個生命週期中,只有一個 AnimationController
,那麼就使用 SingleTickerProviderStateMixin
,因為此種情況下,它的效率相對來說要高很多。反之,如果有多個 AnimationController
,就是用 TickerProviderStateMixin
。
需要注意的是,如果 AnimationController
不需要使用的時候,一定要將其釋放掉,不然有可能造成記憶體洩露。
class StartState extends State<Start> with SingleTickerProviderStateMixin { AnimationController colorController; @override void initState() { // TODO: implement initState super.initState(); colorController = new AnimationController( vsync: this, duration: new Duration(seconds: 3)); } @override Widget build(BuildContext context) { return new Scaffold( body: Container( //省略部分程式碼 ... ), ); } @override void dispose() { // TODO: implement dispose colorController.dispose(); super.dispose(); } }
Animation
有了動畫控制器之後,就需要我們的動畫效果了哦。但是我們可以發現 Animation
本身是個抽象類,所以我們需要的是它的實現類。我們可以直接使用 Tween
或者它的子類去實現一個 Animation
,在 AnimationController
中提供了一個 drive
方法,這個是用來幹什麼的呢?這個是用來連結一個 Tween
到 Animation
並返回一個 Animation
的例項。
Animation<Alignment> _alignment1 = _controller.drive( AlignmentTween( begin: Alignment.topLeft, end: Alignment.topRight, ), );
為什麼要使用 Tween
呢? Tween
就是一個線性的插值器,可以實現一個完整的變化過程
class Tween<T extends dynamic> extends Animatable<T> { Tween({ this.begin, this.end }); T begin; T end; @protected T lerp(double t) { assert(begin != null); assert(end != null); return begin + (end - begin) * t; } @override T transform(double t) { if (t == 0.0) return begin; if (t == 1.0) return end; return lerp(t); } @override String toString() => '$runtimeType($begin \u2192 $end)'; }
Tween
的構造提供了兩個引數,一個開始 bengin
,一個結束 end
,就是說讓動畫可以在這個區間內進行變化,當然它也提供了很多子類,比如: ColorTween
、 SizeTween
、 IntTween
和 CurveTween
等等
-
ColorTween
可以實現兩個顏色的變化 -
SizeTween
可以實現兩個size
的變化 -
IntTween
可以實現兩個int 值之間的變化 -
CurveTween
可以實現動畫非線性變化
CurvedAnimation
CurvedAnimation
就是將一個曲線(非線性)變化應用到另一個動畫,如果想使用 Curve
應用到 Tween
就可以直接使用上面所說的 CurveTween
,可以不 CurvedAnimation
。
final Animation<double> animation = CurvedAnimation( parent: controller, curve: Curves.ease, );
這裡需要兩個引數一個是動畫控制,也就是我們的 AnimationController
,另一個就是 curve
,它描述了到底是按照什麼樣的曲線進行變化的。在 Curves
中提供了很多的變化過程,有興趣的童鞋可以自己去研究一下~~

動畫關係
這裡總結一下:
- AnimationController 控制整個動畫的播放,停止等操作
- Tween 動畫的變化區間
- CurvedAniamtion 控制動畫按照非線性進行變化
閃屏動畫實現
要實現一個動畫的,首先肯定需要上面所說的 AniamtionController
和 Animation
,有這個還不夠,還需要一個可以根據
在閃屏頁面中,我們的動畫是顏色根據時間不同的進行變化,那肯定會用到我們的 Tween
,這裡是顏色的變化,所以使用到了 ColorTween
。
@override void initState() { // TODO: implement initState super.initState(); colorController = new AnimationController( vsync: this, duration: new Duration(seconds: 3)); colorAnimation = colorController .drive(ColorTween(begin: Color(0xFFFF786E), end: Color(0xFFFFA07A))); }
一般我們對AniamtionController和Animation的初始化在 initState()
方法中,然後就需要在動畫的執行過程中將 widget
進行更新,就會使用到我們的 setState()
colorAnimation = colorController .drive(ColorTween(begin: Color(0xFFFF786E), end: Color(0xFFFFA07A))) ..addListener(() { setState(() {}); });
那麼接下來就是讓整個動畫跑起來了~~
Future<Null> playAnimation() async { try { await colorController.forward(); await colorController.reverse(); } on TickerCanceled {} }
這裡使用到了 dart
語言中的非同步,有兩個特點:
-
await
返回一定是Future
,如果不是會報錯 -
await
所在的方法必須在有async
標記的函式中執行。
上面的意思就是讓動畫先正向進行,然後在反向進行~~
但是發現動畫寫完之後執行,但是沒有任何作用,這是因為你沒有將動畫的變化應用到 widget
上
@override Widget build(BuildContext context) { return new Scaffold( body: Container( decoration: BoxDecoration(color: colorAnimation.value), child: Center( ... //省略無關程式碼 ), ), ); }
在上述程式碼中的 BoxDecoration(color: colorAnimation.value)
就是將顏色的值作用於整個 Container
上,所以顏色就隨之變化而變化。
在動畫結束的時候不是要進行路由跳轉到下一個頁面的嘛?這就需要在對動畫的監聽,當動畫結束的時候就進行跳轉,就需要修改 colorAnimation
colorAnimation = colorController .drive(ColorTween(begin: Color(0xFFFF786E), end: Color(0xFFFFA07A))) ..addListener(() { if (colorController.isDismissed) { Navigator.pushAndRemoveUntil(context, new MaterialPageRoute(builder: (context) { return LoginPage(); }), ModalRoute.withName("start_page")); } setState(() {}); });
這裡需要注意的是,在判斷結束的時候,這裡使用的是 colorController.isDismissed
,沒有使用 colorController.isCompleted
是因為在正向動畫完成的時候就會呼叫,還沒讓這個動畫流程執行完成~~
如果需要完整程式碼,就可以來 這兒 。
登入動畫實現
這裡和上面是一樣的實現動動畫,但是直接使用的是 Tween
,而且使用了另一種將 Tween
關聯到Animation的方式,而且使用
@override void initState() { // TODO: implement initState super.initState(); _animationController = new AnimationController( vsync: this, duration: new Duration(milliseconds: 1500)); _buttonLengthAnimation = new Tween<double>( begin: 312.0, end: 42.0, ).animate(new CurvedAnimation( parent: _animationController, curve: new Interval(0.3, 0.6))) ..addListener(() { if (_buttonLengthAnimation.isCompleted) { if(isLogin){ Navigator.pushNamedAndRemoveUntil(context, "snack_page",ModalRoute.withName('login_page')); }else{ showTips("登入失敗"); } } setState(() {}); }); }
這裡有一點需要注意,使用的 curve
是 Interval
,這個的作用就是根據你提供的時間區間進行動畫展示。就如上面定的動畫時間大小是1500ms,那麼只有在1500*0.3 = 500 ms的時候開始,並在1500*0.6=900ms的時候完成。
那麼接下來就直接看改變動畫的對 widget
處理
InkWell( onTap: login, child: Container( margin: EdgeInsets.only(top: 30), height: 42, width: _buttonLengthAnimation.value, decoration:BoxDecoration(borderRadius: radius, color: colorWhite), alignment: Alignment.center, child: _buttonLengthAnimation.value > 75? new Text("立即登入", style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: colorRegular)) : CircularProgressIndicator( valueColor: new AlwaysStoppedAnimation<Color>(colorRegular), strokeWidth: 2, ), ), ),
① 當點選登入按鈕後,動畫開始進行,並且對這個按鈕的寬度就開始進行變化
width: _buttonLengthAnimation.value,
② 當動畫的值還大於75的時候,中間就顯示 Text
,但是如果小於或等於75的時候,那它的child就是一個就是一個圓形的進度 CircularProgressIndicator
child: _buttonLengthAnimation.value > 75? new Text("立即登入", style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: colorRegular)) : CircularProgressIndicator( valueColor: new AlwaysStoppedAnimation<Color>(colorRegular), strokeWidth: 2, ), ),
其實這就是整個動畫的過程,只是其中我做了一個對動畫執行的判斷,當登入失敗,就讓動畫按鈕回到最初的狀態,並提示登入失敗。如果登入成功,就直接跳轉到新的頁面~~
總結
在這裡規整一下,方便大家整理記憶
- 路由有很多中方式,可以根據不同的情況進行選擇,一般常用的就是
push
和pushNamed
,如果是pushNamed
那麼一定要在MaterialApp
中設定路由表。 - 動畫的使用一般需要跟
TickerProvider
配合使用,如果在State
中就可以直接使用它的實現類SingleTickerProviderStateMixin
或TickerProviderStateMixin
。 - 如果只有一個
AnimationController
就是用SingleTickerProviderStateMixin
,反之,使用TickerProviderStateMixin
。 - 動畫的建立跟
AnimationController
、Tween
或CurveAnimation
有關。 -
AnimationController
在不需要的時候一定要進行釋放dispose
,不然可能會造成記憶體溢位。