参考资料: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,
) ,
),
);
}
}