Flutter:可以检测抽屉何时打开?

Posted

技术标签:

【中文标题】Flutter:可以检测抽屉何时打开?【英文标题】:Flutter: possible to detect when a drawer is open? 【发布时间】:2018-10-02 13:29:34 【问题描述】:

是否可以检测抽屉何时打开,以便我们可以运行一些例程来更新其内容?

我的一个典型用例是显示关注者、点赞者的数量......为此,我需要轮询服务器以获取此信息,然后显示它。

我尝试实现一个 NavigatorObserver 来捕捉 Drawer 可见/隐藏的时刻,但 NavigatorObserver 没有检测到有关 Drawer 的任何信息。

这是链接到 NavigatorObserver 的代码:

import 'package:flutter/material.dart';

typedef void OnObservation(Route<dynamic> route, Route<dynamic> previousRoute);
typedef void OnStartGesture();

class NavigationObserver extends NavigatorObserver 
  OnObservation onPushed;
  OnObservation onPopped;
  OnObservation onRemoved;
  OnObservation onReplaced;
  OnStartGesture onStartGesture;

  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) 
    if (onPushed != null) 
      onPushed(route, previousRoute);
    
  

  @override
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) 
    if (onPopped != null) 
      onPopped(route, previousRoute);
    
  

  @override
  void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) 
    if (onRemoved != null)
      onRemoved(route, previousRoute);
  

  @override
  void didReplace( Route<dynamic> oldRoute, Route<dynamic> newRoute ) 
    if (onReplaced != null)
      onReplaced(newRoute, oldRoute);
  

  @override
  void didStartUserGesture()  
    if (onStartGesture != null)
      onStartGesture();
    
  

以及这个观察者的初始化

void main()
  runApp(new MyApp());


class MyApp extends StatefulWidget 
  @override
  _MyAppState createState() => new _MyAppState();


class _MyAppState extends State<MyApp> 
  final NavigationObserver _observer = new NavigationObserver()
                                              ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) 
                                                print('** pushed route: $route');
                                              
                                              ..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) 
                                                print('** poped route: $route');
                                              
                                              ..onReplaced = (Route<dynamic> route, Route<dynamic> previousRoute) 
                                                print('** replaced route: $route');
                                              
                                              ..onStartGesture = () 
                                                print('** on start gesture');
                                              ;

  @override
  void initState()
    super.initState();
  

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) 
    return new MaterialApp(
      title: 'Title',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SplashScreen(),
        routes: <String, WidgetBuilder> 
          '/splashscreen': (BuildContext context) => new SplashScreen(),
        ,
        navigatorObservers: <NavigationObserver>[_observer],
    );
  

感谢您的帮助。

【问题讨论】:

NavigatorObserver 不是解决方案。你能改为展示你是如何使用 Drawer 的吗? 嗨@RémiRousselet。我使用Drawer的方式没有什么特别的,没什么好说的。我只是在 Scaffold 级别初始化抽屉。我正在等待解决方案来检测抽屉何时打开以进一步详细说明。但是正如您提到的 NavigatorObserver 不是解决方案,您还有其他想法吗? 【参考方案1】:

我认为一种简单的解决方案是覆盖 AppBarleading 属性,这样您就可以在按下菜单图标时访问并基于此运行您的 API 调用。

但是我可能误解了您的问题,因为对于您提供的用例,您通常需要以一种可以听取任何会自动更新值的更改的方式对其进行管理,所以我不知道当抽屉打开时你想触发什么。

反正这里就是例子。

class DrawerExample extends StatefulWidget 
  @override
  _DrawerExampleState createState() => new _DrawerExampleState();


class _DrawerExampleState extends State<DrawerExample> 
  GlobalKey<ScaffoldState> _key = new GlobalKey<ScaffoldState>();
  int _counter =0;
  _handleDrawer()
      _key.currentState.openDrawer();

           setState(() 
          ///DO MY API CALLS
          _counter++;
        );

  
  @override
  Widget build(BuildContext context) 
    return new Scaffold(
      key: _key,
      appBar: new AppBar(
        title: new Text("Drawer Example"),
        centerTitle: true,
        leading: new IconButton(icon: new Icon(
          Icons.menu
        ),onPressed:_handleDrawer,),
      ),
      drawer: new Drawer(
        child: new Center(
          child: new Text(_counter.toString(),style: Theme.of(context).textTheme.display1,),
        ),
      ),
    );
  

【讨论】:

如此简单优雅...非常感谢。 @aziza 这不适合滑开的抽屉吗? 不会通过用户滑动操作检测抽屉打开。 如何用滑动手势检测抽屉?【参考方案2】:

抽屉打开/关闭时的检测和运行函数

通过任何操作打开抽屉时运行initState()。 通过任何操作关闭抽屉时运行dispose()
class MyDrawer extends StatefulWidget 
    @override
    _MyDrawerState createState() => _MyDrawerState();


class _MyDrawerState extends State<MyDrawer> 

    @override
    void initState() 
        super.initState();
        print("open");
    

    @override
    void dispose() 
        print("close");
        super.dispose();
    

    @override
    Widget build(BuildContext context) 
        return Drawer(
            child: Column(
                children: <Widget>[
                    Text("test1"),
                    Text("test2"),
                    Text("test3"),
                ],
            ),
        );
    

状态管理注意事项

如果您使用这些函数更改状态以重建抽屉项目,您可能会遇到错误:Unhandled Exception: setState() or markNeedsBuild() called during build

这可以通过在initState()中使用以下两个函数来处理source

选项 1

WidgetsBinding.instance.addPostFrameCallback((_)
  // Add Your Code here.
);

选项 2

