如何将 SliverPersistentHeader 设置为 "overgrow"

How to get the SliverPersistentHeader to "overgrow"

我在我的 CustomScrollView 中使用 SliverPersistentHeader 来获得一个持久的 header,它会在用户滚动时缩小和增大,但当它达到最大尺寸时,感觉有点僵硬,因为它没有 "overgrow".

这是我想要的行为(来自 Spotify 应用程序)和我拥有的行为的视频:

.

编辑:我在 AppBar 中找到了另一种拉伸图像的方法 这是最小的可重现示例:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    debugShowCheckedModeBanner: false,
    home: Home(),
  ));
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        physics: const BouncingScrollPhysics(),
        slivers: [
          SliverAppBar(
            pinned: true,
            expandedHeight: 200,
            title: Text('Title'),
            stretch: true,
            flexibleSpace: FlexibleSpaceBar(
              background: Image.network('https://i.imgur.com/2pQ5qum.jpg', fit: BoxFit.cover),
            ),
          ),
          SliverToBoxAdapter(
            child: Column(
              children: List.generate(50, (index) {
                return Container(
                  height: 72,
                  color: Colors.blue[200],
                  alignment: Alignment.centerLeft,
                  margin: EdgeInsets.all(8),
                  child: Text('Item $index'),
                );
              }),
            ),
          ),
        ],
      ),
    );
  }
}

神奇之处在于 - stretch: trueBouncingScrollPhysics() 属性。
没有复杂的听众,阶段性的小部件等等。只是 FlexibleSpaceBarbackground 上的图像。

在寻找此问题的解决方案时,我遇到了三种不同的解决方法:

  1. 创建一个 Stack,其中包含 CustomScrollView 和一个 header 小部件(覆盖在滚动视图的顶部),为 [=12] 提供一个 ScrollController =] 并将控制器传递给 header 小部件以调整其大小
  2. 用一个ScrollController,传给CustomScrollView,用控制器的值调整SliverPersistentHeadermaxExtent(这就是Eugene recommended).
  3. 写我自己的 Sliver 来做我想做的事。

我 运行 遇到解决方案 1 和 2 的问题:

  1. 这个解决方案对我来说似乎有点 "hackish"。我也遇到了问题,"dragging" header 不再滚动,因为 header 不在 内部 CustomScrollView不再。
  2. 在滚动过程中调整条子的大小会导致 st运行ge 副作用。值得注意的是,header 和下面的条子之间的距离在滚动过程中会增加。

这就是我选择解决方案 3 的原因。我确定我实现它的方式不是最好的,但它完全符合我的要求:

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'dart:math' as math;

/// The delegate that is provided to [ElSliverPersistentHeader].
abstract class ElSliverPersistentHeaderDelegate {
  double get maxExtent;
  double get minExtent;

  /// This acts exactly like `SliverPersistentHeaderDelegate.build()` but with
  /// the difference that `shrinkOffset` might be negative, in which case,
  /// this widget exceeds `maxExtent`.
  Widget build(BuildContext context, double shrinkOffset);
}

/// Pretty much the same as `SliverPersistentHeader` but when the user
/// continues to drag down, the header grows in size, exceeding `maxExtent`.
class ElSliverPersistentHeader extends SingleChildRenderObjectWidget {
  final ElSliverPersistentHeaderDelegate delegate;
  ElSliverPersistentHeader({
    Key key,
    ElSliverPersistentHeaderDelegate delegate,
  })  : this.delegate = delegate,
        super(
            key: key,
            child:
                _ElSliverPersistentHeaderDelegateWrapper(delegate: delegate));

  @override
  _ElPersistentHeaderRenderSliver createRenderObject(BuildContext context) {
    return _ElPersistentHeaderRenderSliver(
        delegate.maxExtent, delegate.minExtent);
  }
}

class _ElSliverPersistentHeaderDelegateWrapper extends StatelessWidget {
  final ElSliverPersistentHeaderDelegate delegate;

  _ElSliverPersistentHeaderDelegateWrapper({Key key, this.delegate})
      : super(key: key);

  @override
  Widget build(BuildContext context) =>
      LayoutBuilder(builder: (context, constraints) {
        final height = constraints.maxHeight;
        return delegate.build(context, delegate.maxExtent - height);
      });
}

