cover

Flutter 创建自定义路由过渡动画

TL;DR

  1. 使用 PageRouteBuilder 创建自定义路由
  2. transitionsBuilder 方法里创建过渡动画
  3. 过渡动画示例
  4. 定义全局路由过渡动画
  5. 封装自定义路由

前言

Flutter 中使用 Navigator.of(context).push(Route route); 方法进行路由跳转时就需要传一个 Route 对象,通常使用 MaterialPageRoute(builder: () {}); 创建,使用时会在路由跳转过程中添加默认的过渡动画。当需要自定义路由过渡动画时,就要使用 PageRouteBuilder,它是 Flutter 提供的用来创建自定义的路由的一个类,实例化这个类会得到一个路由对象 Route,要做的就是创建一个自定义的 Route

PageRouteBuilder

使用 PageRouteBuilder 创建自定义路由过渡动画时需要传入两个回调函数作为参数,一个必要参数 pageBuilder,这个函数用来创建跳转的页面,另一个函数 transitionsBuilder,这个函数就是实现过渡动画的地方。

transitionsBuilderchild 参数是 pageBuilder 函数返回的一个 transitionsBuilder widget 部件, pageBuilder 方法仅会在第一次构建路由的时候被调用,Flutter 能够自动避免做额外的工作,整个过渡期间 child 保存了同一个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PageRouteBuilder(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return widget;
},
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
},
);

创建自定义路由需要继承 PageRouteBuilder,然后实现自定义路由的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义
class YourRoute extends PageRouteBuilder {
final Widget page;

YourRoute(this.page)
: super(
pageBuilder: (
context,
animation,
secondaryAnimation,
) {
return page;
},
transitionsBuilder: (
context,
animation,
secondaryAnimation,
child,
) {
return child;
},
);
}

// 使用
Navigator.of(context).push(YourRoute(NewPage()));

示例

使用 FirstPageSecondPage 两个页面展示效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: FirstPage(),
);
}
}

class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Page'),
elevation: 0.0,
backgroundColor: Colors.purple,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: RaisedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SecondPage()),
);
},
child: Text('Next Page'),
),
)
],
),
backgroundColor: Colors.purple,
);
}
}

class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
elevation: 0.0,
backgroundColor: Colors.deepPurpleAccent,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go Back'),
),
)
],
),
backgroundColor: Colors.deepPurpleAccent,
);
}
}

FadeTransition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FadeRoute extends PageRouteBuilder {
final Widget page;

FadeRoute(this.page)
: super(
pageBuilder: (
context,
animation,
secondaryAnimation,
) {
return page;
},
transitionsBuilder: (
context,
animation,
secondaryAnimation,
child,
) {
return FadeTransition(
opacity: animation,
child: child,
);
},
);
}

ScaleTransition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ScaleRoute extends PageRouteBuilder {
final Widget page;

ScaleRoute(this.page)
: super(
pageBuilder: (
context,
animation,
secondaryAnimation,
) {
return page;
},
transitionsBuilder: (
context,
animation,
secondaryAnimation,
child,
) {
return ScaleTransition(
alignment: Alignment.bottomLeft,
scale: Tween(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
),
),
child: child,
);
},
transitionDuration: Duration(seconds: 1),
);
}

Navigator.of(context).push(ScaleRoute(SecondPage()));

RotationTransition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class RotationRoute extends PageRouteBuilder {
final Widget page;

RotationRoute(this.page)
: super(
pageBuilder: (
context,
animation,
secondaryAnimation,
) {
return page;
},
transitionsBuilder: (
context,
animation,
secondaryAnimation,
child,
) {
Animation myAnimation = CurvedAnimation(
parent: animation,
curve: Curves.easeInBack,
);

return RotationTransition(
turns: myAnimation,
child: child,
);
},
transitionDuration: Duration(seconds: 1),
);
}

Navigator.of(context).push(RotationRoute(SecondPage()));

ScaleRotationRoute

结合两个过渡动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ScaleRotationRoute extends PageRouteBuilder {
final Widget page;

ScaleRotationRoute(this.page)
: super(
pageBuilder: (
context,
animation,
secondaryAnimation,
) {
return page;
},
transitionsBuilder: (
context,
animation,
secondaryAnimation,
child,
) {
return ScaleTransition(
scale: animation,
child: RotationTransition(
turns: Tween(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(parent: animation, curve: Curves.linear),
),
child: child,
),
);
},
transitionDuration: Duration(milliseconds: 800),
);
}

Navigator.of(context).push(ScaleRotationRoute(SecondPage()));

TransformRoute

使用 Transform 部件创造 3D 效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import 'dart:math' show pi;

