在〈Navigator 與 Route〉中談過「路由代表某個資源的銜接…銜接的資源除了畫面元件之外,還包含了原生平台相應的轉場動畫的效果」。
之前已經瞭解了 Flutter 動畫的原理,那麼可否自訂路由動畫呢?這可以透過 PageRouteBuilder
,例如,在〈Navigator 與 MaterialPageRoute〉中可以看到,Android 上原生的路由動畫是由下往上,若想要改為由右而左呢?可以如下:
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(
title: 'Openhome',
home: MainPage(),
));
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('主畫面'),
),
body: GestureDetector(
onTap: () {
Navigator.push(context,
// 使用 PageRouteBuilder
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500),
// 指定 pageBuilder
pageBuilder: (_, __, ___) => DetailPage('說明'),
// 指定 transitionsBuilder
transitionsBuilder: (_, animation, __, child) {
return SlideTransition(
position: Tween<Offset>(
begin: Offset(-1.0, 0.0),
end: Offset(0.0, 0.0)
).animate(animation),
child: child
);
}
)
);
},
child: Image.asset('images/caterpillar.png'),
),
);
}
}
class DetailPage extends StatelessWidget {
String title;
DetailPage(this.title);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: GestureDetector(
onTap: () => Navigator.pop(context),
child: Center(
child: Image.asset('images/caterpillar.png'),
),
),
);
}
}
建立 PageRouteBuilder
時,必須指定 pageBuilder
,它接受的三個參數分別是 BuildContext
、Animation<double>
與 Animation<double>
,如果你不想指定 transitionsBuilder
,也可以在 pageBuilder
中組建動畫,這時就會用到 Animation<double>
。
雖然可以在 pageBuilder
中組建動畫,不過建議 pageBuilder
只用來組建路由要銜接的頁面,動畫的部份由 transitionsBuilder
來處理,它接受的四個參數分別是 BuildContext
、Animation<double>
、Animation<double>
與 Widget
,最後一個參數就是 pageBuilder
傳回的 Widget
。
要建立動畫,必須有 Animation
,PageRouteBuilder
會自動建立、管理 Animation
,並傳入 pageBuilder
、transitionsBuilder
中,通常會使用第二個參數的 Animation
,第三個參數的 Animation
很少使用,這稍後再來談。
第二個參數的 Animation
,在頁面進場時,其 value
會是 0.0 到 1.0,在頁面離場時會是 1.0 到 0.0,在上例中為了要實現滑入滑出,透過 Tween<Offset>
指定了 begin
與 end
來套接了 Animation
,最後完成的效果如下:
在上例中,剛好兩個頁面都有相同的元件(圖片),這可能是個產品詳細資訊頁面的切換,而在說明中,圖片是置中,這令滑動的效果呈現了不銜接的感覺,在過場動畫中,Flutter 提供了一個有趣的 Hero
元件,先看看它可以達成什麼效果:
要達到這個效果,只需要為你的「英雄」元件加上「標籤」:
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(
title: 'Openhome',
home: MainPage(),
));
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('主畫面'),
),
body: GestureDetector(
onTap: () {
Navigator.push(context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (_, animation, __) => DetailPage('說明'),
transitionsBuilder: (_, animation, __, child) {
return SlideTransition(
position: Tween<Offset>(
begin: Offset(-1.0, 0.0),
end: Offset(0.0, 0.0)
).animate(animation),
child: child
);
}
)
);
},
// 設定英雄標籤
child: Hero(
tag: 'caterpillar',
child: Image.asset('images/caterpillar.png'),
)
),
);
}
}
class DetailPage extends StatelessWidget {
String title;
DetailPage(this.title);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: GestureDetector(
child: Center(
// 英雄標籤必須對應
child: Hero(
tag: 'caterpillar',
child: Image.asset('images/caterpillar.png'),
),
),
),
);
}
}
tag
不一定要字串,可以是任何資料,只要設定相同的 tag
就可以了,設定不同的動畫,會影響的是整個頁面,例如:
...略
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (_, animation, __) => DetailPage('說明'),
transitionsBuilder: (_, animation, animation2, child) {
return ScaleTransition(
scale: animation,
child: child
);
}
)
...略
會呈現出以下的效果:
至於英雄的移動路徑,可以藉由 placeholderBuilder
、flightShuttleBuilder
等來控制,有興趣可以自行查看 API 文件研究一下。
方才談到,PageRouteBuilder
的 pageBuilder
、transitionsBuilder
,第三個參數還有個 Animation<double>
,若目前的 PageRouteBuilder
被堆疊時,它的 value
始終是 0,若其上頭堆疊了新的路由,它的 value
會是 0.0 到 1.0,若目前的 PageRouteBuilder
上方的路由被拿掉時,它的 value
會是 1.0 到 0.0,若目前的 PageRouteBuilder
離開堆疊時,它的 value
始終是 0。
簡單來說,第三個參數可以用在目前的 PageRouteBuilder
上方被堆疊新路由時的離場動畫,以及其上方堆疊的路由拿掉時的進場動畫,會說它很少使用的原因是,程式寫起來太複雜了,如果真要點程式碼來秀一下,底下是個範例,有興趣就自己研究一下:
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(
title: 'Openhome',
home: MainPage(),
));
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('主畫面'),
),
body: GestureDetector(
onTap: () {
Navigator.push(context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (_, animation, __) => DetailPage('說明'),
transitionsBuilder: (_, animation, animation2, child) {
return ScaleTransition(
scale: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: Offset(0.0, 0.0),
end: Offset(-1, 0.0)
).animate(animation2),
child: child,
)
);
}
)
);
},
child: Hero(
tag: 'caterpillar',
child: Image.asset('images/caterpillar.png'),
)
),
);
}
}
class DetailPage extends StatelessWidget {
String title;
DetailPage(this.title);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: GestureDetector(
onTap: () {
Navigator.push(context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (_, animation, __) => Scaffold(
appBar: AppBar(
title: Text('阿散不魯'),
),
body: Center(
child: Text('更多文字'),
),
),
transitionsBuilder: (_, animation, animation2, child) {
return ScaleTransition(
scale: animation,
child: child
);
}
)
);
},
child: Center(
child: Hero(
tag: 'caterpillar',
child: Image.asset('images/caterpillar.png'),
),
),
),
);
}
}
執行的效果如下: