持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第22天,点击查看活动详情
概述:
为了避免用户误触返回按钮而导致 App 退出,在很多 App 中都拦截了用户点击返回键的按钮,然后进行一些防误触判断,比如当用户在某一个时间段内点击两次时,才会认为用户是要退出(而非误触)。Flutter中可以通过WillPopScope来实现返回按钮拦截,
WillPopScope的默认构造函数:
const WillPopScope({
...
required WillPopCallback onWillPop,
required Widget child
})
onWillPop是一个回调函数,当用户点击返回按钮时被调用(包括导航返回按钮及Android物理返回按钮)。该回调需要返回一个Future对象,如果返回的Future最终值为false时,则当前路由不出栈(不会返回);最终值为true时,当前路由出栈退出。我们需要提供这个回调来决定是否退出。
双击退出
为了防止用户误触返回键退出,我们拦截返回事件。当用户在1秒内点击两次返回按钮时,则退出;如果间隔超过1秒则不退出,并重新记时。代码如下:
WillPopScope(
child: Container(
alignment: Alignment.center,
child: Text("1秒内连续按两次返回键退出"),
),
onWillPop: () async {
if (_lastPressedAt != null ||
DateTime.now().difference(_lastPressedAt!) >
Duration(seconds: 1)) {
//两次点击间隔超过1秒则重新计时
_lastPressedAt = DateTime.now();
print('false');
return false;
}
print('true');
return true;
});
App中有多个Navigator
我们的App通常是在MaterialApp和CupertinoApp下,MaterialApp和CupertinoApp本身有一个Navigator,所以默认情况下调用Navigator.pop或者Navigator.push就是在操作此Navigator。不过在一些情况下,我们希望有自己定义的Navigator,比如如下场景:
- 在页面底部有一个常驻bar,其上展示内容,这个常驻bar就需要一个自己的Navigator。
- 在使用TabView、BottomNavigationBar、CupertinoTabView这些组件时,希望有多个Tab,但每个Tab中有自己的导航行为,这时需要给每一个Tab加一个Navigator。
如首页:
class MyHomePage1 extends StatefulWidget {
MyHomePage1({required Key key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePage1State createState() => _MyHomePage1State();
}
class _MyHomePage1State extends State<MyHomePage1> {
GlobalKey<NavigatorState> _key = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar('Navigator'),
body: WillPopScope(
onWillPop: () async {
if (_key.currentState!.canPop()) {
_key.currentState!.pop();
return false;
}
return true;
},
child: Column(
children: <Widget>[
Expanded(
child: Navigator(
key: _key,
onGenerateRoute: (RouteSettings settings) =>
MaterialPageRoute(builder: (context) {
return OnePage();
}),
),
),
Container(
height: 64,
color: Colors.blue,
alignment: Alignment.center,
child: Text('底部Bar'),
)
],
)),
);
}
}
class OnePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: ElevatedButton(
child: Text('去下一个页面'),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return TwoPage();
}));
},
),
),
),
);
}
}
class TwoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Text('这是第二个页面'),
),
),
);
}
}
手势返回
WillPopScope
不支持手势返回,iOSCupertinoPageRoute
不支持哪些情况,通过源码可知,当页面处于首屏
static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
// 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 dismissible by back swipe.
if (route.fullscreenDialog)
return false;
// If we're in an animation already, we cannot be manually swiped.
if (route.animation!.status != AnimationStatus.completed)
return false;
// If we're being popped into, we also cannot be swiped until the pop above
// it completes. This translates to our secondary animation being
// dismissed.
if (route.secondaryAnimation!.status != AnimationStatus.dismissed)
return false;
// If we're in a gesture already, we cannot start another.
if (isPopGestureInProgress(route))
return false;
// Looks like a back gesture would be welcome!
return true;
}
按钮返回可以通过WillPopScope
处理,手势返回如何监听处理呢?
这里使用NotificationListener
监听PageView
一旦滑动到边界就主动pop页面,同时还要修改它的physics
为ClampingScrollPhysics
因为iOS默认不会回调OverscrollNotification
。
关于手势返回的初步方式,想办法对手势进行监听并做响应的响应:
class SwipeLeftReturnWidget extends StatelessWidget {
final Widget child;
bool _isOverscroll = false;
SwipeLeftReturnWidget(this.child);
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
child: child,
onNotification: (notification){
print(notification.runtimeType);
switch(notification.runtimeType){
case ScrollUpdateNotification:
_isOverscroll = false;
break;
case ScrollEndNotification:
if(_isOverscroll){
//返回上一页
Navigator.of(context).pop();
}
break;
case OverscrollNotification:
if(notification.depth == 0 && notification.metrics.extentBefore <= 0){
//处于第一个tab并且继续左滑
_isOverscroll = true;
}
break;
default:
break;
}
return false;
},
);
}
}
总结:
本篇主要介绍了WillPopScope
对导航返回事件的拦截处理,如业务界面正在编辑过程发送误触,或者操作中点击退出的一些询问处理等,此外还有手势返回的监听和相关处理等。