class _ElPersistentHeaderRenderSliver extends RenderSliver
    with RenderObjectWithChildMixin<RenderBox> {
  final double maxExtent;
  final double minExtent;

  _ElPersistentHeaderRenderSliver(this.maxExtent, this.minExtent);

  @override
  bool hitTestChildren(HitTestResult result,
      {@required double mainAxisPosition, @required double crossAxisPosition}) {
    if (child != null) {
      return child.hitTest(result,
          position: Offset(crossAxisPosition, mainAxisPosition));
    }
    return false;
  }

  @override
  void performLayout() {
    /// The amount of scroll that extends the theoretical limit.
    /// I.e.: when the user drags down the list, although it already hit the
    /// top.
    ///
    /// This seems to be a bit of a hack, but I haven't found a way to get this
    /// information in another way.
    final overScroll =
        constraints.viewportMainAxisExtent - constraints.remainingPaintExtent;

    /// The actual Size of the widget is the [maxExtent] minus the amount the
    /// user scrolled, but capped at the [minExtent] (we don't want the widget
    /// to become smaller than that).
    /// Additionally, we add the [overScroll] here, since if there *is*
    /// "over scroll", we want the widget to grow in size and exceed
    /// [maxExtent].
    final actualSize =
        math.max(maxExtent - constraints.scrollOffset + overScroll, minExtent);

    /// Now layout the child with the [actualSize] as `maxExtent`.
    child.layout(constraints.asBoxConstraints(maxExtent: actualSize));

    /// We "clip" the `paintExtent` to the `maxExtent`, otherwise the list
    /// below stops moving when reaching the border.
    ///
    /// Tbh, I'm not entirely sure why that is.
    final paintExtent = math.min(actualSize, maxExtent);

    /// For the layout to work properly (i.e.: the following slivers to
    /// scroll behind this sliver), the `layoutExtent` must not be capped
    /// at [minExtent], otherwise the next sliver will "stop" scrolling when
    /// [minExtent] is reached,
    final layoutExtent = math.max(maxExtent - constraints.scrollOffset, 0.0);

    geometry = SliverGeometry(
      scrollExtent: maxExtent,
      paintExtent: paintExtent,
      layoutExtent: layoutExtent,
      maxPaintExtent: maxExtent,
    );
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
      /// This sliver is always displayed at the top.
      context.paintChild(child, Offset(0.0, 0.0));
    }
  }
}

我通过简单地创建自定义 SliverPersistentHeaderDelegate.

解决了这个问题

只需覆盖 stretchConfiguration 的 getter。这是我的代码,以防有用。

class LargeCustomHeader extends SliverPersistentHeaderDelegate {
  LargeCustomHeader(
      {this.children,
      this.title = '',
      this.childrenHeight = 0,
      this.backgroundImage,
      this.titleHeight = 44,
      this.titleMaxLines = 1,
      this.titleTextStyle = const TextStyle(
          fontSize: 30,
          letterSpacing: 0.5,
          fontWeight: FontWeight.bold,
          height: 1.2,
          color: ColorConfig.primaryContrastColor)}) {}

  final List<Widget> children;
  final String title;
  final double childrenHeight;

  final String backgroundImage;

  final int _fadeDuration = 250;
  final double titleHeight;
  final int titleMaxLines;

  final double _navBarHeight = 56;

  final TextStyle titleTextStyle;

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
        constraints: BoxConstraints.expand(),
        decoration: BoxDecoration(
          // borderRadius: BorderRadius.vertical(bottom: Radius.circular(35.0)),
          color: Colors.black,
        ),
        child: Stack(
          fit: StackFit.loose,
          children: <Widget>[
            if (this.backgroundImage != null) ...[
              Positioned(
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                child: FadeInImage.assetNetwork(
                  placeholder: "assets/images/image-placeholder.png",
                  image: backgroundImage,
                  placeholderScale: 1,
                  fit: BoxFit.cover,
                  alignment: Alignment.center,
                  imageScale: 0.1,
                  fadeInDuration: const Duration(milliseconds: 500),
                  fadeOutDuration: const Duration(milliseconds: 200),
                ),
              ),
              Positioned(
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                child: Container(
                  color: Color.fromRGBO(0, 0, 0, 0.6),
                ),
              ),
            ],
            Positioned(
                bottom: 0,
                left: 0,
                right: 0,
                top: _navBarHeight + titleHeight,
                child: AnimatedOpacity(
                    opacity: (shrinkOffset >= childrenHeight / 3) ? 0 : 1,
                    duration: Duration(milliseconds: _fadeDuration),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: <Widget>[if (children != null) ...children],
                    ))),
            Positioned(
              top: _navBarHeight,
              left: 0,
              right: 0,
              height: titleHeight,
              child: Padding(
                padding: const EdgeInsets.only(
                    right: 30, bottom: 0, left: 30, top: 5),
                child: AnimatedOpacity(
                  opacity: (shrinkOffset >= childrenHeight + (titleHeight / 3))
                      ? 0
                      : 1,
                  duration: Duration(milliseconds: _fadeDuration),
                  child: Text(
                    title,
                    style: titleTextStyle,
                    maxLines: titleMaxLines,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ),
            ),
            Container(
              color: Colors.transparent,
              height: _navBarHeight,
              child: AppBar(
                  elevation: 0.0,
                  backgroundColor: Colors.transparent,
                  title: AnimatedOpacity(
                    opacity:
                        (shrinkOffset >= childrenHeight + (titleHeight / 3))
                            ? 1
                            : 0,
                    duration: Duration(milliseconds: _fadeDuration),
                    child: Text(
                      title,
                    ),
                  )),
            )
          ],
        ));
  }

  @override
  double get maxExtent => _navBarHeight + titleHeight + childrenHeight;

  @override
  double get minExtent => _navBarHeight;

  // @override
  // FloatingHeaderSnapConfiguration get snapConfiguration => FloatingHeaderSnapConfiguration() ;

  @override
  OverScrollHeaderStretchConfiguration get stretchConfiguration =>
      OverScrollHeaderStretchConfiguration(
        stretchTriggerOffset: maxExtent,
        onStretchTrigger: () {},
      );

  double get maxShrinkOffset => maxExtent - minExtent;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    //TODO: implement specific rebuild checks
    return true;
  }
}

