flutter 常见组件的特殊用法 —— AppBar

AppBar 的高度与 PreferredSizeWidget

通常可以观察到 Scaffold.appBar 与 AppBar.bottom 属性,要求其值必须是 PreferredSizeWidget(典型的是 AppBar 与 TabBar 组件)。

abstract class PreferredSizeWidget implements Widget {
  Size get preferredSize;
}

① 那么 AppBar 是怎么实现 PreferredSizeWidget 的呢?

  AppBar({
    Key? key,
    this.leading,
    this.automaticallyImplyLeading = true,
    this.title,
    //...
  }) : assert(automaticallyImplyLeading != null),
       //...
       // 这里即是实现
       preferredSize = _PreferredAppBarSize(toolbarHeight, bottom?.preferredSize.height),
       super(key: key);

class _PreferredAppBarSize extends Size {
  // kToolbarHeight 值是 56.0
  _PreferredAppBarSize(this.toolbarHeight, this.bottomHeight)
    : super.fromHeight((toolbarHeight ?? kToolbarHeight) + (bottomHeight ?? 0));

  final double? toolbarHeight;
  final double? bottomHeight;
}

可以看到 AppBar 的默认高度由 toolbarHeight(56.0) + bottomHeight 组成。

② 由于 bottom 组件通常是 TabBar,那么 TabBar 的默认高度是多少呢?

  @override
  Size get preferredSize {
    double maxHeight = _kTabHeight;
    for (final Widget item in tabs) {
      if (item is PreferredSizeWidget) {
        final double itemHeight = item.preferredSize.height;
        maxHeight = math.max(itemHeight, maxHeight);
      }
    }
    // indicatorWeight 默认高度是 2
    return Size.fromHeight(maxHeight + indicatorWeight);
  }

可以看到高度是 2 + tabs 的最大高度。

③ tabs 组件基础组件通常是 Tab,那么 Tab 的默认高度是多少呢?

  @override
  Size get preferredSize {
    if (height != null)
      return Size.fromHeight(height!);
    else if ((text != null || child != null) && icon != null)
      // _kTextAndIconTabHeight 是 72
      return const Size.fromHeight(_kTextAndIconTabHeight);
    else
      // _kTabHeight 是 46
      return const Size.fromHeight(_kTabHeight);
  }

即Tab有文字和图标时是 72,只有文字或图标时为 46。

④ 总结:AppBar 的高度是 104(56 + 48),其中 toolbarHeight 高度是 56(kToolbarHeight),bottom 组件 TabBar 组件高度通常是 48(46+2)(kTextTabBarHeight)。

特殊属性说明

primary: true

当 Scaffold.primary 等于 true 时,AppBar 的高度等于 statusBarHeight + toolbarHeight + bottomHeight 之和。

// 如果 Scaffold 的 primary 为 true,则 _appBarMaxHeight 即 AppBar 最大高度还要加上 topPadding
final double topPadding = widget.primary ? mediaQuery.padding.top : 0.0;
_appBarMaxHeight = AppBar.preferredHeightFor(context, widget.appBar!.preferredSize) + topPadding;

// _addIfNonNull 方法用于向 Scaffold body 中增加 appBar
_addIfNonNull(
  children,
  // AppBar
  ConstrainedBox(
    constraints: BoxConstraints(maxHeight: _appBarMaxHeight!),
    child: FlexibleSpaceBar.createSettings(
      currentExtent: _appBarMaxHeight!,
      child: widget.appBar!,
    ),
  ),
  _ScaffoldSlot.appBar,
  removeLeftPadding: false,
  removeTopPadding: false,
  removeRightPadding: false,
  removeBottomPadding: true,
);

当 AppBbar.primary 等于 true 时,会增加一个 statusBarPadding

  // The padding applies to the toolbar and tabbar, not the flexible space.
  if (widget.primary) {
    appBar = SafeArea(
      bottom: false,
      child: appBar,
    );
  }

