flutter 页面右滑返回失效研究

flutter pop Gesture

在 flutter 的页面跳转中,flutter 已经实现了iOS的右滑退出手势,但是有时候有时候会失效,带着这个问题研究了一下源码.

页面跳转会用到 MaterialPageRouteCupertinoPageRoute 这两个类,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<T> get _cupertinoPageRoute {
    assert(_useCupertinoTransitions);
    _internalCupertinoPageRoute ??= new CupertinoPageRoute<T>(
      builder: builder, // Not used.
      fullscreenDialog: fullscreenDialog,
      hostRoute: this,
    );
    return _internalCupertinoPageRoute;
  }
  CupertinoPageRoute<T> _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<double> animation, Animation<double> 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<double> animation, Animation<double> 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<T>(
          enabledCallback: () => popGestureEnabled,
          onStartPopGesture: _startPopGesture,
          child: child,
        ),
      );
    }
  }

其中 popGestureEnabled 引起了注意

  bool get popGestureEnabled {
    final PageRoute<T> 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 手势的.

  • route.isFirst : 当前页面是首屏
  • route.willHandlePopInternally : 当前页面有 通过 addLocalHistoryEntry 修改页面的
  • route.hasScopedWillPopCallback : 实现 WillPop 回调函数的.就是有可能回退页面被拒绝的情况
  • fullscreenDialog : 全屏的对话框
  • route.controller.status != AnimationStatus.completed : 当前页面动画未完成
  • popGestureInProgress : 当前页面已经有一个手势在运行

第一个条件不用说了,都是第一屏了,没有可返回的.

后两个条件也不需要说,都是某一时间段内禁用返回手势.

咱说说 fullscreenDialog, route.hasScopedWillPopCallback, route.willHandlePopInternally 这三个条件.

fullscreenDialog

什么情况下 fullscreenDialog 为 true.

fullscreenDialog 是 MaterialPageRouteCupertinoPageRoute 的一个属性.

应用举例:

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<HomePage> {
  @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 ".
这种情况下返回手势失效.
在这里插入图片描述

route.hasScopedWillPopCallback

这个是判断是否实现 WillPop 回调函数.如果实现了就禁止.

要添加回调需要调用下面两个函数

route?.addScopedWillPopCallback()
route?.removeScopedWillPopCallback();

添加和删除必须成对出现. flutter 对这个功能做了一个封装,那就是 WillPopScope

应用场景是监听实体/虚拟返回键和AppBar返回键.

举个例子,只修改上面的 _HomePageState 类,如下

class _HomePageState extends State<HomePage>{

  _showDialog() {
    showDialog<Null>(
      context: context,
      child: new AlertDialog(content: new Text('退出当前界面'), actions: <Widget>[
        new FlatButton(
            onPressed: () {
//              Navigator.of(context).pop();
              SystemNavigator.pop();
            },
            child: new Text('确定'))
      ]),
    );
  }

  Future<bool> _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);
  }

}

在这里插入图片描述

route.willHandlePopInternally

如何让这个返回 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<HomePage> {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       body: Center(
         child: Column(
           mainAxisSize: MainAxisSize.min,
           children: <Widget>[
             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<SecondPage> {

   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: <Widget>[
             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();
               },
             ),
           ],
         ),
       ),
     );
   }
 }

在这里插入图片描述

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Flutter中,你可以使用`WillPopScope`来监听页面返回操作。`WillPopScope`是一个Widget,它可以监听用户按下返回按钮或者类似的导航手势,然后执行相应的操作。 下面是一个示例代码,展示如何使用`WillPopScope`来监听页面返回: ```dart class YourPage extends StatelessWidget { @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { // 在这里执行你希望执行的操作 // 返回true表示允许返回返回false表示阻止返回 // 例如,你可以弹出一个对话框询问用户是否确认返回 bool confirmExit = await showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: Text('确认退出?'), content: Text('确定要离开此页面吗?'), actions: [ FlatButton( child: Text('取消'), onPressed: () { Navigator.of(context).pop(false); // 阻止返回 }, ), FlatButton( child: Text('确定'), onPressed: () { Navigator.of(context).pop(true); // 允许返回 }, ), ], ), ); return confirmExit ?? false; // 如果对话框关闭时没有选择,则默认不允许返回 }, child: Scaffold( appBar: AppBar( title: Text('Your Page'), ), body: Center( child: Text('Your Page Content'), ), ), ); } } ``` 在上述示例中,我们将整个页面包裹在`WillPopScope`中,并指定了`onWillPop`回调函数。在回调函数中,你可以执行任何你希望在页面返回时进行的操作。在这个示例中,我们弹出一个对话框来询问用户是否确认返回。用户的选择将决定是否允许返回。 注意:`WillPopScope`只能监听物理返回按钮或者类似的导航手势,不能监听页面中的其他返回操作,例如点击一个返回按钮。如果你想监听页面中其他返回操作,你需要手动实现相应的逻辑。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值