Flutter SliverAppBar 完全指南 - 第一部分

创建 Flutter 中自定义 SliverAppBar 行为的指南

目录

  • Flutter SliverAppBar 完全指南 - 第一部分

    引言 ℹ️

    扩展应用栏(可折叠工具栏)是 Material Design 应用栏行为中广泛使用的一种,当用户向上滚动时可以隐藏应用栏内容。一个简单的用例是 AppBar,当用户向下滚动时可能会显示完整的个人资料图片,当用户向上滚动时会逐渐过渡到只显示用户名。

    Source: Material Design Guide

    在 Flutter 中,我们可以使用 SliverAppBar 小部件来实现相同的行为。正如小部件名称所示,它只能在 CustomScrollView 小部件中使用。此小部件通过直接向其提供 slivers(SliverAppBar、SliverList、SliverGrid 等)来帮助您创建各种滚动效果,例如列表、网格和扩展标题。

    但是等等…… sliver 到底是什么? ?

    Sliver 只是一个可滚动区域的一部分。仅此而已!您使用的所有可滚动视图,如 ListView 和 GridView,实际上都是使用 Slivers 实现的。您可以将 Slivers 视为一个较低级别的接口,它在实现可滚动区域方面提供更精细级别的控制。由于 slivers 可以仅在项滚动到视图时才惰性地构建每个项,因此 slivers 特别适用于高效地滚动大量子项。- Slivers, Demystified

    我们要构建什么? ?

    我们将构建以下自定义行为

    Example

    来发现 SliverAppBar 小部件! ? ?

    SliverAppBar(
      pinned: _pinned,
      snap: _snap,
      floating: _floating,
      expandedHeight: 160.0,
      collapsedHeight: 80.0,
      flexibleSpace: const FlexibleSpaceBar(
        title: Text('SliverAppBar'),
        background: FlutterLogo(),
      ),
    ),

    如前所述,此小部件直接在 CustomScrollView 中使用,并包含一些有用的属性来自定义其行为。

    最重要的属性是

    我们将重点关注这些属性

    • collapsedHeight:一个双精度值,定义了应用栏折叠时的高度。
    • expandedHeight:一个双精度值,定义了应用栏展开时的高度。 展开/折叠示例
    • flexibleSpace:此小部件堆叠在工具栏和标签栏的后面,通常与 FlexibleSpaceBar 小部件一起使用。FlexibleSpaceBar 小部件包含属性,用于在应用栏展开时自定义空间。

    flexibleSpace field Example

    我们将重点关注两个属性

    • title 是一个在折叠/展开应用栏时会(缩放)的小部件。

    • background 是一个在应用栏折叠时会淡出的(fade out)小部件。

    • 还有其他属性控制折叠应用栏的行为,如 pinnedfloatingsnap 等。要了解更多信息,请查看 官方文档

    开始构建吧! ?

    首先,我们将使用 flutter_hooks 包来更轻松地管理局部状态和处理 ScrollControllers。当然,您可以使用 StatefulWidgetsetState 或任何其他局部状态管理解决方案。

    让我们开始添加 Stack 小部件以添加电影背景图片和 CustomScrollView 小部件,以便能够使用 SliverAppBar。

    class MovieProfilePage extends HookWidget {
      const MovieProfilePage({super.key, required this.movieDetails});
      final MovieDetails movieDetails;
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: [
            /// The background image
            BlurredBackdropImage(movieDetails: movieDetails),
            CustomScrollView(
              slivers: [
                
              ],
            ),
          ],
        );
      }
    }

    现在让我们将 SliverAppBar 添加到 CustomScrollView 中。

    @override
      Widget build(BuildContext context) {
        const collapsedBarHeight = 60.0;
        const expandedBarHeight = 400.0;
    
        return Stack(
          children: [
            /// Blurred Background Image Widget
            BlurredBackdropImage(movieDetails: movieDetails),
            CustomScrollView(
              slivers: [
                SliverAppBar(
                  expandedHeight: expandedBarHeight,
                  collapsedHeight: collapsedBarHeight,
                  centerTitle: false,
                  pinned: true,
                  /// 1
                  title: CollapsedAppBarContent(movieDetails: movieDetails),
                  elevation: 0,
                  /// 2
                  backgroundColor: Colors.transparent,
                  leading: const BackButton(
                    color: Colors.white,
                  ),
                  /// 3
                  flexibleSpace: FlexibleSpaceBar(
                    background: ExpandedAppBarContent(
                      movieDetails: movieDetails,
                    ),
                  ),
                ),
    
                /// 4
                SliverToBoxAdapter(
                  child: ConstrainedBox(
                    constraints: BoxConstraints(
                      minHeight: MediaQuery.of(context).size.height,
                    ),
                    child: Material(
                      elevation: 7,
                      borderRadius: const BorderRadius.only(
                        topLeft: Radius.circular(15),
                        topRight: Radius.circular(15),
                      ),
                      child: PageBodyWidget(movieDetails: movieDetails),
                    ),
                  ),
                )
              ],
            ),
          ],
        );
      }
    }
    1. 添加 CollapsedAppBarContent 小部件,该小部件包含顶部 Row 到 title 字段。 title 小部件将始终保持可见。
    2. 将 AppBar 的 background 颜色更改为 transparent 以显示 BlurredBackdropImage 小部件。
    3. 添加 FlexibleSpaceBar 小部件并将 ExpandedAppBarContent 小部件分配给 background 字段。当应用栏折叠时,background 字段内容会淡出,这正是我们需要的!
    4. 添加 SliverToBoxAdapter 小部件以添加主体内容。简单来说,SliverToBoxAdapter 所做的是连接 slivers 和常规小部件(即基于 box 的小部件)。由于 CustomScrollView 的直接子项只能是 slivers 对象。

    现在应用栏将如下所示: 示例-1示例-2

    现在,如果我们分析所需的设计,我们会发现 CollapsedAppBarContent 小部件与 ExpandedAppBarContent 小部件重叠。此外,当应用栏折叠时,我们需要将背景颜色更改为黑色而不是透明。

    但是如何实现这种行为呢? ?

    首先,我们需要知道应用栏是折叠还是展开;基于此,我们可以显示应用栏的正确内容。

    为此,我们可以使用 NotificationListener 小部件并监听 ScrollNotifications 来检测滚动偏移量的变化。基于此,我们可以知道应用栏是折叠还是展开。

    @override
      Widget build(BuildContext context) {
        ...
        /// 1
        final scrollController = useScrollController();
        final isCollapsed = useState(false);
    
        return NotificationListener<ScrollNotification>(
          onNotification: (notification) {
            /// 2
            isCollapsed.value = scrollController.hasClients &&
                scrollController.offset > (expandedBarHeight - collapsedBarHeight);
            return false;
          },
          child: Stack(
            children: [
              BlurredBackdropImage(movieDetails: movieDetails),
              CustomScrollView(
                controller: scrollController,
                slivers: [
                  SliverAppBar(
                    expandedHeight: expandedBarHeight,
                    collapsedHeight: collapsedBarHeight,
                    centerTitle: false,
                    pinned: true,
                    /// 3
                    title: AnimatedOpacity(
                      duration: const Duration(milliseconds: 200),
                      opacity: isCollapsed.value ? 1 : 0,
                      child: CollapsedAppBarContent(movieDetails: movieDetails),
                    ),
                    elevation: 0,
                    /// 4
                    backgroundColor:
                        isCollapsed.value ? Colors.black : Colors.transparent,
                    leading: const BackButton(
                      color: Colors.white,
                    ),
                    flexibleSpace: FlexibleSpaceBar(
                      background: ExpandedAppBarContent(
                        movieDetails: movieDetails,
                      ),
                    ),
                  ),
                  ...
                ],
              ),
            ],
          ),
        );
      }
    1. 使用两个 hooks,useScrollController() hook 来创建 ScrollController,以及一个初始值为 falseuseState() hook 来创建 isCollapsed 状态对象。isCollapsed 标志将用于根据其值更新应用栏内容。
    2. 通过检查 scrollController 对象是否已附加到滚动视图来更新 isCollapsed 状态值。此外,它会检查当前滚动偏移量是否大于 expandedBarHeightcollapsedBarHeight 之间的差值。
    3. CollapsedAppBarContent 小部件包装在 AnimatedOpacity 小部件中,以根据 isCollapsed 值隐藏和显示内容。
    4. 根据 isCollapsed 值更改 backgroundColor

    现在应用栏将如下所示

    example-1example-2

    太棒了,我们成功了! ? ?

    奖励 ?

    我们还可以为应用栏折叠时添加触觉反馈,以进一步改善用户体验。我们可以通过使用 HapticFeedback 类来实现这一点,该类允许访问设备上的触觉反馈接口。

    @override
      Widget build(BuildContext context) {
        ...
        final didAddFeedback = useState(false);
    
        return NotificationListener<ScrollNotification>(
          onNotification: (notification) {
            ...
            /// When the app bar is collapsed and the feedback 
            /// hasn't been added previously will invoke 
            /// the `mediumImpact()` method, otherwise will 
            /// reset the didAddFeedback value.
            ///
            if (isCollapsed.value && !didAddFeedback.value) {
              HapticFeedback.mediumImpact();
              didAddFeedback.value = true;
            } else if (!isCollapsed.value) {
              didAddFeedback.value = false;
            }
            return false;
          },
          child: Stack(
            children: [
                  ...
                ],
              ),
            ],
          ),
        );
      }

    我们添加了另一个名为 didAddFeedback 的 hook 状态变量,基于此它将调用 HapticFeedback.mediumImpact() 方法。

    总结 ?

    slivers 帮助您自定义滚动行为并构建复杂的滚动效果。掌握 slivers 将使您能够改善用户界面,更重要的是,改善用户体验。

    感谢阅读! ❤️

    GitHub

    查看 Github