flexibleSpace 的布局不受此属性影响


AppBar 的层级与背景透明分析

效果描述:AppBar 背景半透明,并且 body 可以透过 AppBar 显示?

关于这个问题需要理解 Scaffold 的基本布局。

1、首先,最底层是 Scaffold.background 背景色,它是一个 Material 组件,其颜色可以被子组件继承(效果等同于 Stack 布局中处于最底层的组件)。
2、其次,中间层是 body 层,body 层距离顶部有一个 padding(关于这个 padding 是如何产生的,大概有两种情况,一是由 类似 ListView 内部增加的 SliverPadding,二是由 body 外包裹的组件增加的)。

参考 https://www.cnblogs.com/lemos/p/16577900.html 中关于 body 初始位置的说明。关于形成此情况的具体原因有时间再补充说明。

3、最后,最上层是 AppBar 层。

用 Stack 模拟:

Stack(
  children: [
    //背景色,高度是整个屏幕高度
    Container(
      width: double.infinity,
      height: mediaQuery.size.height,
      color: Colors.green,
      child: Text("Scaffold.background"),
    ),
    // body,通常矩离顶部有一个AppBar 的高度
    Padding(
      padding: EdgeInsets.only(top: appBarHeight),
      child: Container(
        width: double.infinity,
        // body 自身高度是 screenHeight - AppBar 的高度
        height: mediaQuery.size.height - appBarHeight,
        child: SingleChildScrollView(
          child: Container(
            color: Colors.yellow,
            height: 1100,
            child: Text("Scaffold.body"),
          ),
        ),
      ),
    ),
    // appbar, 导航栏
    Container(
      width: double.infinity,
      height: appBarHeight,
      color: Colors.blue,
      alignment: Alignment.center,
      // 通常 AppBar.primary 等于 true,距离顶部有个 padding
      padding: EdgeInsets.only(top: statusBarHeight),
      child: Text("Scaffold.appBar"),
    )
  ],
)

那么如何让 AppBar 的背景半透明,并且 body 部分可以显示在 AppBar 之下?

第一步,AppBar 的背景色改成透明色,比如

Container(
  width: double.infinity,
  height: appBarHeight,
  // AppBar 改成透明色
  color: Colors.blue.withOpacity(0.3),
  alignment: Alignment.center,
  //AppBar.primary 等于 true,距离顶部有个 padding
  padding: EdgeInsets.only(top: statusBarHeight),
  child: Text("Scaffold.appBar"),
)

第二步,去除 body 的外部 padding,增加内部 sliverPadding,调整body整体高度为 screenHeight

Container(
  // 1、去除外部的 padding
  // padding: EdgeInsets.only(top: appBarHeight),
  child: Container(
    width: double.infinity,
    // 2、body 自身高度调整为 screenHeight 的高度
    height: mediaQuery.size.height,
    child: SingleChildScrollView(
      child: Column(
        children: [
          Padding(
            // 3、增加内部 padding
            padding: EdgeInsets.only(top: appBarHeight),
            child: Container(
              width: double.infinity,
              color: Colors.yellow,
              height: 1100,
              child: Text("Scaffold.body"),
            ),
          ),
        ],
      ),
    ),
  ),
)

用 Stack 实现该效果的完整源码:

class StackPage extends StatefulWidget {
  const StackPage({Key? key}) : super(key: key);

  @override
  State<StackPage> createState() => _StackPageState();
}