class TransformRoute extends PageRouteBuilder {
final Widget page;

TransformRoute(this.page)
: super(
pageBuilder: (
context,
animation,
secondaryAnimation,
) {
return page;
},
transitionsBuilder: (
context,
animation,
secondaryAnimation,
child,
) {
return Transform(
transform: Matrix4.identity()
// 类似于 CSS 里面 `perspective` 属性,确定 z=0 平面与用户之间的距离
..setEntry(3, 2, 0.0001)
..rotateX(animation.value * pi * 2)
..rotateY(animation.value * pi * 2),
alignment: FractionalOffset.center,
child: child,
);
},
transitionDuration: Duration(seconds: 2),
);
}

Navigator.of(context).push(TransformRoute(SecondPage()));

EnterExitRoute

同时为进入页面和退出页面添加动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class EnterExitRoute extends PageRouteBuilder {
final Widget enterPage;
final Widget exitPage;

EnterExitRoute(this.enterPage, this.exitPage)
: super(
pageBuilder: (
context,
animation,
secondaryAnimation,
) {
return exitPage;
},
transitionsBuilder: (
context,
animation,
secondaryAnimation,
child,
) =>
Stack(
children: [
SlideTransition(
position: Tween<Offset>(
begin: Offset(0.0, 0.0),
end: Offset(-1.0, 0.0),
).animate(
CurvedAnimation(parent: animation, curve: Curves.easeIn),
),
child: enterPage,
),
SlideTransition(
position: Tween<Offset>(
begin: Offset(1.0, 0.0),
end: Offset.zero,
).animate(
CurvedAnimation(parent: animation, curve: Curves.easeInOut),
),
child: exitPage,
)
],
),
);
}

Navigator.of(context).push(
EnterExitRoute(FirstPage(), SecondPage()),
);

使用 Navigator.pushNamed 方法跳转

onGenerateRoute 对跳转路由的 name 进行判断,对特定的路由添加过渡动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: FirstPage(),
onGenerateRoute: (settings) {
switch (settings.name) {
case '/second':
return ScaleRoute(SecondPage());
break;
default:
return null;
}
},
);
}
}

Navigator.pushNamed(context, '/second', arguments: {});

设置全局的路由过渡动画

Flutter 的默认路由过渡动画是由 buildTransitions 方法创建的,它使用的是 Theme.of(context).pageTransitionsTheme方法,因此可以定义全局的路由跳转过渡动画。

1
2
3
4
5
@override
Widget buildTransitions(context, animation, secondaryAnimation, child) {
final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}

首先自定义一个 TransitionBuilderbuildTransitions 方法返回跳转页面。然后配置 themepageTransitionsTheme,设置对应的平台,最后在使用 MaterialPageRoute 或者 CupertinoPageRoute 进行页面跳转时就会有自定义的过渡动画了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ScaleTransitionBuilder extends PageTransitionsBuilder {
@override
Widget buildTransitions<T>(
route,
context,
animation,
secondaryAnimation,
child,
) {
return ScaleTransition(
scale: CurvedAnimation(parent: animation, curve: Curves.easeIn),
child: child,
);
}
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: FirstPage(),
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: ScaleTransitionBuilder(),
TargetPlatform.iOS: ScaleTransitionBuilder(),
},
),
),
);
}
}

Navigator.push(context, MaterialPageRoute(builder: (ctx) => SecondPage()));

将动画封装成一个库

将自定义的路由过渡动画封装起来方便使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
enum TransitionType {
fade,
scale,
rotate,
transform,
}

class PageTransition extends PageRouteBuilder {
PageTransition(TransitionType type, Widget page, Duration time)
: super(
pageBuilder: (
context,
animation,
secondaryAnimation,
) {
return page;
},
transitionsBuilder: (
context,
animation,
secondaryAnimation,
child,
) {
switch (type) {
case TransitionType.fade:
return FadeTransition(opacity: animation, child: child);
break;
case TransitionType.scale:
return ScaleTransition(
scale: Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: animation, curve: Curves.easeInOut),
),
child: child,
);
break;
case TransitionType.rotate:
return RotationTransition(
turns: CurvedAnimation(
parent: animation,
curve: Curves.easeInBack,
),
child: child,
);
break;
case TransitionType.transform:
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.0001)
..rotateX(animation.value * pi * 2)
..rotateY(animation.value * pi * 2),
alignment: FractionalOffset.center,
child: child,
);
break;
default:
return child;
};
},
transitionDuration: time,
);
}

// 使用
Navigator.push(
context,
PageTransition(
TransitionType.rotate,
SecondPage(),
Duration(milliseconds: 800),
),
}

参考文章

为页面切换加入动画效果

Everything you need to know about Flutter page route transition

Perspective on Flutter

Buy Me A Coffee
← 为 Flutter 应用添加搜索功能 Egg.js 上传接口开发总结 →