Flutter入門篇(三)— 如何實現(xiàn)登錄動畫效果

在上一篇的時候虑椎,我們講解了怎么做一個登錄界面,但是之后呢俱笛?完全是草草結(jié)尾的感覺嘛捆姜,這不,接下來就是給大家詳細說說迎膜,這個登錄里面不得鳥的故事泥技。先來看一個登錄的過程~~

登錄失敗

分析

可能上面的gif圖不是很真切,這上面展示了兩個功能:

  • 顏色變換的閃屏頁面
  • 動畫效果的登錄頁面

有沒有感覺這樣的登錄好像還不錯呢磕仅,哈哈哈珊豹,接下來就詳細分析一下這其中的玄機~~

路由

一般我們的頁面跳轉(zhuǎn)都會涉及到路由,路由就是從一個頁面跳轉(zhuǎn)到另一個頁面的過程榕订,就比如Android中的Activity或IOS中的ViewController的跳轉(zhuǎn)店茶。

在Flutter中所以的路由都使用Navigator來進行管理的,換句話說它就是讓這些本來相對獨立的個體形成一個完美的整體劫恒。那么Navigator是直接管理的就是頁面嗎贩幻?當(dāng)然不是,實際上它管理是Route對象两嘴,而且提供了管理堆棧的相關(guān)方法丛楚,比如:

  • Navigator.push (入棧)
  • Navigator.pop (出棧)

雖然能夠直接創(chuàng)建一個navigator,但是呢憔辫,一般不建議這樣直接使用鸯檬,我們常常通過WidgetsApp或者MaterialApp去創(chuàng)建。還記得第一篇的時候螺垢,就跟大家提過喧务,F(xiàn)lutter提供了許多widgets,可幫助您構(gòu)建遵循Material Design的應(yīng)用程序枉圃。Material應(yīng)用程序以MaterialApp widget開始功茴, 該widget在應(yīng)用程序的根部創(chuàng)建了一些有用的widget,其中包括一個Navigator孽亲, 它管理由字符串標(biāo)識的Widget棧(即頁面路由棧)坎穿。Navigator可以讓您的應(yīng)用程序在頁面之間的平滑的過渡。 所以我們的應(yīng)用啟動一般這樣寫:

void main() {
    runApp(MaterialApp(home: MyAppHome()));
}

那么返劲,home所指向的頁面也就是我們棧中最底層的路由玲昧,那MaterialApp到底是怎么創(chuàng)建這個底層路由的呢?它遵循以下幾個原則:

const MaterialApp({
    Key key,
    this.navigatorKey,
    this.home,
    this.routes = const <String, WidgetBuilder>{},
    this.initialRoute,
    this.onGenerateRoute,
    this.onUnknownRoute,
    //省略無關(guān)代碼
    ...
})
  • 首先使用我們的home所指向的
  • 如果失敗篮绿,那么就會使用routes路由表
  • 如果路由表為空孵延,那么就會調(diào)用onGenerateRoute
  • 如果以上所有都失敗了,那么onUnknownRoute將會被調(diào)用

所以說如果要創(chuàng)建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的方式去入棧(或者出棧)吼虎。如上可以看出犬钢,當(dāng)我點擊 POP按鈕的時候又可以將這個頁面進行出棧,又可以回到我們的home頁面思灰。但是玷犹,通常我們不這么去返回上一個頁面,在上一章的時候就使用ScaffoldAppBar中可以直接添加一個返回洒疚,究其根本這個返回最終也是調(diào)用的這個

Navigator.pop(context);

當(dāng)我們需要在返回的時候帶一個返回值的時候歹颓,可以像如下的方式進行使用,那么這個時候就不能使用ScaffoldAppBar中的返回了拳亿,因為它是不會返回任何結(jié)果的晴股。

Navigator.pop(context, true);

pushNamed

上面是通過一個動態(tài)的方式去進行路由,我們也可以使用一種靜態(tài)的方式去路由肺魁,那就是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");

那么,我們能不能攜帶參數(shù)呢瘾晃?當(dāng)然是可以的咯

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'),
  );
}

當(dāng)然還有一些其他的方式:

  1. pushReplacementNamed 和 pushReplacement 替換當(dāng)前頁面
  2. popAndPushNamed 當(dāng)前頁面出棧贷痪,入棧新的頁面
  3. pushNamedAndRemoveUntil 和 pushAndRemoveUntil 入棧新頁面并關(guān)閉之前的所有頁面

