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 中子组件具体的布局,有时间再分析


posted on 2022-08-11 21:26  Lemo_wd  阅读(1681)  评论(0编辑  收藏  举报

导航