在 flutter 的页面跳转中,flutter 已经实现了iOS的右滑退出手势,但是有时候有时候会失效,带着这个问题研究了一下源码.
页面跳转会用到 MaterialPageRoute
或 CupertinoPageRoute
这两个类,MaterialPageRoute
是 Android 风格的,CupertinoPageRoute
是 iOS 风格的.
如果用 MaterialPageRoute
跳转页面. iOS 端有返回手势, Android 端没有返回手势.
看源码, flutter/src/material/page.dart
/// A delegate PageRoute to which iOS themed page operations are delegated to.
/// It's lazily created on first use.
CupertinoPageRoute get _cupertinoPageRoute {
assert(_useCupertinoTransitions);
_internalCupertinoPageRoute ??= new CupertinoPageRoute(
builder: builder, // Not used.
fullscreenDialog: fullscreenDialog,
hostRoute: this,
);
return _internalCupertinoPageRoute;
}
CupertinoPageRoute _internalCupertinoPageRoute;
/// Whether we should currently be using Cupertino transitions. This is true
/// if the theme says we're on iOS, or if we're in an active gesture.
bool get _useCupertinoTransitions {
return _internalCupertinoPageRoute?.popGestureInProgress == true
|| Theme.of(navigator.context).platform == TargetPlatform.iOS;
}
...
@override
Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) {
if (_useCupertinoTransitions) {
return _cupertinoPageRoute.buildTransitions(context, animation, secondaryAnimation, child);
} else {
return new _MountainViewPageTransition(
routeAnimation: animation,
child: child,
fade: true,
);
}
}
关键函数 _useCupertinoTransitions
和 _cupertinoPageRoute
.
通过 _useCupertinoTransitions
判断当前设备是否是 iOS , 如果是 iOS, 就调用 CupertinoPageRoute
类的对象函数.
通过 iOS 端有返回手势, Android 端没有返回手势
现象,和源码分析出 flutter 中的右滑返回的代码是通过 CupertinoPageRoute
的对象函数 buildTransitions
返回的 Widget 实现的.
让我们来看一下源码,flutter/src/cupertino/route.dart
@override
Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) {
if (fullscreenDialog) {
return new CupertinoFullscreenDialogTransition(
animation: animation,
child: child,
);
} else {
return new CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
// In the middle of a back gesture drag, let the transition be linear to
// match finger motions.
linearTransition: popGestureInProgress,
child: new _CupertinoBackGestureDetector(
enabledCallback: () => popGestureEnabled,
onStartPopGesture: _startPopGesture,
child: child,
),
);
}
}
其中 popGestureEnabled
引起了注意
bool get popGestureEnabled {
final PageRoute route = hostRoute ?? this;
// If there's nothing to go back to, then obviously we don't support
// the back gesture.
if (route.isFirst)
return false;
// If the route wouldn't actually pop if we popped it, then the gesture
// would be really confusing (or would skip internal routes), so disallow it.
if (route.willHandlePopInternally)
return false;
// If attempts to dismiss this route might be vetoed such as in a page
// with forms, then do not allow the user to dismiss the route with a swipe.
if (route.hasScopedWillPopCallback)
return false;
// Fullscreen dialogs aren't dismissable by back swipe.
if (fullscreenDialog)
return false;
// If we're in an animation already, we cannot be manually swiped.
if (route.controller.status != AnimationStatus.completed)
return false;
// If we're in a gesture already, we cannot start another.
if (popGestureInProgress)
return false;
// Looks like a back gesture would be welcome!
return true;
}
有几个条件是禁止 pop 手势的.
第一个条件不用说了,都是第一屏了,没有可返回的.
后两个条件也不需要说,都是某一时间段内禁用返回手势.
咱说说 fullscreenDialog
, route.hasScopedWillPopCallback
, route.willHandlePopInternally
这三个条件.
什么情况下 fullscreenDialog
为 true.
fullscreenDialog 是 MaterialPageRoute
和 CupertinoPageRoute
的一个属性.
应用举例:
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Weight Tracker',
theme: new ThemeData(
primarySwatch: Colors.green,
),
home: new HomePage(title: 'Weight Tracker'),
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
floatingActionButton: new FloatingActionButton(
onPressed: _openFullscreenDialog,
tooltip: 'Add new weight entry',
child: new Icon(Icons.add),
),
);
}
void _openFullscreenDialog(){
Navigator.of(context).push(new MaterialPageRoute(
builder: (BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Text("food"),
);;
},
fullscreenDialog: true));
}
}
在最后一行代码将 fullscreenDialog
属性设置为 true
.当点击按钮后弹出的对话框左上角的按钮是 " X ".
这种情况下返回手势失效.
这个是判断是否实现 WillPop 回调函数.如果实现了就禁止.
要添加回调需要调用下面两个函数
route?.addScopedWillPopCallback()
route?.removeScopedWillPopCallback();
添加和删除必须成对出现. flutter 对这个功能做了一个封装,那就是 WillPopScope
类
应用场景是监听实体/虚拟返回键和AppBar返回键.
举个例子,只修改上面的 _HomePageState 类,如下
class _HomePageState extends State{
_showDialog() {
showDialog(
context: context,
child: new AlertDialog(content: new Text('退出当前界面'), actions: [
new FlatButton(
onPressed: () {
// Navigator.of(context).pop();
SystemNavigator.pop();
},
child: new Text('确定'))
]),
);
}
Future _requestPop() {
_showDialog();
return new Future.value(false);
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return new WillPopScope(
child: new Scaffold(
appBar: new AppBar(
title: new Text('WillPopScope'),
centerTitle: true,
),
body: new Center(
child: new Text('strawberry'),
)),
onWillPop: _requestPop);
}
}
如何让这个返回 true, 那就是调用 addLocalHistoryEntry
函数.
正常情况下,Navigator.of(context).push(A)
将一个页面 A 压入路由,如果要将这个页面弹出路由,在 A 页面调用 Navigator.of(context).pop()
或者 点击实体/虚拟返回键就好了.
但是现在有一个需求就是要在 A 页面填表单,内容比较多,需要下一页下一页的改变界面的问题.如果这个时候点击了实体/虚拟返回键,或者调用了 Navigator.of(context).pop()
导致 A 页面弹出路由,这种交互就不够友好.
要改变这种状况,那就用到 addLocalHistoryEntry
函数了
例子
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (BuildContext context) => HomePage(),
'/second_page': (BuildContext context) => SecondPage(),
},
);
}
}
class HomePage extends StatefulWidget {
HomePage();
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('HomePage'),
// Press this button to open the SecondPage.
RaisedButton(
child: Text('Second Page >'),
onPressed: () {
Navigator.pushNamed(context, '/second_page');
},
),
],
),
),
);
}
}
class SecondPage extends StatefulWidget {
@override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State {
bool _showRectangle = false;
void _navigateLocallyToShowRectangle() async {
// This local history entry essentially represents the display of the red
// rectangle. When this local history entry is removed, we hide the red
// rectangle.
setState(() => _showRectangle = true);
ModalRoute.of(context).addLocalHistoryEntry(
LocalHistoryEntry(
onRemove: () {
// Hide the red rectangle.
setState(() => _showRectangle = false);
}
)
);
}
@override
Widget build(BuildContext context) {
final localNavContent = _showRectangle
? Container(
width: 100.0,
height: 100.0,
color: Colors.red,
)
: RaisedButton(
child: Text('Show Rectangle'),
onPressed: _navigateLocallyToShowRectangle,
);
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
localNavContent,
RaisedButton(
child: Text('< Back'),
onPressed: () {
// Pop a route. If this is pressed while the red rectangle is
// visible then it will will pop our local history entry, which
// will hide the red rectangle. Otherwise, the SecondPage will
// navigate back to the HomePage.
Navigator.of(context).pop();
},
),
],
),
),
);
}
}