今天給你們將兩個關於路由的騷操做,雖說項目裏不太會用到,可是看看漲漲姿式老是好的。
我想你們應該都知道,Flutter在push/pop路由的時候,都是能夠自定義動畫的,路由動畫在Flutter裏面寫起來很是的靈活,通常來講在push的時候帶上一個自定義的Route就能夠了:git
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, _, __) {
return ProductDetailPage(
product: product,
);
},
transitionDuration:
const Duration(milliseconds: 500),
transitionsBuilder:
(_, animation, __, child) {
return FadeTransition(
opacity: animation,
child: FadeTransition(
opacity:
Tween(begin: 0.5, end: 1.0).animate(animation),
child: child,
),
);
}),
);
複製代碼
transitionDuration
定義路由動畫時間,transitionsBuilder
中能夠自定義路由動畫,旋轉、位移、縮放等等均可以。api
那麼,你有沒有想過下面這兩個問題呢?bash
其實,這兩個問題都是屬於路由範疇:less
問題一一樣可使用PageRouteBuilder
來解決,只是寫法有些不一樣。通常來講咱們都是直接在MaterialApp
中設置home
屬性,配置初始頁面,這樣寫明顯是不行的啦,home
屬性接受的是一個Widget
,而不是一個路由。莫急,很快就教大家一個不同的設置初始頁面的寫法。ide
MaterialApp(
home: MyHomePage(),
)
複製代碼
問題二徹底能夠在頁面打開的時候播放一個動畫A,而後監聽頁面關閉,關閉時倒着播放動畫A。可是這麼作明顯不太優雅,咱們換個角度來思考,從路由的角度來講,你的圖片進入/退出動畫不就是隨着路由動畫來進行的嗎?它們徹底可使用同一個controller
,那麼你只需定義好動畫,將其和路由controller
綁定,何時開始動畫,何時結束,都不須要你來手動控制。函數
一樣也是在MaterialApp
中設置,可是不是設置home
屬性,而是onGenerateRoute
屬性,它和routes
屬性很像,也是用來配置路由的:佈局
MaterialApp(
onGenerateRoute: (settings) {
if (settings.isInitialRoute) {
return createInitialRoute();
}
},
)
Route<dynamic> createInitialRoute() {
return PageRouteBuilder(
transitionDuration: const Duration(seconds: 1),
pageBuilder: (BuildContext context, _, __) {
return MyHomePage(title: 'Flutter YMUI');
},
transitionsBuilder: (_, animation, __, child) {
return RotationTransition(
turns: Tween(begin: 0.0, end: 1.0).animate(animation),
child: ScaleTransition(
scale: Tween(begin: 0.0, end: 1.0).animate(animation),
child: child,
),
);
});
}
複製代碼
onGenerateRoute
屬性會告訴咱們一個RouteSettings
值,這個值有兩個重要的api:動畫
settings.isInitialRoute
判斷是不是初始路由;settings.name
返回路由名(和routes
屬性中的路由名同樣,是一個字符串,用於和push/pop時的name配對) 所以,咱們根據settings.isInitialRoute
判斷是不是初始路由,若是是,那麼就替換成咱們的自定義PageRouteBuilder
,這個時候是否是就很熟悉啦,咱們能夠肆意添加路由動畫了。運行一下看下效果:ui
PS:關於home
、routes
、onGenerateRoute
和onUnknownRoute
屬性的優先級:this
Navigator
會按照home---->routes---->onGenerateRoute---->onUnknownRoute
的順序去尋找路由:
home
,也就是初始路由,路徑爲:/
;routes
,也就是咱們通常定義路由映射的地方,它的優先級會比onGenerateRoute
,若是二者定義的路由又重複,確定是先找routes
中的;onGenerateRoute
優先級最低,用來處理home
和routes
都沒有處理的路由,因此通常返回非空值;onUnknownRoute
若是說某個路由上面三個都沒處理,那麼就會由onUnknownRoute
來處理這個路由。咱們先看一下效果,理解一下咱們的需求:
其實很簡單,就是圖片和文字從頁面底部進入和退出,你徹底監聽頁面的打開和退出,手動執行動畫,只是這樣有點兒麻煩。獲取路由動畫須要用到ModalRoute
這個類中的
ModalRoute.of(context).animation
方法。
PageRouteBuilder<T> ----> PageRoute<T> ----> ModalRoute<T> ----> TransitionRoute<T> ----> OverlayRoute<T> ----> Route<T>
,咱們找源碼能夠發現控制路由動畫的
controller
是存在於
TransitionRoute
中的,因此它的子類都是能夠獲取到這個屬性的,再仔細看源碼就能夠發現,子類中的
ModalRoute
有一個
.of(context)
的工廠函數,能夠獲取到
Route
實例,那麼綁定就能夠這麼寫了(就是將
controller
賦給你自定義的圖片變換動畫的
parent
):
Animation<double> controller;
Animation<Offset> imageTranslation;
void _buildAniamtion(){
controller = ModalRoute.of(context).controller;
imageTranslation = Tween(
begin: Offset(0.0, 2.0),
end: Offset(0.0, 0.0),
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(0.0, 0.67, curve: Curves.fastOutSlowIn),
),
);
}
複製代碼
其實這麼寫最終運行的時候,效果也是OK的,可是你會發現有一個警告:
info: The member 'controller' can only be used within instance members of subclasses of 'package:flutter/src/widgets/routes.dart'.
也就是說,Flutter是不建議你在頁面中獲取這個controller
的,由於這個屬性有@protected
註解,雖然說最後運行效果是沒問題的,可是有個警告老是不太好的。源碼註釋終有一句話,說是controller
控制的動畫是經過animation
暴露出來的,因此,咱們不妨來看看這個animation
。咱們很容易就能找到animation
和secondaryAnimation
成員變量,是否是看起來很熟悉?建立路由時的pageBuilder
就給了咱們兩個動畫值,就是這兩個動畫啦。因此ModalRoute.of(context).animation
獲取到的animation
就是PageRouteBuilder
構建時,pageBuilder
屬性中包含的animation
:
/// animation 對應 ModalRoute.of(context).animation
/// secondaryAnimation 對應 ModalRoute.of(context).secondaryAnimation
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return TestPage();
},
複製代碼
因此,咱們能夠這麼定義咱們的動畫,將每個animation
都和路由的animation
綁定起來就能夠了(AnimationController
是繼承自Animation<double>
的,因此這裏將controller
和animation
賦給你自定義的圖片變換動畫的parent
是一個效果,這就是爲何最終運行效果是同樣的了):
Animation<double> navAnimation;
Animation<Offset> imageTranslation;
Animation<Offset> textTranslation;
Animation<double> imageOpacity;
Animation<double> textOpacity;
void _buildAniamtion(){
if (navAnimation == null) {
navAnimation = ModalRoute.of(context).animation;
imageTranslation = Tween(
begin: Offset(0.0, 2.0),
end: Offset(0.0, 0.0),
).animate(
CurvedAnimation(
parent: navAnimation,
curve: Interval(0.0, 0.67, curve: Curves.fastOutSlowIn),
),
);
imageOpacity = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: navAnimation,
curve: Interval(0.0, 0.67, curve: Curves.easeIn),
),
);
textTranslation = Tween(
begin: Offset(0.0, 1.0),
end: Offset(0.0, 0.0),
).animate(
CurvedAnimation(
parent: navAnimation,
curve: Interval(0.34, 0.84, curve: Curves.ease),
),
);
textOpacity = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: navAnimation,
curve: Interval(0.34, 0.84, curve: Curves.linear),
),
);
}
}
複製代碼
接下來就剩最後一個問題了,何時來進行這個綁定操做呢?通常來講,動畫的初始化咱們會選擇在initState()
中進行,可是若是咱們將_buildAniamtion()
方法放入initState()
中執行的話,會報以下的錯誤:
The following assertion was thrown building Builder:
inheritFromWidgetOfExactType(_ModalScopeStatus) or inheritFromElement() was called before _TestPageState.initState() completed.
When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget.
Typically references to to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.
劃報錯重點:最後一段中:can be placed in the didChangeDependencies method
,嗯,寫的很清楚了,咱們在didChangeDependencies()
中初始化動畫就能夠了:
@override
void didChangeDependencies() {
super.didChangeDependencies();
_buildAniamtion();
}
複製代碼
didChangeDependencies()
是緊接着initState()
後面執行的,源碼註釋中有一句:
It is safe to call [BuildContext.inheritFromWidgetOfExactType] from this method.
二者的區別我也說不清楚,反正若是你在initState()
中執行某些方法報錯,不妨試試放到didChangeDependencies()
中去。
補充:評論有人說將操做放置到 addPostFrameCallback((timeStamp){ })
,也就是第一幀繪製完成以後,聽起來頗有道理對不對??可是跟佈局渲染順序是矛盾的,由於第一幀繪製完成也就意味着build()
方法走完了,可是咱們的佈局初始化的時候確定是須要用到動畫value
的,好比下面這樣:
FractionalTranslation(
translation: imageTranslation.value,
hild: HeaderImage(),
),
複製代碼
而初始化佈局的時候,咱們的自定義的圖片aniamtion
尚未初始化和綁定好呢,imageTranslation
尚未值,會報錯的。若是非要這麼寫,那麼就須要修改一下佈局了,咱們能夠先給imageTranslation
賦一個默認值,而後在addPostFrameCallback((timeStamp){ })
監聽中再去修改這個值,而後再刷新狀態:
FractionalTranslation(
translation:
imageTranslation == null ? 0.0 : imageTranslation.value,
child: HeaderImage(),
),
WidgetsBinding.instance.addPostFrameCallback((callback) {
_buildAniamtion();
setState(() { });
});
複製代碼
TestPage.dart
完整代碼以下:
class TestPage extends StatefulWidget {
@override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
Animation<double> controller;
Animation<Offset> imageTranslation;
Animation<Offset> textTranslation;
Animation<double> imageOpacity;
Animation<double> textOpacity;
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_buildAniamtion(); // 此處代碼省略,見上面
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: controller,
builder: (BuildContext context, Widget child) {
return Column(
children: <Widget>[
FractionalTranslation(
translation: imageTranslation.value,
child: HeaderImage(),
),
Expanded(
child: FractionalTranslation(
translation: textTranslation.value,
child: AppText(),
),
),
],
);
},
),
);
}
}
class AppText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 12.0, right: 12.0, top: 44.0),
child: Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras non lorem non justo congue feugiat ut a enim. Ut et sem nec lacus aliquet gravida. Mauris viverra lectus nec vulputate placerat. Nullam sit amet blandit massa, volutpat blandit arcu. Vivamus eu tellus tincidunt, vestibulum neque eu, sagittis neque. Phasellus vitae rutrum magna, eu finibus mi. Suspendisse eget laoreet metus. In mattis dui vitae vestibulum molestie. Curabitur bibendum ut purus in faucibus.",
style: Theme.of(context).textTheme.body2,
),
);
}
}
class HeaderImage extends StatelessWidget {
const HeaderImage({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: Image.asset(
"images/food01.jpeg",
height: 300.0,
fit: BoxFit.cover,
),
);
}
}
複製代碼
PS:如何判斷當前頁面是不是初始頁面?如何獲取當前頁面路由名?
ModalRoute.of(context).settings
能夠拿到當前路由的基礎配置信息,RouteSettings
這個類一開始的時候就提到過啦,能夠取到布爾值isInitialRoute
和路由名。
關於ModalRoute
的更多屬性,能夠看下這個:Flutter當前路由屬性詳解