如何让 SliverPersistentHeader “过度生长”

Posted

技术标签:

【中文标题】如何让 SliverPersistentHeader “过度生长”【英文标题】:How to get the SliverPersistentHeader to "overgrow" 【发布时间】:2019-09-24 02:17:54 【问题描述】:

我在我的CustomScrollView 中使用SliverPersistentHeader 来拥有一个持久的标题,当用户滚动时它会收缩和增长,但是当它达到最大尺寸时感觉有点僵硬,因为它不会“过度生长” .

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

.

【问题讨论】:

【参考方案1】:

编辑:我在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() 属性。 没有复杂的听众,舞台小部件等。只需 FlexibleSpaceBar 并在 background 上添加图片。

【讨论】:

谢谢!但是当我向下滚动时,这不会增加SliverAppBar 的大小。我尝试修改您的代码,因此它也增加了大小,但这显然不起作用,因为它会干扰其余的滚动项。 谢谢,我会试试的。仅供参考,我使用的是SliverPersistentHeader 而不是SliverAppBar,所以我不确定这是否也能正常工作。 我刚试过,它也适用于 SliverPersistentHeader Mh,但在您的屏幕截图中,我认为同样的问题是可见的:当您查看被向下拖动的列表(项目 0、项目 1 等...)时,列表和标题增加。你知道为什么会这样吗?【参考方案2】:

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

    创建一个包含CustomScrollView 和一个标题小部件(覆盖在滚动视图顶部)的Stack,向CustomScrollView 提供一个ScrollController,并将控制器传递给标题小部件以调整其大小 使用ScrollController,将其传递给CustomScrollView,并使用控制器的值来调整SliverPersistentHeadermaxExtent(这就是Eugene recommended)。 编写我自己的 Sliver 来做我想做的事。

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

    这个解决方案在我看来有点“骇人听闻”。我也遇到了问题,“拖动”标题不再滚动,因为标题不再 inside CustomScrollView。 在滚动过程中调整条子的大小会导致奇怪的副作用。值得注意的是,在滚动过程中,标题和下面的条子之间的距离会增加。

这就是我选择解决方案 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));
    
  

【讨论】:

【参考方案3】:

我通过简单地创建一个自定义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;
  

【讨论】:

【参考方案4】:

您可以尝试将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),
        ],
      ),
    ),
  ],
);

【讨论】:

【参考方案5】:

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

@override
  OverScrollHeaderStretchConfiguration get stretchConfiguration =>
      OverScrollHeaderStretchConfiguration();

默认情况下如果为null,但一旦添加它就会允许你拉伸视图。

这是我使用的类:


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;
  

【讨论】:

【参考方案6】:

也许我有一个简单的编码方法。

通过使用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,
    ),
  ),
),

【讨论】:

以上是关于如何让 SliverPersistentHeader “过度生长”的主要内容,如果未能解决你的问题,请参考以下文章

Jfreechart柱状图如何让变成纯色

android中如何让文字环绕图片

如何让GridView 标题居中

请教如何让UIActivityIndicatorView永远居中

如何让ActionBar标题居中

EBS如何让从值集不显示