如何使SliverPersistentHeader“过度增长”

eny*_*nyo 5 flutter flutter-sliver flutter-layout

我使用的是SliverPersistentHeaderCustomScrollView有一个永久报头是收缩,生长的用户滚动的时候,但是,当它达到其最大大小,因为它没有“长满”感觉有点僵硬。

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

行为视频

eny*_*nyo 11

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

  1. 创建一个Stack包含CustomScrollView和报头部件(覆盖在滚动视图的顶部),提供一个ScrollControllerCustomScrollView与控制器传递到头部插件来调整其大小
  2. 使用 a ScrollController,将其传递给CustomScrollView并使用控制器的值来调整maxExtentSliverPersistentHeader(这是Eugene 推荐的)。
  3. 写我自己的 Sliver 来做我想做的事。

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

  1. 这个解决方案对我来说似乎有点“hackish”。我也有这个问题,即“拖”的标题没有滚动了,因为头是不是里面CustomScrollView了。
  2. 在滚动期间调整条子的大小会导致奇怪的副作用。值得注意的是,在滚动过程中,集管和下方的条子之间的距离会增加。

这就是我选择解决方案 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));
    }
  }
}
Run Code Online (Sandbox Code Playgroud)


小智 9

编辑:我发现了另一种如何在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'),
                );
              }),
            ),
          ),
        ],
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

神奇之处在于 -stretch: trueBouncingScrollPhysics()属性。
没有复杂的侦听器、阶段性小部件等。只需FlexibleSpaceBarbackground.


jam*_*sco 7

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

@override
  OverScrollHeaderStretchConfiguration get stretchConfiguration =>
      OverScrollHeaderStretchConfiguration();

Run Code Online (Sandbox Code Playgroud)

默认情况下,如果为 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;
  }
}
Run Code Online (Sandbox Code Playgroud)