Flutter - 登錄動畫

在Flutter應用程序中實現超級流暢的動畫

在這篇文章中,我將帶您完成在Flutter應用程序中實現流暢動畫的步驟。


時間線

這是一個時間軸,顯示了應用中發生的所有動畫。

登錄屏幕和主屏幕之間平滑過渡背后的秘密在于登錄屏幕的最后一幀與主屏幕的第一幀完全匹配。

讓我們仔細看看登錄界面。

在登錄屏幕中,當用戶單擊“登錄”按鈕時,動畫開始。按鈕被擠壓成圓形,按鈕內的文本被“加載”動畫取代。接下來,按鈕內的加載動畫消失,按鈕在移動到中心時以圓形方式增長。一旦到達中心,按鈕就會長方形地變長并覆蓋整個屏幕。

按鈕覆蓋整個屏幕后,應用程序將導航至屏幕。

這里初始動畫不需要任何用戶輸入。主屏幕出現在屏幕上,帶有“淡入淡出”動畫。配置文件圖像,通知氣泡和“+”按鈕以圓形方式在屏幕上生長,同時列表垂直滑動到屏幕上。

當用戶點擊“+”按鈕時,按鈕內的“+”消失并且在移動到屏幕中心時圓形地增大。一旦到達中心,按鈕現在呈矩形增長并覆蓋整個屏幕。

以不同的方式查看事物,動畫也可以分為進入動畫退出動畫

進入動畫的時間表如下所示:

退出動畫的時間表是這樣的:

以上有足夠的理論!讓我們開始編碼。


動畫登錄屏幕

主屏幕動畫

在此屏幕上,當用戶單擊Sign In按鈕時動畫開始。此屏幕中的動畫可以分為以下幾部分:

設計按鈕

登錄界面上有這個按鈕組件:

new Container(
  width: 320.0,
  height: 60.0,
  alignment: FractionalOffset.center,
  decoration: new BoxDecoration(
    color: const Color.fromRGBO(247, 64, 106, 1.0),
    borderRadius: new BorderRadius.all(const Radius.circular(30.0)),
  ),
  child: new Text(
    "Sign In",
    style: new TextStyle(
      color: Colors.white,
      fontSize: 20.0,
      fontWeight: FontWeight.w300,
      letterSpacing: 0.3,
    ),
  ),
)

請注意窗口小部件的width屬性Container。這個屬性將在按鈕的初始動畫中扮演重要角色,它被擠壓成一個圓圈。

構建動畫之前的初始設置。

我們需要告訴應用程序僅在用戶單擊“ 登錄”按鈕時才啟動動畫。為此,我們創建了一個AnimationController負責管理所有動畫的類。

簡單地說,AnimationController控制動畫。該類是Flutter動畫框架中的一個重要角色。它擴展了動畫類,并添加了最基本的元素使其可用。這個類不是動畫對象的控制器,而是動畫概念的控制器。

AnimationController的基本功能是在指定的時間內將動畫從一個雙值轉換為另一個雙值。

AnimationController看起來像這樣的基本構造函數:

AnimationController({
  double value,
  this.duration,
  this.debugLabel,
  this.lowerBound: 0.0,
  this.upperBound: 1.0,
  @required TickerProvider vsync,
})

AnimationController需要一個TickerProvider獲得他們的Ticker。如果AnimationController要從狀態創建,則可以使用TickerProviderStateMixinSingleTickerProviderStateMixin類來獲取合適的狀態TickerProvider。窗口小部件測試框架WidgetTester對象可以在測試環境中用作股票提供者。

我們在initState函數內初始化這個構造函數:

void initState() {
  super.initState();
  _loginButtonController = new AnimationController(
    duration: new Duration(milliseconds: 3000),
    vsync: this
  );
}

我們唯一需要關注的是Duration小部件。此小組件定義登錄屏幕動畫必須發生的總時間。

LoginScreenState是一個有狀態小部件,并創建AnimationController指定3000毫秒的持續時間。它播放動畫并構建小部件樹的非動畫部分。當在屏幕上檢測到按鈕時,動畫開始。動畫向前,然后向后。

onTap: () {
  _playAnimation();
},
Future<Null> _playAnimation() async {
  try {
    await _loginButtonController.forward();
    await _loginButtonController.reverse();
  }
  on TickerCanceled{}
}

