How to get the SliverPersistentHeader to “overgrow”

与世无争的帅哥 提交于 2020-05-24 15:55:11

问题


I'm using a SliverPersistentHeader in my CustomScrollView to have a persistent header that shrinks and grows when the user scrolls, but when it reaches its maximum size it feels a bit stiff since it doesn't "overgrow".

Here is a video of the behaviour I want (from the Spotify app) and the behaviour I have:

Video of behaviour.


回答1:


While looking for a solution for this problem, I came across three different ways to solve it:

  1. Create a Stack that contains the CustomScrollView and a header widget (overlaid on top of the scroll view), provide a ScrollController to the CustomScrollView and pass the controller to the header widget to adjust its size
  2. Use a ScrollController, pass it to the CustomScrollView and use the value of the controller to adjust the maxExtent of the SliverPersistentHeader (this is what Eugene recommended).
  3. Write my own Sliver to do exactly what I want.

I ran into problems with solution 1 & 2:

  1. This solution seemed a bit "hackish" to me. I also had the problem, that "dragging" the header didn't scroll anymore, since the header was not inside the CustomScrollView anymore.
  2. Adjusting the size of the sliver during scrolling results in strange side effects. Notably, the distance between the header and slivers below increases during the scroll.

That's why I opted for solution 3. I'm sure the way I implemented it, is not the best, but it works exactly as I want:

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));
    }
  }
}



回答2:


as an option, you can just copy-paste this code to see how it works

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Page(),
      ),
    );
  }
}

class Page extends StatefulWidget {
  @override
  _PageState createState() => _PageState();
}

class _PageState extends State<Page> {
  static final double _initialToolbarHeight = 300;
  static final double _maxSizeFactor = 1.3; // image max size will 130%
  static final double _transformSpeed = 0.001; // 0.1 very fast,   0.001 slow

  ScrollController _controller;
  double _factor = 1;
  double _expandedToolbarHeight = _initialToolbarHeight;

  @override
  void initState() {
    _controller = ScrollController();
    _controller.addListener(_scrollListener);
    super.initState();
  }

  _scrollListener() {
    if (_controller.offset < 0) {
      _factor = 1 + _controller.offset.abs() * _transformSpeed;
      _factor = _factor.clamp(1, _maxSizeFactor);
      _expandedToolbarHeight = _initialToolbarHeight + _controller.offset.abs(); //
    } else {
      _factor = 1;
      _expandedToolbarHeight = _initialToolbarHeight; //
    }
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      physics: const BouncingScrollPhysics(),
      controller: _controller,
      slivers: [
        SliverPersistentHeader(
          floating: false,
          pinned: true,
          delegate: _SliverAppBarDelegate(
            minHeight: 300,
            maxHeight: 300,
            child: Transform.scale(
              scale: _factor,
              child: Image.network('https://picsum.photos/id/1025/990/660', fit: BoxFit.cover),
            ),
          ),
        ),
        SliverList(
          delegate: SliverChildListDelegate(getItems()),
        )
      ],
    );
  }

  getItems() {
    return List.generate(50, (pos) {
      return Container(
        height: 64,
        child: Text('item $pos'),
      );
    });
  }
}

class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
    @required this.minHeight,
    @required this.maxHeight,
    @required this.child,
  });

  final double minHeight;
  final double maxHeight;
  final Widget child;

  @override
  double get minExtent => minHeight;

  @override
  double get maxExtent => maxHeight;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new SizedBox.expand(child: child);
  }

  @override
  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child;
  }
}

Video of behaviour.




回答3:


I solved this problem by simply creating a custom SliverPersistentHeaderDelegate.

Just override the getter for stretchConfiguration. Here's my code in case this is useful.

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:


You can try using SliverAppBar with stretch:true and pass the widget you want to display in the appbar as flexibleSpace.

Here is an example

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),
        ],
      ),
    ),
  ],
);


来源:https://stackoverflow.com/questions/56005307/how-to-get-the-sliverpersistentheader-to-overgrow

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!