SchedulerBinding.instance.addPostFrameCallback((_) 
  // add your code here.
);

选项 1 的完整示例

@override
void initState() 
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) 
        // Your Code Here
    );

【讨论】:

为什么不被接受?这说明了按钮按下和滑动抽屉打开 这个解决方案非常适合我。它应该已被接受。【参考方案3】:

很遗憾,目前没有readymade solution。

您可以为此使用脏 hack:观察抽屉的可见位置。

比如我就是用这种方式来同步按钮上图标的动画和Drawer框的位置。

解决这个问题的代码你可以在下面看到:

    import 'package:flutter/material.dart';
    import 'package:flutter/scheduler.dart';

    class DrawerListener extends StatefulWidget 
      final Widget child;
      final ValueChanged<FractionalOffset> onPositionChange;

      DrawerListener(
        @required this.child,
        this.onPositionChange,
      );

      @override
      _DrawerListenerState createState() => _DrawerListenerState();
    

    class _DrawerListenerState extends State<DrawerListener> 
      GlobalKey _drawerKey = GlobalKey();
      int taskID;
      Offset currentOffset;

      @override
      void initState() 
        super.initState();
        _postTask();
      

      _postTask() 
        taskID = SchedulerBinding.instance.scheduleFrameCallback((_) 
          if (widget.onPositionChange != null) 
            final RenderBox box = _drawerKey.currentContext?.findRenderObject();
            if (box != null) 
              Offset newOffset = box.globalToLocal(Offset.zero);
              if (newOffset != currentOffset) 
                currentOffset = newOffset;
                widget.onPositionChange(
                  FractionalOffset.fromOffsetAndRect(
                    currentOffset,
                    Rect.fromLTRB(0, 0, box.size.width, box.size.height),
                  ),
                );
              
            
          

          _postTask();
        );
      

      @override
      void dispose() 
        SchedulerBinding.instance.cancelFrameCallbackWithId(taskID);
        if (widget.onPositionChange != null) 
          widget.onPositionChange(FractionalOffset(1.0, 0));
        
        super.dispose();
      

      @override
      Widget build(BuildContext context) 
        return Container(
          key: _drawerKey,
          child: widget.child,
        );
      
    

如果您只对打开或关闭盒子的最终事件感兴趣,调用initStatedispose 函数中的回调就足够了。

【讨论】:

正是我想要的,谢谢分享!如果您需要将按钮的动画与抽屉的移动同步,这非常有用! Flutter 2 有现成的解决方案:***.com/a/66600590/11120544 这个答案来自这个问题本身【参考方案4】:

最佳解决方案

ScaffoldState 有一个有用的方法isDrawerOpen 提供打开/关闭状态。

示例:这里在后按时,它首先检查抽屉是否打开,如果是则首先它会在退出前关闭。

/// create a key for the scaffold in order to access it later.
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

@override
Widget build(context) 
   return WillPopScope(
  child: Scaffold(
    // assign key (important)
    key: _scaffoldKey,
    drawer: SideNavigation(),
  onWillPop: () async 
    // drawer is open then first close it
    if (_scaffoldKey.currentState.isDrawerOpen) 
      Navigator.of(context).pop();
      return false;
    
    // we can now close the app.
    return true;
  );

【讨论】:

【参考方案5】:

ScaffoldState 中有 isDrawerOpen 属性,因此您可以随时检查。

创建一个全局键;

GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

分配给脚手架

Scaffold(
      key: scaffoldKey,
      appBar: ..)

检查应用中的任何位置

bool opened =scaffoldKey.currentState.isDrawerOpen;

【讨论】:

【参考方案6】:

由于https://github.com/flutter/flutter/pull/67249 已经与 Flutter 2.0 合并并发布,这是检测抽屉打开/关闭的正确方法:

Scaffold(
      onDrawerChanged: (isOpened) 
        //todo what you need for left drawer
      ,
      onEndDrawerChanged: (isOpened) 
        //todo what you need for right drawer
      ,
)

【讨论】:

既然这个功能是内置的,这篇文章应该更新为正确答案。谢谢【参考方案7】:

到发布此问题时,完成此操作有点技巧。但是从 Flutter 2.0 开始,这很容易。在 Scaffold 内部,您可以同时检测到右抽屉和左抽屉,如下所示。

@override
  Widget build(BuildContext context) 
    return Scaffold(
      onDrawerChanged: (isOpened) 
        *//Left drawer, Your code here,*
      ,
      onEndDrawerChanged: (isOpened) 
        *//Right drawer, Your code here,*
      ,
    );
  

【讨论】:

【参考方案8】:

您可以简单地使用 onDrawerChanged 来检测抽屉是在 Scaffold 小部件中打开还是关闭。

属性:

void 函数(bool)? onDrawerChanged 类型:void Function(bool)? Scaffold.drawer 打开或关闭时调用的可选回调。

示例:

@覆盖 小部件构建(BuildContext 上下文)

return Scaffold(
  onDrawerChanged:(val)
    if(val)
      setState(() 
        //foo bar;
      );
    else
      setState(() 
        //foo bar;
      );
    
,     
  drawer: Drawer(        
      child: Container(
      )
  ));

【讨论】:

以上是关于Flutter:可以检测抽屉何时打开?的主要内容,如果未能解决你的问题,请参考以下文章

如何在颤振抽屉中返回主页/主页

VSCode Flutter 未检测到像素 3

Flutter,如何制作按钮以在 Flutter 中打开抽屉 [重复]

如何在 Flutter 中收听抽屉打开/关闭动画

Flutter:如何在抽屉内推送/弹出导航器页面?

Flutte VS RN