按鈕擠壓動畫(減慢速度)

buttonSqueezeAnimation = new Tween(
  begin: 320.0,
  end: 70.0,
).animate(new CurvedAnimation(
  parent: buttonController,
  curve: new Interval(0.0, 0.250)
))

我已經將這個Tween類與CurvedAnimation小部件一起使用了。Tween指定動畫應該begin和的點end。在動畫的開始時,width該按鈕的是320.0pixels,由動畫結束按鈕被降低到width70.0pixels。按鈕的高度保持不變。持續時間由在Interval窗口小部件內調用的CurvedAnimation窗口小部件定義。Interval基本上告訴應用程序在250毫秒內將按鈕擠壓成一個圓圈。

將按鈕擠壓成圓形后,我們需要登錄文本消失。

我使用條件語句來實現這一點。如果width按鈕的大小超過75.0pixels,則該按鈕應包含登錄文本。否則,按鈕應包含一個CircularProgressIndicator


加載動畫(放慢速度)

new Container(
  width: buttonSqueezeAnimation.value,
  height: 60.0,
  alignment: FractionalOffset.center,
  decoration: new BoxDecoration(
    color: const Color.fromRGBO(247, 64, 106, 1.0),
    borderRadius: new BorderRadies.all(const Radius.circular(30.0))
  ),
  child: buttonSqueezeAnimation.value > 75.0 ? new Text(
    "Sign In",
    style: new TextStyle(
      color: Colors.white,
      fontSize: 20.0,
      fontWeight: FontWeight.w300,
      letterSpacing: 0.3,
    ),
  ) : new CircularProgressIndicator(
    valueColor: new AlwaysStoppedAnimation<Color>(
      Colors.white
    ),
  )
)

接下來,我希望按鈕組件縮小。要做到這一點,只需創建一個Tween小部件,告訴按鈕從70.0pixels增長到1000.0pixels

但是Tween它本身不能為按鈕設置動畫。為此,我們需要使用animate()鏈接Tween一個CurvedAnimation小部件,小部件包含一個Interval小部件,告訴應用程序在350毫秒之間完成動畫。


按鈕增長動畫(減速)

buttonZoomout = new Tween(
  begin: 70.0,
  end: 1000.0,
).animate(
  new CurvedAnimation(
    parent: buttonController,
    curve: new Interval(
      0.550, 0.900,
      curve: Curves.bounceOut,
    ),
  )),
)

我們還需要同時CircularProgressIndicator從按鈕中刪除。為此,我們再次使用條件語句。

雖然buttonZoomOut.value它等于70,但它buttonSqueezeAnimation會起作用。當Interval上面的代碼片段超過0.550時,buttonSqueezeAnimation停止并buttonZoomOut開始動畫。

同樣,如果buttonZoomOut仍然是70.0像素,則高度更改為60.0像素。否則,高度也等于buttonZoomOut的值。

現在,如果buttonZoomOut.value小于300.0,那么我們保持CircularProgressIndicator按鈕內部。否則,我們用null替換它。

這是它的代碼:

new Container(
  width: buttonZoomOut.value == 70 ? buttonSqueezeAnimation.value : buttonZoomOut.value,
  height: buttonZoomOut.value == 70 ? 60.0 : buttonZoomOut.value,
  alignment: FractionalOffset.center,
  decoration: new BoxDecoration(
    color: const Color.fromRGBO(247, 64, 106, 1.0)
    borderRadius: new BorderRadius.all(const Radius.circular(30.0))
  ),
  child: buttonSqueezeAnimation.value > 75.0 ? new Text(
    "Sign In",
    style: new TextStyle(
      color: Colors.white,
      fontSize: 20.0,
    ),
  ) : buttonZoomOut.value < 300.0 ? new CircularProgressIndicator(
    valueColor: new AlwaysStoppedAnimation<Color>(
      Colors.white
    ),
  ) : null),
)

最后,我們希望按鈕容器增長并覆蓋整個屏幕。為此,我們將首先刪除整個頁面的paddingtopbottom),radius容器也應該可以忽略不計。按鈕現在應該增長并覆蓋整個屏幕,并帶有顏色過渡動畫。

封面動畫(放慢速度)