class _StackPageState extends State<StackPage> {
  @override
  Widget build(BuildContext context) {
    var mediaQuery = MediaQuery.of(context);
    final statusBarHeight = mediaQuery.padding.top;
    final appBarHeight = statusBarHeight + kToolbarHeight;
    return Scaffold(
      body: Stack(
        children: [
          //背景色,高度是整个屏幕高度
          Container(
            width: double.infinity,
            height: mediaQuery.size.height,
            color: Colors.green,
            child: Text("Scaffold.background"),
          ),
          // body,通常矩离顶部有一个AppBar 的高度
          Container(
            // 1、去除外部的 padding
            // padding: EdgeInsets.only(top: appBarHeight),
            child: Container(
              width: double.infinity,
              // 2、body 自身高度调整为 screenHeight 的高度
              height: mediaQuery.size.height,
              child: SingleChildScrollView(
                child: Column(
                  children: [
                    Padding(
                      // 3、增加内部 padding
                      padding: EdgeInsets.only(top: appBarHeight),
                      child: Container(
                        width: double.infinity,
                        color: Colors.yellow,
                        height: 1100,
                        child: Text("Scaffold.body"),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
          // appbar, 导航栏
          Container(
            width: double.infinity,
            height: appBarHeight,
            // AppBar 改成透明色
            color: Colors.blue.withOpacity(0.3),
            alignment: Alignment.center,
            //AppBar.primary 等于 true,距离顶部有个 padding
            padding: EdgeInsets.only(top: statusBarHeight),
            child: Text("Scaffold.appBar"),
          )
        ],
      ),
    );
  }
}

用 AppBar 实现该效果的完整源码

class AppBarOpacityPage extends StatefulWidget {
  const AppBarOpacityPage({Key? key}) : super(key: key);

  @override
  State<AppBarOpacityPage> createState() => _AppBarOpacityPageState();
}

class _AppBarOpacityPageState extends State<AppBarOpacityPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green,
      // 说明:第二步中去除外部 padding
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        // 说明:第一步设置 AppBar 透明
        backgroundColor: Colors.blue.withOpacity(0.3),
        elevation: 0,
      ),
      body: ListView(
        // 说明:第二步中增加内部 padding,这里 ListView 会自动处理
        children: [
          Container(
            height: 1100,
            color: Colors.yellow,
            child: Text("yellow"),
          ),
        ],
      ),
    );
  }
}

沉浸式状态栏的实现化可参考:https://www.cnblogs.com/lemos/p/16581865.html


AppBar 的 组成

简单看下关键源码

@override
Widget build(BuildContext context) {
  // 获取 context 组件
  final ThemeData theme = Theme.of(context);
  final AppBarTheme appBarTheme = AppBarTheme.of(context);
  final AppBarTheme defaults = theme.useMaterial3 ? _TokenDefaultsM3(context) : _DefaultsM2(context);
  final ScaffoldState? scaffold = Scaffold.maybeOf(context);
  final ModalRoute<dynamic>? parentRoute = ModalRoute.of(context);

  final FlexibleSpaceBarSettings? settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
  final Set<MaterialState> states = <MaterialState>{
    if (settings?.isScrolledUnder ?? _scrolledUnder) MaterialState.scrolledUnder,
  };

  // 一些属性的初始化
  final bool hasDrawer = scaffold?.hasDrawer ?? false;
  final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false;
  final bool canPop = parentRoute?.canPop ?? false;
  final bool useCloseButton = parentRoute is PageRoute<dynamic> && parentRoute.fullscreenDialog;

  // ... 太多省略

  // 工具栏
  final Widget toolbar = NavigationToolbar(
    leading: leading,
    middle: title,
    trailing: actions,
    centerMiddle: widget._getEffectiveCenterTitle(theme),
    middleSpacing: widget.titleSpacing ?? appBarTheme.titleSpacing ?? NavigationToolbar.kMiddleSpacing,
  );

  // 注:此处的 appBar 仅仅是包装了一下工具栏 toolbar
  // If the toolbar is allocated less than toolbarHeight make it
  // appear to scroll upwards within its shrinking container.
  Widget appBar = ClipRect(
    child: CustomSingleChildLayout(
      // 约束子级,使其 appBar 高度等于 toolbarHeight
      delegate: _ToolbarContainerLayout(toolbarHeight),
      child: IconTheme.merge(
        data: overallIconTheme,
        child: DefaultTextStyle(
          style: toolbarTextStyle!,
          child: toolbar,
        ),
      ),
    ),
  );
  if (widget.bottom != null) {
    appBar = Column(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        // Flexible: 让子级拥有父级布局中剩余的空间或更小的空间(子级本身的大小)
        // 不同于 Expanded 组件,如果里面的 appBar 不足 toolbarHeight 的大小,
        // 则会向上缩小至本身的高度。
        //奇怪的是上面已经限制 appBar 的高度等于 toolbarHeight,所以这个组件没起作用?
        Flexible(
          child: ConstrainedBox(
            constraints: BoxConstraints(maxHeight: toolbarHeight),
            // 此处的 appBar 还是一个 toolbar
            child: appBar,
          ),
        ),
        if (widget.bottomOpacity == 1.0)
          widget.bottom!
        else
          Opacity(
            // 透明度在0.25 ~ 1.0 之间取个值,小于 0.25 则算作 0
            // 转换公式:t = ((t - begin) / (end - begin)).clamp(0.0, 1.0)
            opacity: const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.bottomOpacity),
            child: widget.bottom,
          ),
      ],
    );
  }

  // The padding applies to the toolbar and tabbar, not the flexible space.
  if (widget.primary) {
    // 增加顶部的 padding 区域
    appBar = SafeArea(
      bottom: false,
      child: appBar,
    );
  }

  // 顶部居中
  appBar = Align(
    alignment: Alignment.topCenter,
    child: appBar,
  );

  if (widget.flexibleSpace != null) {
    // 当 flexibleSpace 不为空时,执行 Stack 布局
    appBar = Stack(
      // 对于非 positioned 组件,其约束继承自 Stack 的父组件(透传过来)
      fit: StackFit.passthrough,
      children: <Widget>[
        // flexibleSpace 位于最底层
        Semantics(
          sortKey: const OrdinalSortKey(1.0),
          explicitChildNodes: true,
          child: widget.flexibleSpace,
        ),
        // appBar 位于最上层
        Semantics(
          sortKey: const OrdinalSortKey(0.0),
          explicitChildNodes: true,
          // Creates a material widget to prevent the flexibleSpace from
          // obscuring the ink splashes produced by appBar children.
          child: Material(
            type: MaterialType.transparency,
            child: appBar,
          ),
        ),
      ],
    );
  }

  // 系统状态栏样式
  final SystemUiOverlayStyle overlayStyle = backwardsCompatibility
      ? _systemOverlayStyleForBrightness(
    widget.brightness
        ?? appBarTheme.brightness
        ?? ThemeData.estimateBrightnessForColor(backgroundColor),
  )
      : widget.systemOverlayStyle
      ?? appBarTheme.systemOverlayStyle
      ?? defaults.systemOverlayStyle
      ?? _systemOverlayStyleForBrightness(ThemeData.estimateBrightnessForColor(backgroundColor));

  return Semantics(
    container: true,
    child: AnnotatedRegion<SystemUiOverlayStyle>(
      value: overlayStyle,
      child: Material(
        // AppBar 的背景色
        color: backgroundColor,
        elevation: effectiveElevation,
        shadowColor: widget.shadowColor
            ?? appBarTheme.shadowColor
            ?? defaults.shadowColor,
        surfaceTintColor: widget.surfaceTintColor
            ?? appBarTheme.surfaceTintColor
            ?? defaults.surfaceTintColor,
        shape: widget.shape ?? appBarTheme.shape ?? defaults.shape,
        child: Semantics(
          explicitChildNodes: true,
          child: appBar,
        ),
      ),
    ),
  );
}

注:如果 AppBar.flexibleSpace 属性不为空,则会构建 Stack 布局,其中 flexibleSpace 位于底层,AppBar 位与上层。并且 flexibleSpace 如果是 FlexibleSpaceBar 组件,则高度跟 AppBar 的整个高度相等。

2233

posted on 2022-08-13 03:38  Lemo_wd  阅读(933)  评论(0编辑  收藏  举报

导航