如何让 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: true
和 BouncingScrollPhysics()
属性。
没有复杂的听众,舞台小部件等。只需 FlexibleSpaceBar
并在 background
上添加图片。
【讨论】:
谢谢!但是当我向下滚动时,这不会增加SliverAppBar
的大小。我尝试修改您的代码,因此它也增加了大小,但这显然不起作用,因为它会干扰其余的滚动项。
谢谢,我会试试的。仅供参考,我使用的是SliverPersistentHeader
而不是SliverAppBar
,所以我不确定这是否也能正常工作。
我刚试过,它也适用于 SliverPersistentHeader
Mh,但在您的屏幕截图中,我认为同样的问题是可见的:当您查看被向下拖动的列表(项目 0、项目 1 等...)时,列表和标题增加。你知道为什么会这样吗?【参考方案2】:
在寻找解决此问题的方法时,我遇到了三种不同的解决方法:
-
创建一个包含
CustomScrollView
和一个标题小部件(覆盖在滚动视图顶部)的Stack
,向CustomScrollView
提供一个ScrollController
,并将控制器传递给标题小部件以调整其大小
使用ScrollController
,将其传递给CustomScrollView
,并使用控制器的值来调整SliverPersistentHeader
的maxExtent
(这就是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】:您可以尝试将SliverAppBar
与stretch: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
和内部子小部件leading
、FlexibleSpaceBar
和内部子小部件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 “过度生长”的主要内容,如果未能解决你的问题,请参考以下文章