new Padding(
  padding: buttonZoomOut.value == 70 ? const EdgeInsets.only(
    bottom: 50.0,
  ) : const EdgeInsets.only(
    top: 0.0, bottom: 0.0
  ),
  child: new Inkwell(
    onTap: () {
      _playAnimation();
    },
    child: new Hero(
      tag: "fade",
      child: new Container(
        width: buttonZoomOut.value == 70 ? buttonSqueezeAnimation.value : buttonZoomOut.value,
        height: buttonZoomOut.value == 70 ? 60.0 : buttonZoomOut.value,
        alignment: FractionalOffset.center,
        decoration: new BoxDecoration(
          color: buttonZoomOut.value == 70 ? const Color.fromRGBO(247, 64, 106, 1.0) : buttonGrowColorAnimation.value,
          borderRadius: buttonZoomOut.value < 400 ? new BorderRadius.all(const Radius.circular(30.0)) : new BorderRadius.all(const Radius.circular(0.0)),
        ),
        child: buttonSqueezeAnimation.value > 75.0 ? new Text(
          "Sign In",
          style: new TextStyle(
            color: Colors.white,
            fontSize: 20.0,
            fontWeight: FontWeight.w300,
            letterSpacing: 0.3,
          ),
        ) : buttonZoomOut.value < 300.0 ? new CircularProgressIndicator(
          value: null,
          strokeWidth: 1.0,
          valueColor: new AlwaysStoppedAnimation<Color>(
            Colors.white
          ),
        ) : null
      ),
    )
  ),
)

動畫完成后,用戶將導航到主屏幕。

這是兩個屏幕之間平滑過渡的秘訣。我們需要登錄屏幕的最后一幀與屏幕的第一幀匹配。我還添加了一個監聽器,等待按鈕組件完全覆蓋整個屏幕。完成后,我使用內置的Flutter Navigator將我們帶到屏幕。

buttonController.addListener(() {
  if (buttonController.isCompleted) {
    Navigator.pushNamed(context, "/home");
  }
});

動畫主屏幕

就像登錄屏幕一樣,主屏幕上的動畫可以分為以下幾部分:

主屏幕動畫(慢動作)

從上面顯示的G??IF中,我們可以看到當應用程序導航到主屏幕時,首先發生的事情是屏幕漸變為白色。

淡出屏幕

淡入淡出動畫(慢動作)

為此,我們創建一個新的ColorTween小部件并將其命名為fadeScreenAnimation。在這個小部件中,我們將opacity1.0更改為0.0,使顏色消失。我們將此小部件鏈接到CurvedAnimation使用animate()

接下來,我們創建一個名為的新窗口小部件FadeBoxfadeScreenAnimation在其中調用。

fadeScreenAnimation = new ColorTween(
  begin: const Color.fromRGBO(247, 64, 106, 1.0),
  end: const Color.fromRGBO(247, 64, 107, 0.0),
).animate(
  new CurvedAnimation(
    parent: _screenController,
    curve: Curves.ease,
  ),
);
new FadeBox(
  fadeScreenAnimation: fadeScreenAnimation,
  containerGrowAnimation: containerGrowAnimation,
),
new Hero(
  tag: "fade",
  child: new Container(
    width: containerGrowAnimation.value < 1 ? screenSize.width : 0.0,
    height: containerGrowAnimation.value < 1 ? screebSize.height : 0.0,
    decoration: new BoxDecoration(
      color: fadeScreenAnimation.value,
    ),
  )
)

這里Hero是Flutter的一個小部件,用于創建hero動畫。此動畫使對象從一個屏幕“飛行”到另一個屏幕。添加到此窗口小部件的所有子項都有資格使用hero動畫。

配置文件圖像,通知氣泡和添加按鈕以循環方式成長。