動畫

在前面gif圖中我們可以看到在閃屏頁在不同的時間顏色有不同的變化(圖片模糊,效果不明顯)蹦误,還有點擊登錄的時候劫拢,按鈕的樣子也有變化肉津,那么這個是怎么實現(xiàn)的呢?當(dāng)然是我們的動畫了~~

AnimationController

AnimationController用來控制一個動畫的正向播放舱沧、反向播放和停止動畫等操作妹沙。在默認情況下AnimationController是按照線性進行動畫播放的。
需要注意的是在使用AnimationController的時候需要結(jié)合TickerProvider熟吏,因為只有在TickerProvider下才能配置AnimationController中的構(gòu)造參數(shù)vsync距糖。TickerProvider是一個抽象類,所以我們一般使用它的實現(xiàn)類TickerProviderStateMixinSingleTickerProviderStateMixin牵寺。

那么悍引,這兩種方式有什么不同呢?
如果整個生命周期中帽氓,只有一個AnimationController趣斤,那么就使用SingleTickerProviderStateMixin,因為此種情況下杏节,它的效率相對來說要高很多唬渗。反之,如果有多個AnimationController奋渔,就是用TickerProviderStateMixin镊逝。

需要注意的是,如果AnimationController不需要使用的時候嫉鲸,一定要將其釋放掉撑蒜,不然有可能造成內(nèi)存泄露。

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

有了動畫控制器之后玄渗,就需要我們的動畫效果了哦座菠。但是我們可以發(fā)現(xiàn)Animation本身是個抽象類,所以我們需要的是它的實現(xiàn)類藤树。我們可以直接使用Tween或者它的子類去實現(xiàn)一個Animation浴滴,在AnimationController中提供了一個drive方法,這個是用來干什么的呢岁钓?這個是用來鏈接一個TweenAnimation并返回一個Animation的實例升略。

Animation<Alignment> _alignment1 = _controller.drive(
  AlignmentTween(
    begin: Alignment.topLeft,
    end: Alignment.topRight,
  ),
);

為什么要使用Tween呢?Tween就是一個線性的插值器屡限,可以實現(xiàn)一個完整的變化過程

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的構(gòu)造提供了兩個參數(shù)品嚣,一個開始bengin ,一個結(jié)束end钧大,就是說讓動畫可以在這個區(qū)間內(nèi)進行變化翰撑,當(dāng)然它也提供了很多子類,比如:ColorTween啊央、SizeTween眶诈、IntTweenCurveTween等等

  • ColorTween 可以實現(xiàn)兩個顏色的變化
  • SizeTween 可以實現(xiàn)兩個size的變化
  • IntTween 可以實現(xiàn)兩個int 值之間的變化
  • CurveTween 可以實現(xiàn)動畫非線性變化

CurvedAnimation

CurvedAnimation就是將一個曲線(非線性)變化應(yīng)用到另一個動畫涨醋,如果想使用 Curve應(yīng)用到Tween就可以直接使用上面所說的CurveTween,可以不CurvedAnimation册养。

final Animation<double> animation = CurvedAnimation(
  parent: controller,
  curve: Curves.ease,
);

這里需要兩個參數(shù)一個是動畫控制东帅,也就是我們的AnimationController,另一個就是curve球拦,它描述了到底是按照什么樣的曲線進行變化的。在Curves中提供了很多的變化過程帐我,有興趣的童鞋可以自己去研究一下~~

動畫關(guān)系

這里總結(jié)一下:

  • AnimationController 控制整個動畫的播放坎炼,停止等操作
  • Tween 動畫的變化區(qū)間
  • CurvedAniamtion 控制動畫按照非線性進行變化

閃屏動畫實現(xiàn)

要實現(xiàn)一個動畫的,首先肯定需要上面所說的AniamtionControllerAnimation拦键,有這個還不夠谣光,還需要一個可以根據(jù)
在閃屏頁面中,我們的動畫是顏色根據(jù)時間不同的進行變化芬为,那肯定會用到我們的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標(biāo)記的函數(shù)中運行。

上面的意思就是讓動畫先正向進行份氧,然后在反向進行~~
但是發(fā)現(xiàn)動畫寫完之后運行唯袄,但是沒有任何作用,這是因為你沒有將動畫的變化應(yīng)用到widget

