写在前面
在 Flutter 里,默认是支持 iOS 的屏幕边缘侧滑返回的,但如果由于一些需求,我们对 WillPopScope 的onWillPop 回调进行了重写,就会导致这个特性失效。
内容
一般情况下,我们没有对页面添加 WillPopScope 这个 Widget 并重写它的 onWillPop 方法,在 iOS 上是可以在屏幕左侧边缘进行侧滑返回的。但如果我们重写了 onWillPop 方法,就会发现这个手势特性失效了。在router.dart 文件里,我们可以知道为什么:
// route.dart
static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
...
// 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;
....
// Looks like a back gesture would be welcome!
return true;
}
// routes.dart
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
...
void addScopedWillPopCallback(WillPopCallback callback) {
assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
_willPopCallbacks.add(callback);
}
/// True if one or more [WillPopCallback] callbacks exist.
///
/// This method is used to disable the horizontal swipe pop gesture supported
/// by [MaterialPageRoute] for [TargetPlatform.iOS] and
/// [TargetPlatform.macOS]. If a pop might be vetoed, then the back gesture is
/// disabled.
///
/// The [buildTransitions] method will not be called again if this changes,
/// since it can change during the build as descendants of the route add or
/// remove callbacks.
///
/// See also:
///
/// * [addScopedWillPopCallback], which adds a callback.
/// * [removeScopedWillPopCallback], which removes a callback.
/// * [willHandlePopInternally], which reports on another reason why
/// a pop might be vetoed.
@protected
bool get hasScopedWillPopCallback {
return _willPopCallbacks.isNotEmpty;
}
...
}
// will_pop_scope.dart
class _WillPopScopeState extends State<WillPopScope> {
ModalRoute<dynamic>? _route;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (widget.onWillPop != null)
_route?.removeScopedWillPopCallback(widget.onWillPop!);
_route = ModalRoute.of(context);
if (widget.onWillPop != null)
_route?.addScopedWillPopCallback(widget.onWillPop!);
}
@override
void didUpdateWidget(WillPopScope oldWidget) {
super.didUpdateWidget(oldWidget);
assert(_route == ModalRoute.of(context));
if (widget.onWillPop != oldWidget.onWillPop && _route != null) {
if (oldWidget.onWillPop != null)
_route!.removeScopedWillPopCallback(oldWidget.onWillPop!);
if (widget.onWillPop != null)
_route!.addScopedWillPopCallback(widget.onWillPop!);
}
}
}
由于我们重写了 onWillPop ,它会把这个回调加入到路由里的_willPopCallbacks 列表里,因此这里就禁用了退出的手势操作。
将 onWillPop 置为 null
假如我们确实需要在一些情况下使用到 WillPopScope ,那么应该怎么处理呢?我们可以看到把回调加入到队列的前提是 widget.onWillPop != null ,所以我们可以结合具体情况,提供一个触发条件,来改变这个onWillPop ,类似下面:
bool condition = true;
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: condition ? () async{
// do something
return true;
} : null,
child: Scaffold(
);
}
这样我们就可以在较为灵活地处理这个问题了。
在 Document that WillPopScope prevents swipe to go back on MaterialPageRoute #14203 这个问题里,我们可以看到另外的处理方法。
重写 MaterialPageRoute
class CustomMaterialPageRoute extends MaterialPageRoute {
@override
@protected
bool get hasScopedWillPopCallback {
return false;
}
CustomMaterialPageRoute({
required WidgetBuilder builder,
RouteSettings? settings,
bool maintainState = true,
bool fullscreenDialog = false,
}) : super(
builder: builder,
settings: settings,
maintainState: maintainState,
fullscreenDialog: fullscreenDialog,
);
}
通过重写 MaterialPageRoute,直接将 hasScopedWillPopCallback 改为 false,并在跳转的时候修改实现:
// old:
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return const Second();
}),
);
// new:
Navigator.push(
context,
CustomMaterialPageRoute(builder: (context) {
return const Second();
}),
);
但此举会导致的一个问题是,如果你想在返回页面的时候传递数据回去,即使你重写了 onWillPop 回调,通过手势返回是不会返回数据的,但 AppBar 上的返回按钮,还是可以将数据返回的。
其它
在处理底部弹窗的返回处理的时候,发现了一个 Android 与 iOS 上的交互不同之处。在 Android 上只要手指在屏幕左侧边缘右滑,就可以收起弹窗。而 iOS 需要你的手指有一个向下的方向,才能收起弹窗。
在 iOS 上,我们总可以感觉到它的手势操作是非常跟手的,如果这个东西是从哪来的,它就应该原路回去。具体可以看[WWDC 2018] Designing Fluid Interfaces 流畅的界面设计。
所以我想这可能是 WillPopScope 在 Android 和 iOS 上的表现有所差异的原因。在 Android 上,手指侧滑,就等同于底部返回按钮,很明确的表示要回退。而在 iOS 上,因为它非常的跟手,你虽然在屏幕边缘侧滑,但只要你手指还在屏幕上,你是可以再放回去的。这样在 WillPopScope 的判断意图上,就可能不像 Android 那么清晰。
|