containerGrowAnimation = new CurvedAnimation(
  parent: _screenController,
  curve: Curves.easeIn,
);
buttonGrowAnimation = new CurvedAnimation(
  parent: _screnController,
  curve: Curves.easeOut,
);
new Container(
  child: new Column(
    children: [
      new Container(
        width: containerGrowAnimation.value * 35,
        height: containerGrowAnimation.value * 35,
        margin: new EdgeInsets.only(left: 80.0),
        child: new Center(
          child: new Text(
            "3",
            style: new TextStyle(
              fontSize: containerGrowAnimation.value * 15,
              fontWeight: FontWeight.w400,
              color: Colors.white
            )
          ),
        ),
        decoration: new BoxDecoration(
          shape: BoxShape.circle,
          color: const Color.fromRGBO(80, 210, 194, 1.0),
        )
      ),
    ],
  ),
  width: containerGrowAnimation.value * 120,
  height: containerGrowAnimation.value * 120,
  decoration: new BoxDecoration(
    shape: BoxShape.circle,
    image: profileImage,
  )
);

屏幕上半部分的所有元素都有各自的位置。隨著概要文件圖像和列表塊的增長,它們會垂直移動并占據各自的位置。

列表和+按鈕動畫(慢動作)

listTileWidth = new Tween<double>(
  begin: 1000.0,
  end: 600.0,
).animate(
  new CurvedAnimation(
    parent: _screenController,
    curve: new Interval(
      0.225,
      0.600,
      curve: Curves.bounceIn,
    ),
  ),
);
listSlideAnimation = new AlignmentTween(
  begin: Alignment.topCenter,
  end: Alignment.bottomCenter,
).animate(
  new CurvedAnimation(
    parent: _screenController,
    curve: new Interval(
      0.325,
      0.500,
      curve: Curves.ease,
    ),
  ),
);
listSlidePosition = new EdgeInsetsTween(
  begin: const EdgeInsets.only(bottom: 16.0),
  end: const EdgeInsets.only(bottom: 80.0),
).animate(
  new CurvedAnimation(
    parent: _screenController,
    curve: new Interval(
      0.325,
      0.800,
      curve: Curves.ease,
    ),
  ),
);
new Stack(
  alignment: listSlideAnimation.value,
  children: <Widget>[
    new Calendar(margin: listSlidePosition.value * 3.5),
    new ListData(
      margin: listSlidePosition.value * 2.5,
      width: listTileWidth.value,
      title: "New subpage for Janet",
      subtitle: "8-10am",
      image: avatar1
    ),
  ],
)

這樣,主屏幕就會在屏幕上呈現。現在讓我們看一下將用戶帶回登錄屏幕的動畫。


當用戶單擊該Add按鈕時,AnimationController會初始化:

void initState() {
  super.initState();
  _button.Controller = new AnimationController(
    duration: new Duration(milliseconds: 1500),
    vsync: this
  );
}

該按鈕從屏幕右下方導航到中心

按鈕增長動畫(慢動作)

buttonBottomCenterAnimation = new AlignmentTween(
  begin: Alignment.bottomRight,
  end: Alignment.center,
).animate(
  new CurvedAnimation(
    parent: buttonController,
    curve: new Interval(
      0.0,
      0.200,
      curve: Curves.easeOut,
    ),
  ),
)

當按鈕移動到屏幕的中心時,它的大小會逐漸增大并Icon消失。

buttonZoomOutAnimation = new Tween(
  begin: 60.0,
  end: 1000.0,
).animate(
  new CurvedAnimation(
    parent: buttonController,
    curve: Curves.bounceOut
  ),
)

當按鈕到達中心時,它以圓形方式縮小,轉換為矩形并覆蓋整個屏幕。

封面動畫(慢動作)

new Container(
  alignment: buttonBottomtoCenterAnimation.value,
  child: new Inkwell(
    child: new Container(
      width: buttonZoomOutAnimation.value,
      height: buttonZoomOutAnimation.value,
      alignment: buttonBottomtoCenterAnimation.value,
      decoration: new BoxDecoration(
        color: buttonGrowColorAnimation.value,
        shape: buttonZoomOutAnimation.value < 500 ? BoxShape.circle : BoxShape.rectangle
      ),
      child: new Icon(
        Icons.add,
        size: buttonZoomOutAnimation.value < 50 ? buttonZoomOutAnimation.value : 0.0,
        color: Colors.white,
      ),
    ),
  )
)     

最后,動畫完成后,應用程序需要導航回登錄屏幕。

(buttonController.isComplete) {
  Navigator.pushReplacementName(context, "/login");
}

這就是所有人!

你可以在這里查看這個應用程序的完整代碼

轉:https://blog.geekyants.com/flutter-login-animation-ab3e6ed4bd19

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容