@override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: Container(
        decoration: BoxDecoration(color: colorAnimation.value),
        child: Center(
        ...
        //省略無關(guān)代碼
        ),
     ),
   );
}

在上述代碼中的BoxDecoration(color: colorAnimation.value)就是將顏色的值作用于整個Container上蜗帜,所以顏色就隨之變化而變化恋拷。

在動畫結(jié)束的時候不是要進行路由跳轉(zhuǎn)到下一個頁面的嘛?這就需要在對動畫的監(jiān)聽厅缺,當(dāng)動畫結(jié)束的時候就進行跳轉(zhuǎn)蔬顾,就需要修改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(() {});
          });

這里需要注意的是,在判斷結(jié)束的時候店归,這里使用的是colorController.isDismissed阎抒,沒有使用colorController.isCompleted是因為在正向動畫完成的時候就會調(diào)用,還沒讓這個動畫流程運行完成~~

如果需要完整代碼消痛,就可以來這兒且叁。

登錄動畫實現(xiàn)

這里和上面是一樣的實現(xiàn)動動畫,但是直接使用的是Tween秩伞,而且使用了另一種將Tween關(guān)聯(lián)到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(() {});
    });
}

這里有一點需要注意欺矫,使用的curveInterval,這個的作用就是根據(jù)你提供的時間區(qū)間進行動畫展示展氓。就如上面定的動畫時間大小是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,
            ),
    ),
),

① 當(dāng)點擊登錄按鈕后未妹,動畫開始進行,并且對這個按鈕的寬度就開始進行變化

 width: _buttonLengthAnimation.value,

② 當(dāng)動畫的值還大于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,
            ),
    ),

其實這就是整個動畫的過程歪赢,只是其中我做了一個對動畫運行的判斷化戳,當(dāng)?shù)卿浭。妥寗赢嫲粹o回到最初的狀態(tài)埋凯,并提示登錄失敗点楼。如果登錄成功,就直接跳轉(zhuǎn)到新的頁面~~

總結(jié)

在這里規(guī)整一下白对,方便大家整理記憶

  • 路由有很多中方式掠廓,可以根據(jù)不同的情況進行選擇,一般常用的就是pushpushNamed躏结,如果是pushNamed那么一定要在MaterialApp中設(shè)置路由表却盘。
  • 動畫的使用一般需要跟TickerProvider配合使用,如果在State中就可以直接使用它的實現(xiàn)類SingleTickerProviderStateMixinTickerProviderStateMixin媳拴。
  • 如果只有一個AnimationController就是用SingleTickerProviderStateMixin黄橘,反之,使用TickerProviderStateMixin屈溉。
  • 動畫的建立跟AnimationController塞关、TweenCurveAnimation有關(guān)。
  • AnimationController在不需要的時候一定要進行釋放dispose子巾,不然可能會造成內(nèi)存溢出帆赢。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市线梗,隨后出現(xiàn)的幾起案子椰于,更是在濱河造成了極大的恐慌,老刑警劉巖仪搔,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瘾婿,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機偏陪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門抢呆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人笛谦,你說我怎么就攤上這事抱虐。” “怎么了饥脑?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵恳邀,是天一觀的道長。 經(jīng)常有香客問我灶轰,道長轩娶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任框往,我火速辦了婚禮,結(jié)果婚禮上闯捎,老公的妹妹穿的比我還像新娘椰弊。我一直安慰自己,他們只是感情好瓤鼻,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布秉版。 她就那樣靜靜地躺著,像睡著了一般茬祷。 火紅的嫁衣襯著肌膚如雪清焕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天祭犯,我揣著相機與錄音秸妥,去河邊找鬼。 笑死沃粗,一個胖子當(dāng)著我的面吹牛粥惧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播最盅,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼突雪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了涡贱?” 一聲冷哼從身側(cè)響起咏删,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎问词,沒想到半個月后督函,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年侨核,在試婚紗的時候發(fā)現(xiàn)自己被綠了草穆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡搓译,死狀恐怖悲柱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情些己,我是刑警寧澤豌鸡,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站段标,受9級特大地震影響涯冠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜逼庞,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一蛇更、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧赛糟,春花似錦派任、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至司倚,卻和暖如春豆混,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背动知。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工皿伺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拍柒。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓心傀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拆讯。 傳聞我的和親對象是個殘疾皇子脂男,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容