您可以尝试将 SliverAppBarstretch:true 一起使用,并将要在应用栏中显示的小部件作为 flexibleSpace 传递。

这是一个例子

CustomScrollView(
  physics: BouncingScrollPhysics(),
  slivers: <Widget>[
    SliverAppBar(
      stretch: true,
      floating: true,
      backgroundColor: Colors.black,
      expandedHeight: 300,
      centerTitle: true,
      title: Text("My Custom Bar"),
      leading: IconButton(
        onPressed: () {},
        icon: Icon(Icons.menu),
      ),
      actions: <Widget>[
        IconButton(
          onPressed: () {},
          icon: Icon(Icons.search),
        )
      ],
      flexibleSpace: FlexibleSpaceBar(
        collapseMode: CollapseMode.pin,
        stretchModes: 
        [
          StretchMode.zoomBackground,
          StretchMode.blurBackground
        ],
        background: YourCustomWidget(),
      ),
    ),
    SliverList(
      delegate: SliverChildListDelegate(
        [
          Container(color: Colors.red, height: 300.0),
          Container(color: Colors.blue, height: 300.0),
        ],
      ),
    ),
  ],
);

现在您可以创建自己的 SliverPersistentHeaderDelegate 并覆盖此参数

@override
  OverScrollHeaderStretchConfiguration get stretchConfiguration =>
      OverScrollHeaderStretchConfiguration();

默认情况下为 null,但一旦添加,您就可以拉伸视图。

这是我使用的class:


class CustomSliverDelegate extends SliverPersistentHeaderDelegate {
  final Widget child;
  final Widget title;
  final Widget background;
  final double topSafeArea;
  final double maxExtent;

  CustomSliverDelegate({
    this.title,
    this.child,
    this.maxExtent = 350,
    this.background,
    this.topSafeArea = 0,
  });

  @override
  Widget build(BuildContext context, double shrinkOffset,
      bool overlapsContent) {
    final appBarSize = maxExtent - shrinkOffset;
    final proportion = 2 - (maxExtent / appBarSize);
    final percent = proportion < 0 || proportion > 1 ? 0.0 : proportion;
    return Theme(
      data: ThemeData.dark(),
      child: ConstrainedBox(
        constraints: BoxConstraints(minHeight: maxExtent),
        child: Stack(
          children: [
            Positioned(
              bottom: 0.0,
              left: 0.0,
              right: 0.0,
              top: 0,
              child: background,
            ),
            Positioned(
              bottom: 0.0,
              left: 0.0,
              right: 0.0,
              child: Opacity(opacity: percent, child: child),
            ),
            Positioned(
              top: 0.0,
              left: 0.0,
              right: 0.0,
              child: AppBar(
                title: Opacity(opacity: 1 - percent, child: title),
                backgroundColor: Colors.transparent,
                elevation: 0,
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  OverScrollHeaderStretchConfiguration get stretchConfiguration =>
      OverScrollHeaderStretchConfiguration();

  @override
  double get minExtent => kToolbarHeight + topSafeArea;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }
}

可能是我有一个简单的编码方法。

通过使用 SliverAppBar 和内部子部件 leadingFlexibleSpaceBar 和内部子部件 title.

然后 LayoutBuilder 我们可以制作一些动画。

Full code link

SliverAppBar(
  toolbarHeight: _appBarHeight,
  collapsedHeight: _appBarHeight,
  backgroundColor: Colors.white.withOpacity(1),
  shadowColor: Colors.white.withOpacity(0),
  expandedHeight: maxWidth,
  /// ========================================
  /// custom your app bar
  /// ========================================
  leading: Container(
    width: 100,
    height: _appBarHeight,
    // color: Colors.blueAccent,
    child: Center(
      child: Icon(Icons.arrow_back_ios, color: Colors.black),
    ),
  ),
  pinned: true,
  stretch: true,
  flexibleSpace: FlexibleSpaceBar(
    stretchModes: [
      StretchMode.fadeTitle,
      StretchMode.blurBackground,
      StretchMode.zoomBackground,
    ],
    titlePadding: EdgeInsets.all(0),
    title: LayoutBuilder(
      builder: (_, __) {
        var height = __.maxHeight;
        /// ========================================
        /// custom animate you want by height change
        /// ========================================
        // Logger.debug(__.maxHeight);
        return Stack(
          children: [
            if (height > 100)
              Container(
                width: double.infinity,
                height: double.infinity,
                color: Colors.black.withOpacity(0.3),
              ),
          ],
        );
      },
    ),
    background: Image.network(
      'https://xx',
      fit: BoxFit.cover,
    ),
  ),
),