zl程序教程

您现在的位置是:首页 >  移动开发

当前栏目

Flutter之BottomSheet

2023-09-14 09:06:42 时间

参考资料:Flutter之路由系列之LocalhistoryRoute
Flutter之SnackBar原理详解详细的介绍了SnackBar的使用极其原理,SnackBar主要功能是提供了一个简单的消息,虽然跟用户有一定的交互。但是其目的主要是提示性消息。且会自动消失。除了SnackBar之外,Flutter又提供了一个BottomSheet,该组件可以在屏幕底部展示了一个可供用户交互功能的页面。

通过本篇博文你可以了解到:
1、showBottomSheet和showModalBottomSheet的区别
2、关闭BottomSheet的方式
3、BottomSheet的基本原理

Flutter提供了两种展示BottomSheet的方法,showBottomSheet和showModalBottomSheet。下面就来逐一分析这两种方法的区别,看下图(图片展示的demo源码在本篇博文最后提供):
在这里插入图片描述
如上图左边的是通过showBottomSheet展示的BottomSheet,右边的是使用showModalBottomSheet展示的BottomSheet。(注意,除了背景颜色不一样,二者布局代码全部一样)。
1、直观上来看二者的不同之处在于:使用showBottomSheet显示的时候,底部导航栏没有被遮挡;而使用了showModalBottomSheet展示的页面会把底部导航栏给遮挡住
2、操作上来看二者的不同之处在于:点击BottomSheet显示区域之外的地方二者表现有所不同。使用showBottomSheet的方式,点击红色区域之外的地方,BottomSheet不会自动关闭;而使用了showModalBottomSheet,点击绿色区域之外的话,BottomSheet会自动关闭
3、两者共同之处在于关闭BottomSheet的方式一样,都是用了Navigator.pop(context)关闭:
4、手指按住红色或者绿色区域向下滑动,滑动一定的距离后可以关闭BottomSheet

showBottomSheet实现原理简析

因为使用该方式展示的BottomSheet,点击其他区域的时候该BottomSheet不会消失,所以连续点击左边图的“showBottomSheet”按钮会有如下效果:连续点击的时候会先关闭之前的BottomSheet,然后重新打开一个新的BottomSheet:
在这里插入图片描述
下面就来具体分析其原理。在阅读下文之前,建议读者读读Flutter之GlobalKey详解,在这里直接说下Globalkey的作用:持有当前StatefulWidget的StatefulElement对象,我们通过此对象可以获取到当前StatefulWidget的State,从而操控State的方法,比如FormState的validate()方法进行非空校验。Flutter页面随着state的改变,确切的说是随着 setState(() { })方法的调用会调用build方法刷新页面,所以我们可以通过GlobalKey拿到最新的state状态。

1、showBottomSheet简析

下面进入showBottomSheet源码分析阶段,具体是Scaffold.of(context).showBottomSheet<T>方法:

 //ScaffoldState的showBottomSheet方法
 PersistentBottomSheetController<T> showBottomSheet<T>( WidgetBuilder builder, {//省略部分参数}) {
    //1、删除目前正在展示的BottomSheet
    _closeCurrentBottomSheet();
    //2、为BottomSheet添加动画控制器
    final AnimationController controller = BottomSheet.createAnimationController(this)..forward();
    //3、调用setState方法,会引起Scaffold的重绘
    setState(() {
     //4、创建新的BottomSheet
      _currentBottomSheet = _buildBottomSheet<T>(
        builder,
      //省略一些其他参数
      );
    });
    return _currentBottomSheet as PersistentBottomSheetController<T>;
  }

showBottomSheet主要做了如下工作:
1、删除当前正在展示的BottomSheet,具体效果见上面gif图
2、调用_buildBottomSheet方法创建PersistentBottomSheetController对象,赋值给_currentBottomSheet
3、调用setState方法,会调用Scafflod的build,重绘Scaffold。从而展示BottomSheet.
PersistentBottomSheetController顾名思义,用来控制当前展示的BottomSheet,比如BottomSheet的关闭等.这个类的功能有点类似于在SnackBar的ScaffoldFeatureController。现在大致看一下PersistentBottomSheetController的结构,它包含了BottomSheet的布局Widget。这个widget是通过_buildBottomSheet方法创建的,最终showBottomSheet方法的builder参数会构建成一个_StandardBottomSheet 的Widget,并交给PersistentBottomSheetController持有。

2、_StandardBottomSheet 的简单说明

上文通过分析showBottomSheet方法,我们知道其方法参数builder最终会通过_buildBottomSheet构建出一个_StandardBottomSheet 对象,下面就具体看看_buildBottomSheet怎么创建_StandardBottomSheet,进而再将_StandardBottomSheet交给一个PersistentBottomSheetController对象的:


  PersistentBottomSheetController<T> _buildBottomSheet<T>(
    WidgetBuilder builder,bool isPersistent, //默认是false
    { }) {
    //创建一个completer,主要交给PersistentBottomSheetController使用
    final Completer<T> completer = Completer<T>();
    final GlobalKey<_StandardBottomSheetState> bottomSheetKey = GlobalKey<_StandardBottomSheetState>();
    _StandardBottomSheet bottomSheet;
    bool removedEntry = false;
    //用来处理关闭BottomSheet的方法
    void _removeCurrentBottomSheet() {
       //省略关闭BottomSheet的操作,下面会详细说明
    }

    //本地历史实体,主要用来进行路由控制
    final LocalHistoryEntry entry = isPersistent
      ? null
      : LocalHistoryEntry(onRemove: () {
          if (!removedEntry) {
            _removeCurrentBottomSheet();
          }
        });
    //最终形成一个_StandardBottomSheet
    bottomSheet = _StandardBottomSheet(
      key: bottomSheetKey,
      //省略部方法
     
      builder: builder,,
    );
    
    //将entry添加到路由里
    if (!isPersistent)
      ModalRoute.of(context).addLocalHistoryEntry(entry);
    
     return PersistentBottomSheetController<T>._(
      bottomSheet,
      completer,
      entry != null
        ? entry.remove
        : _removeCurrentBottomSheet,
      (VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); },
      !isPersistent,
    );
  }

