flutter 常见组件的特殊用法 —— Scaffold
scaffold 中 body 的 初始位置
通常 body 内容的初始滚动位置位于状态栏或导航栏之下。但某些情况下需要调整初始位置:
具体分以下几个情况讨论:
1、对于非 ListView 组件
① 在AppBar 不存在时,亦即状态栏未被 AppBar 占用:
body 会占满状态栏,跟 ② 中设置了了 extendBodyBehindAppbar
为 true 的效果一样,(如果是 ListView,则会增加一个 padding)。
② 在有 AppBar 的情况下,亦即状态栏已被 AppBar 占用时:
默认 body 处于导航栏之下,不占用导航栏与状态栏。如果设置 extendBodyBehindAppbar
为 true,则 body 不仅会占用导航栏,还会占用状态栏(对比 ListView,后者会增加两个 padding)。
2、对于 ListView 组件
① 在AppBar 不存在时,亦即状态栏未被 AppBar 占用:
此时默认 ListView 的 padding 不为 0(我这里是 47, 因为 appBar 不存在了,而状态栏高度是 47),所以可以知道此时状态栏未被 body 占用。
② 在有 AppBar 的情况下,亦即状态栏已被 AppBar 占用时:
默认,它的 padding 是 0。如果设置 extendBodyBehindAppbar
为 true,它的 SliverPadding 不为0(我这里是 103 = 47 + 56),为了让 body 延伸到导航栏之下,还需要手动去除 ListView 的 padding。
注:
当 ListView 的 padding 不为 0 时,有以下两种方法可以将其剔除,一是直接设置 ListView 的 padding 属性为 EdgeInsets.zero。二是使用下面这种方式包裹组件:
MediaQuery.removeViewPadding(
context: context,
removeTop: true,
child: ListView(
// padding: EdgeInsets.zero,
children: [
],
),
)
scaffold 的组成
简单看下关键源码
class Scaffold {
@override
Widget build(BuildContext context) {
//获取 context 组件
final MediaQueryData mediaQuery = MediaQuery.of(context);
final ThemeData themeData = Theme.of(context);
final TextDirection textDirection = Directionality.of(context);
// children 中是 Scaffold中待堆叠的子对象。观察各个子组件存放:
final List<LayoutId> children = <LayoutId>[];
_addIfNonNull(
children,
widget.body == null ? null : _BodyBuilder(
extendBody: widget.extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
body: widget.body!,
),
_ScaffoldSlot.body,
removeLeftPadding: false,
removeTopPadding: widget.appBar != null,
removeRightPadding: false,
removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null,
removeBottomInset: _resizeToAvoidBottomInset,
);
// 1.模态框
if (_showBodyScrim) {
_addIfNonNull(
children,
ModalBarrier(
dismissible: false,
color: _bodyScrimColor,
),
_ScaffoldSlot.bodyScrim,
removeLeftPadding: true,
removeTopPadding: true,
removeRightPadding: true,
removeBottomPadding: true,
);
}
if (widget.appBar != null) {
// 如果 Scaffold 的 primary 为 true,则 _appBarMaxHeight 即 AppBar 最大高度还要加上 topPadding
final double topPadding = widget.primary ? mediaQuery.padding.top : 0.0;
_appBarMaxHeight = AppBar.preferredHeightFor(context, widget.appBar!.preferredSize) + topPadding;
assert(_appBarMaxHeight! >= 0.0 && _appBarMaxHeight!.isFinite);
// 2.appBar 导航栏
_addIfNonNull(
children,
ConstrainedBox(
constraints: BoxConstraints(maxHeight: _appBarMaxHeight!),
child: FlexibleSpaceBar.createSettings(
currentExtent: _appBarMaxHeight!,
child: widget.appBar!,
),
),
_ScaffoldSlot.appBar,
removeLeftPadding: false,
removeTopPadding: false,
removeRightPadding: false,
removeBottomPadding: true,
);
}
bool extendBodyBehindMaterialBanner = false;
// MaterialBanner set by ScaffoldMessenger
if (_messengerMaterialBanner != null) {
final MaterialBannerThemeData bannerTheme = MaterialBannerTheme.of(context);
final double elevation = _messengerMaterialBanner?._widget.elevation ?? bannerTheme.elevation ?? 0.0;
extendBodyBehindMaterialBanner = elevation != 0.0;
//3.materialBanner
_addIfNonNull(
children,
_messengerMaterialBanner?._widget,
_ScaffoldSlot.materialBanner,
removeLeftPadding: false,
removeTopPadding: widget.appBar != null,
removeRightPadding: false,
removeBottomPadding: true,
maintainBottomViewPadding: !_resizeToAvoidBottomInset,
);
}
if (widget.persistentFooterButtons != null) {
//4. 不知道啥
_addIfNonNull(
children,
Container(
decoration: BoxDecoration(
border: Border(
top: Divider.createBorderSide(context, width: 1.0),
),
),
child: SafeArea(
top: false,
child: IntrinsicHeight(
child: Container(
alignment: AlignmentDirectional.centerEnd,
padding: const EdgeInsets.all(8),
child: OverflowBar(
spacing: 8,
overflowAlignment: OverflowBarAlignment.end,
children: widget.persistentFooterButtons!,
),
),
),
),
),
_ScaffoldSlot.persistentFooter,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: widget.bottomNavigationBar != null,
maintainBottomViewPadding: !_resizeToAvoidBottomInset,
);
}
if (widget.bottomNavigationBar != null) {
//5.底部导航栏
_addIfNonNull(
children,
widget.bottomNavigationBar,
_ScaffoldSlot.bottomNavigationBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: false,
maintainBottomViewPadding: !_resizeToAvoidBottomInset,
);
}
_addIfNonNull(
//6.悬浮按钮
children,
_FloatingActionButtonTransition(
fabMoveAnimation: _floatingActionButtonMoveController,
fabMotionAnimator: _floatingActionButtonAnimator,
geometryNotifier: _geometryNotifier,
currentController: _floatingActionButtonVisibilityController,
child: widget.floatingActionButton,
),
_ScaffoldSlot.floatingActionButton,
removeLeftPadding: true,
removeTopPadding: true,
removeRightPadding: true,
removeBottomPadding: true,
);
switch (themeData.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
//7.状态栏的监听手势?
_addIfNonNull(
children,
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleStatusBarTap,
// iOS accessibility automatically adds scroll-to-top to the clock in the status bar
excludeFromSemantics: true,
),
_ScaffoldSlot.statusBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: true,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
break;
}
//8.drawer
if (_endDrawerOpened.value) {
_buildDrawer(children, textDirection);
_buildEndDrawer(children, textDirection);
} else {
_buildEndDrawer(children, textDirection);
_buildDrawer(children, textDirection);
}
// The minimum insets for contents of the Scaffold to keep visible.
final EdgeInsets minInsets = mediaQuery.padding.copyWith(
bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0,
);
// The minimum viewPadding for interactive elements positioned by the
// Scaffold to keep within safe interactive areas.
final EdgeInsets minViewPadding = mediaQuery.viewPadding.copyWith(
bottom: _resizeToAvoidBottomInset && mediaQuery.viewInsets.bottom != 0.0 ? 0.0 : null,
);
// extendBody locked when keyboard is open
// 如果 bottomNavigationBar 或 persistentFooterButtons 存在,则会影响 body 是否能延伸到其下方,默认为 false。
final bool extendBody = minInsets.bottom <= 0 && widget.extendBody;
return _ScaffoldScope(
hasDrawer: hasDrawer,
geometryNotifier: _geometryNotifier,
child: ScrollNotificationObserver(
child: Material(
// background 是 scaffold 所有子组件的底色
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) {
// Scaffold 的 children
return CustomMultiChildLayout(
// 布局类 _ScaffoldLayout,用于构建 Scaffold 中 children 子组件的布局
// 注意,这里不是按 children 中子组件的先后顺序进行布局,而是根据 _ScaffoldSlot 名称进行灵活配置。
delegate: _ScaffoldLayout(
extendBody: extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
minInsets: minInsets,
minViewPadding: minViewPadding,
currentFloatingActionButtonLocation: _floatingActionButtonLocation!,
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
geometryNotifier: _geometryNotifier,
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation!,
textDirection: textDirection,
isSnackBarFloating: isSnackBarFloating,
extendBodyBehindMaterialBanner: extendBodyBehindMaterialBanner,
snackBarWidth: snackBarWidth,
),
children: children,
);
}),
),
),
);
}
}
关于 _ScaffoldLayout 中子组件具体的布局,有时间再分析