在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
要從狀態創建,則可以使用TickerProviderStateMixin
和SingleTickerProviderStateMixin
類來獲取合適的狀態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,由動畫結束按鈕被降低到width
的70.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),
)
最后,我們希望按鈕容器增長并覆蓋整個屏幕。為此,我們將首先刪除整個頁面的padding
(top
和bottom
),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
。在這個小部件中,我們將opacity
從1.0更改為0.0,使顏色消失。我們將此小部件鏈接到CurvedAnimation
使用animate()
。
接下來,我們創建一個名為的新窗口小部件FadeBox
并fadeScreenAnimation
在其中調用。
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