因为_buildBottomSheet方法里涉及到了LocalHistoryRoute的相关知识,其主要作用就是讲LocalHistoryEntry 添加到LocalHistoryRoute中,这样当 Navigator.of(context).pop()来返回上一步的时候,就会将之前添加的LocalHistoryEntry 弹出来,并执行LocalHistoryEntry 的onRemove方法,从而关闭了当前展示的BottomSheet。关于LocalHistoryRoute可阅读此博客了解更多。在上面_buildBottomSheet代码中初始化LocalHistoryEntry 操作如下:

  //初始化LocalHistoryEntry 
   final LocalHistoryEntry entry = isPersistent
      ? null
      : LocalHistoryEntry(onRemove: () {
          if (!removedEntry) {
            _removeCurrentBottomSheet();
          }
        });
      
   //将LocalHistoryEntry添加到LocalHistoryRoute中     
   if (!isPersistent)
      ModalRoute.of(context).addLocalHistoryEntry(entry);

Navigator.of(context).pop()的时候就会执行LocalHistoryEntry 的onRemove方法,进而执行_removeCurrentBottomSheet方法:

   void _removeCurrentBottomSheet() {
      removedEntry = true;
      //如果当前BottomSheet已经删除了,就不在删除
      if (_currentBottomSheet == null) {
        return;
      }
 
      _showFloatingActionButton();

      void _closed(void value) {
        //执行关闭方法,也就是将_currentBottomSheet设置为null,掉用setState刷新页面
        setState(() {
          _currentBottomSheet = null;
        });
        
        if (animationController.status != AnimationStatus.dismissed) {
          _dismissedBottomSheets.add(bottomSheet);
        }
        completer.complete();
      }

      final Future<void> closing = bottomSheetKey.currentState.close();
      if (closing != null) {
        closing.then(_closed);
      } else {
        _closed(null);
      }
    }

_removeCurrentBottomSheet方法会执行其内部的_closed方法,_closed方法先想_currentBottomSheet 设置为null,然后调用setState方法,是的Scaffold回调其build方法进行页面的重绘,因为_currentBottomSheet 设置了null,所以页面重绘的时候不会展示BottomSheet,进而关闭了BottomSheet. 下面就来看看Scaffold build方法

3、BottomSheet在Scaffold build方法的构建过程

因为showBottomSheet会调用setState方法,从而回调了Scaffold的build方法,所以在此在看看其build方法:

  @override
  Widget build(BuildContext context) {
      final List<LayoutId> children = <LayoutId>[];
    //如果_currentBottomSheet不等于null
    if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) {
      //将_currentBottomSheet和_dismissedBottomSheets集合里的BottomSheets放在Stack
      final Widget stack = Stack(
        //底部居中展示
        alignment: Alignment.bottomCenter,
        children: <Widget>[
          ..._dismissedBottomSheets,
          if (_currentBottomSheet != null) _currentBottomSheet._widget,
        ],
      );
      //将Stack使用LayoutI包裹然后放入到children 集合
      _addIfNonNull(
        children,
        stack,  
      );
      
    return _ScaffoldScope(
    //省略部分代码
      child: PrimaryScrollController(
          child: AnimatedBuilder(//省略部分参数) {
            return CustomMultiChildLayout(
              //使用children数据构建页面
              children: children,
               //省略部分代码,
            );
          }),
        ),
      ),
    );

    }
  }

build方法的逻辑其实跟处理SnackBar的展示逻辑上总体差不多
1、将当前的BottomSheet放在Stack中,另外有一个_dismissedBottomSheets集合,如果该集合不为空的话,也要将集合中的BottomSheet添加到Stack中(常规情况下应该为空,后面会有说明)
2、将Stack调用_addIfNonNull方法包裹在LayoutId中,然后将之添加到children集合
3、最终build方法会使用children集合构成我们最终的UI

最后附上本篇博文的demo代码如下:

class BottomSheetDemo extends StatelessWidget {
  Widget build(BuildContext context) {
    return Center(
      child: RaisedButton(
        child: const Text('showModalBottomSheet'),
        onPressed: () {
//             showModalBottomSheet<void>(///使用showModalBottomSheet的方式
          showModalBottomSheet<void>(///使用showBottomSheet的方式
            context: context,
            builder: (BuildContext context) {
              return _createBottomContent(context);
            },
          );
        },
      ),
    );
  }

  Widget _createBottomContent(BuildContext context) {
    return Container(
      height: 300,
      color: Colors.green,
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            const Text('绿色区域一个Bottom Sheet'),
            RaisedButton(
              child: const Text('关闭Bottom Sheet'),
              onPressed: () => Navigator.pop(context),
            )
          ],
        ),
      ),
    );
  }
}


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home:Scaffold(
        body:  BottomSheetDemo (),
        bottomNavigationBar:BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
                  BottomNavigationBarItem(
                    icon: Icon(Icons.home),
                    title: Text('Home'),
                  ),
                  BottomNavigationBarItem(
                    icon: Icon(Icons.business),
                    title: Text('Business'),
                  ),
                  BottomNavigationBarItem(
                    icon: Icon(Icons.school),
                    title: Text('School'),
                  ),
                ],
          currentIndex: 0,
        ) ,
      ),
    );
  }
}