Unverified Commit 11e1c240 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

SliverAppBar - Configurable overscroll stretch with callback feature &...

SliverAppBar - Configurable overscroll stretch with callback feature & FlexibleSpaceBar support (#42250)
parent 0f6c093d
......@@ -690,6 +690,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required this.floating,
@required this.pinned,
@required this.snapConfiguration,
@required this.stretchConfiguration,
@required this.shape,
}) : assert(primary || topPadding == 0.0),
_bottomHeight = bottom?.preferredSize?.height ?? 0.0;
......@@ -728,6 +729,9 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@override
final FloatingHeaderSnapConfiguration snapConfiguration;
@override
final OverScrollHeaderStretchConfiguration stretchConfiguration;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final double visibleMainHeight = maxExtent - shrinkOffset - topPadding;
......@@ -800,7 +804,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|| topPadding != oldDelegate.topPadding
|| pinned != oldDelegate.pinned
|| floating != oldDelegate.floating
|| snapConfiguration != oldDelegate.snapConfiguration;
|| snapConfiguration != oldDelegate.snapConfiguration
|| stretchConfiguration != oldDelegate.stretchConfiguration;
}
@override
......@@ -914,6 +919,9 @@ class SliverAppBar extends StatefulWidget {
this.floating = false,
this.pinned = false,
this.snap = false,
this.stretch = false,
this.stretchTriggerOffset = 100.0,
this.onStretchTrigger,
this.shape,
}) : assert(automaticallyImplyLeading != null),
assert(forceElevated != null),
......@@ -922,7 +930,9 @@ class SliverAppBar extends StatefulWidget {
assert(floating != null),
assert(pinned != null),
assert(snap != null),
assert(stretch != null),
assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
assert(stretchTriggerOffset > 0.0),
super(key: key);
/// A widget to display before the [title].
......@@ -1132,10 +1142,9 @@ class SliverAppBar extends StatefulWidget {
/// behavior of the app bar in combination with [floating].
final bool pinned;
/// The material's shape as well its shadow.
/// The material's shape as well as its shadow.
///
/// A shadow is only displayed if the [elevation] is greater than
/// zero.
/// A shadow is only displayed if the [elevation] is greater than zero.
final ShapeBorder shape;
/// If [snap] and [floating] are true then the floating app bar will "snap"
......@@ -1165,6 +1174,21 @@ class SliverAppBar extends StatefulWidget {
/// behavior of the app bar in combination with [pinned] and [floating].
final bool snap;
/// Whether the app bar should stretch to fill the over-scroll area.
///
/// The app bar can still expand and contract as the user scrolls, but it will
/// also stretch when the user over-scrolls.
final bool stretch;
/// The offset of overscroll required to activate [onStretchTrigger].
///
/// This defaults to 100.0.
final double stretchTriggerOffset;
/// The callback function to be executed when a user over-scrolls to the
/// offset specified by [stretchTriggerOffset].
final AsyncCallback onStretchTrigger;
@override
_SliverAppBarState createState() => _SliverAppBarState();
}
......@@ -1173,6 +1197,7 @@ class SliverAppBar extends StatefulWidget {
// by the floating appbar snap animation (via FloatingHeaderSnapConfiguration).
class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMixin {
FloatingHeaderSnapConfiguration _snapConfiguration;
OverScrollHeaderStretchConfiguration _stretchConfiguration;
void _updateSnapConfiguration() {
if (widget.snap && widget.floating) {
......@@ -1186,10 +1211,22 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
}
}
void _updateStretchConfiguration() {
if (widget.stretch) {
_stretchConfiguration = OverScrollHeaderStretchConfiguration(
stretchTriggerOffset: widget.stretchTriggerOffset,
onStretchTrigger: widget.onStretchTrigger,
);
} else {
_stretchConfiguration = null;
}
}
@override
void initState() {
super.initState();
_updateSnapConfiguration();
_updateStretchConfiguration();
}
@override
......@@ -1197,6 +1234,8 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
super.didUpdateWidget(oldWidget);
if (widget.snap != oldWidget.snap || widget.floating != oldWidget.floating)
_updateSnapConfiguration();
if (widget.stretch != oldWidget.stretch)
_updateStretchConfiguration();
}
@override
......@@ -1236,6 +1275,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
pinned: widget.pinned,
shape: widget.shape,
snapConfiguration: _snapConfiguration,
stretchConfiguration: _stretchConfiguration,
),
),
);
......
......@@ -3,14 +3,16 @@
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
/// The collapsing effect while the space bar expands or collapses.
/// The collapsing effect while the space bar collapses from its full size.
enum CollapseMode {
/// The background widget will scroll in a parallax fashion.
parallax,
......@@ -22,17 +24,115 @@ enum CollapseMode {
none,
}
/// The part of a material design [AppBar] that expands and collapses.
/// The stretching effect while the space bar stretches beyond its full size.
enum StretchMode {
/// The background widget will expand to fill the extra space.
zoomBackground,
/// The background will blur using a [ImageFilter.blur] effect.
blurBackground,
/// The title will fade away as the user over-scrolls.
fadeTitle,
}
/// The part of a material design [AppBar] that expands, collapses, and
/// stretches.
///
/// Most commonly used in in the [SliverAppBar.flexibleSpace] field, a flexible
/// space bar expands and contracts as the app scrolls so that the [AppBar]
/// reaches from the top of the app to the top of the scrolling contents of the
/// app.
/// app. Furthermore is included functionality for stretch behavior. When
/// [SliverAppBar.stretch] is true, and your [ScrollPhysics] allow for
/// overscroll, this space will stretch with the overscroll.
///
/// The widget that sizes the [AppBar] must wrap it in the widget returned by
/// [FlexibleSpaceBar.createSettings], to convey sizing information down to the
/// [FlexibleSpaceBar].
///
/// {@tool snippet --template=freeform}
/// This sample application demonstrates the different features of the
/// [FlexibleSpaceBar] when used in a [SliverAppBar]. This app bar is configured
/// to stretch into the overscroll space, and uses the
/// [FlexibleSpaceBar.stretchModes] to apply `fadeTitle`, `blurBackground` and
/// `zoomBackground`. The app bar also makes use of [CollapseMode.parallax] by
/// default.
///
/// ```dart imports
/// import 'package:flutter/material.dart';
/// ```
/// ```dart
/// void main() => runApp(MaterialApp(home: MyApp()));
///
/// class MyApp extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: CustomScrollView(
/// physics: const BouncingScrollPhysics(),
/// slivers: <Widget>[
/// SliverAppBar(
/// stretch: true,
/// onStretchTrigger: () {
/// // Function callback for stretch
/// return;
/// },
/// expandedHeight: 300.0,
/// flexibleSpace: FlexibleSpaceBar(
/// stretchModes: <StretchMode>[
/// StretchMode.zoomBackground,
/// StretchMode.blurBackground,
/// StretchMode.fadeTitle,
/// ],
/// centerTitle: true,
/// title: const Text('Flight Report'),
/// background: Stack(
/// fit: StackFit.expand,
/// children: [
/// Image.network(
/// 'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg',
/// fit: BoxFit.cover,
/// ),
/// const DecoratedBox(
/// decoration: BoxDecoration(
/// gradient: LinearGradient(
/// begin: Alignment(0.0, 0.5),
/// end: Alignment(0.0, 0.0),
/// colors: <Color>[
/// Color(0x60000000),
/// Color(0x00000000),
/// ],
/// ),
/// ),
/// ),
/// ],
/// ),
/// ),
/// ),
/// SliverList(
/// delegate: SliverChildListDelegate([
/// ListTile(
/// leading: Icon(Icons.wb_sunny),
/// title: Text('Sunday'),
/// subtitle: Text('sunny, h: 80, l: 65'),
/// ),
/// ListTile(
/// leading: Icon(Icons.wb_sunny),
/// title: Text('Monday'),
/// subtitle: Text('sunny, h: 80, l: 65'),
/// ),
/// // ListTiles++
/// ]),
/// ),
/// ],
/// ),
/// );
/// }
/// }
///
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [SliverAppBar], which implements the expanding and contracting.
......@@ -49,6 +149,7 @@ class FlexibleSpaceBar extends StatefulWidget {
this.centerTitle,
this.titlePadding,
this.collapseMode = CollapseMode.parallax,
this.stretchModes = const <StretchMode>[StretchMode.zoomBackground],
}) : assert(collapseMode != null),
super(key: key);
......@@ -73,6 +174,11 @@ class FlexibleSpaceBar extends StatefulWidget {
/// Defaults to [CollapseMode.parallax].
final CollapseMode collapseMode;
/// Stretch effect while over-scrolling,
///
/// Defaults to include [StretchMode.zoomBackground].
final List<StretchMode> stretchModes;
/// Defines how far the [title] is inset from either the widget's
/// bottom-left or its center.
///
......@@ -167,8 +273,13 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final FlexibleSpaceBarSettings settings = context.inheritFromWidgetOfExactType(FlexibleSpaceBarSettings);
assert(settings != null, 'A FlexibleSpaceBar must be wrapped in the widget returned by FlexibleSpaceBar.createSettings().');
assert(
settings != null,
'A FlexibleSpaceBar must be wrapped in the widget returned by FlexibleSpaceBar.createSettings().',
);
final List<Widget> children = <Widget>[];
......@@ -178,26 +289,52 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
// 1.0 -> Collapsed to toolbar
final double t = (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent).clamp(0.0, 1.0);
// background image
// background
if (widget.background != null) {
final double fadeStart = math.max(0.0, 1.0 - kToolbarHeight / deltaExtent);
const double fadeEnd = 1.0;
assert(fadeStart <= fadeEnd);
final double opacity = 1.0 - Interval(fadeStart, fadeEnd).transform(t);
if (opacity > 0.0) {
double height = settings.maxExtent;
// StretchMode.zoomBackground
if (widget.stretchModes.contains(StretchMode.zoomBackground) &&
constraints.maxHeight > height) {
height = constraints.maxHeight;
}
children.add(Positioned(
top: _getCollapsePadding(t, settings),
left: 0.0,
right: 0.0,
height: settings.maxExtent,
height: height,
child: Opacity(
opacity: opacity,
child: widget.background,
),
));
// StretchMode.blurBackground
if (widget.stretchModes.contains(StretchMode.blurBackground) &&
constraints.maxHeight > settings.maxExtent) {
final double blurAmount = (constraints.maxHeight - settings.maxExtent) / 10;
children.add(Positioned.fill(
child: BackdropFilter(
child: Container(
color: Colors.transparent,
),
filter: ui.ImageFilter.blur(
sigmaX: blurAmount,
sigmaY: blurAmount,
)
)
));
}
}
}
// title
if (widget.title != null) {
final ThemeData theme = Theme.of(context);
......@@ -214,6 +351,17 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
);
}
// StretchMode.fadeTitle
if (widget.stretchModes.contains(StretchMode.fadeTitle) &&
constraints.maxHeight > settings.maxExtent) {
final double stretchOpacity = 1 -
((constraints.maxHeight - settings.maxExtent) / 100).clamp(0.0, 1.0);
title = Opacity(
opacity: stretchOpacity,
child: title,
);
}
final double opacity = settings.toolbarOpacity;
if (opacity > 0.0) {
TextStyle titleStyle = theme.primaryTextTheme.title;
......@@ -249,6 +397,8 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
return ClipRect(child: Stack(children: children));
}
);
}
}
/// Provides sizing and opacity information to a [FlexibleSpaceBar].
......
......@@ -17,6 +17,28 @@ import 'sliver.dart';
import 'viewport.dart';
import 'viewport_offset.dart';
/// Specifies how a stretched header is to trigger an [AsyncCallback].
///
/// See also:
///
/// * [SliverAppBar], which creates a header that can be stretched into an
/// overscroll area and trigger a callback function.
class OverScrollHeaderStretchConfiguration {
/// Creates an object that specifies how a stretched header may activate an
/// [AsyncCallback].
OverScrollHeaderStretchConfiguration({
this.stretchTriggerOffset = 100.0,
this.onStretchTrigger,
}) : assert(stretchTriggerOffset != null);
/// The offset of overscroll required to trigger the [onStretchTrigger].
final double stretchTriggerOffset;
/// The callback function to be executed when a user over-scrolls to the
/// offset specified by [stretchTriggerOffset].
final AsyncCallback onStretchTrigger;
}
/// A base class for slivers that have a [RenderBox] child which scrolls
/// normally, except that when it hits the leading edge (typically the top) of
/// the viewport, it shrinks to a minimum size ([minExtent]).
......@@ -38,10 +60,15 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
/// viewport.
///
/// This is an abstract class; this constructor only initializes the [child].
RenderSliverPersistentHeader({ RenderBox child }) {
RenderSliverPersistentHeader({
RenderBox child,
this.stretchConfiguration,
}) {
this.child = child;
}
double _lastStretchOffset;
/// The biggest that this render object can become, in the main axis direction.
///
/// This value should not be based on the child. If it changes, call
......@@ -76,6 +103,17 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
double _lastShrinkOffset = 0.0;
bool _lastOverlapsContent = false;
/// Defines the parameters used to execute an [AsyncCallback] when a
/// stretching header over-scrolls.
///
/// If [stretchConfiguration] is null then callback is not triggered.
///
/// See also:
///
/// * [SliverAppBar], which creates a header that can stretched into an
/// overscroll area and trigger a callback function.
OverScrollHeaderStretchConfiguration stretchConfiguration;
/// Update the child render object if necessary.
///
/// Called before the first layout, any time [markNeedsLayout] is called, and
......@@ -138,10 +176,24 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
DoubleProperty('The specified minExtent was', minExtent),
]);
}());
double stretchOffset = 0.0;
if (stretchConfiguration != null && childMainAxisPosition(child) == 0.0)
stretchOffset += constraints.overlap.abs();
child?.layout(
constraints.asBoxConstraints(maxExtent: math.max(minExtent, maxExtent - shrinkOffset)),
constraints.asBoxConstraints(
maxExtent: math.max(minExtent, maxExtent - shrinkOffset) + stretchOffset,
),
parentUsesSize: true,
);
if (stretchConfiguration != null &&
stretchConfiguration.onStretchTrigger != null &&
stretchOffset >= stretchConfiguration.stretchTriggerOffset &&
_lastStretchOffset <= stretchConfiguration.stretchTriggerOffset) {
stretchConfiguration.onStretchTrigger();
}
_lastStretchOffset = stretchOffset;
}
/// Returns the distance from the leading _visible_ edge of the sliver to the
......@@ -245,12 +297,38 @@ abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersist
/// scrolls off.
RenderSliverScrollingPersistentHeader({
RenderBox child,
}) : super(child: child);
OverScrollHeaderStretchConfiguration stretchConfiguration,
}) : super(
child: child,
stretchConfiguration: stretchConfiguration,
);
// Distance from our leading edge to the child's leading edge, in the axis
// direction. Negative if we're scrolled off the top.
double _childPosition;
/// Updates [geometry], and returns the new value for [childMainAxisPosition].
///
/// This is used by [performLayout].
@protected
double updateGeometry() {
double stretchOffset = 0.0;
if (stretchConfiguration != null && _childPosition == 0.0) {
stretchOffset += constraints.overlap.abs();
}
final double maxExtent = this.maxExtent;
final double paintExtent = maxExtent - constraints.scrollOffset;
geometry = SliverGeometry(
scrollExtent: maxExtent,
paintOrigin: math.min(constraints.overlap, 0.0),
paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
maxPaintExtent: maxExtent + stretchOffset,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent);
}
@override
void performLayout() {
final double maxExtent = this.maxExtent;
......@@ -263,7 +341,7 @@ abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersist
maxPaintExtent: maxExtent,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
_childPosition = math.min(0.0, paintExtent - childExtent);
_childPosition = updateGeometry();
}
@override
......@@ -283,7 +361,11 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
/// stays pinned there.
RenderSliverPinnedPersistentHeader({
RenderBox child,
}) : super(child: child);
OverScrollHeaderStretchConfiguration stretchConfiguration,
}) : super(
child: child,
stretchConfiguration: stretchConfiguration,
);
@override
void performLayout() {
......@@ -292,12 +374,15 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
excludeFromSemanticsScrolling = overlapsContent || (constraints.scrollOffset > maxExtent - minExtent);
layoutChild(constraints.scrollOffset, maxExtent, overlapsContent: overlapsContent);
final double layoutExtent = (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);
final double stretchOffset = stretchConfiguration != null ?
constraints.overlap.abs() :
0.0;
geometry = SliverGeometry(
scrollExtent: maxExtent,
paintOrigin: constraints.overlap,
paintExtent: math.min(childExtent, constraints.remainingPaintExtent),
layoutExtent: layoutExtent,
maxPaintExtent: maxExtent,
maxPaintExtent: maxExtent + stretchOffset,
maxScrollObstructionExtent: minExtent,
cacheExtent: layoutExtent > 0.0 ? -constraints.cacheOrigin + layoutExtent : layoutExtent,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
......@@ -355,8 +440,12 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
RenderSliverFloatingPersistentHeader({
RenderBox child,
FloatingHeaderSnapConfiguration snapConfiguration,
OverScrollHeaderStretchConfiguration stretchConfiguration,
}) : _snapConfiguration = snapConfiguration,
super(child: child);
super(
child: child,
stretchConfiguration: stretchConfiguration,
);
AnimationController _controller;
Animation<double> _animation;
......@@ -406,6 +495,10 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
/// This is used by [performLayout].
@protected
double updateGeometry() {
double stretchOffset = 0.0;
if (stretchConfiguration != null && _childPosition == 0.0) {
stretchOffset += constraints.overlap.abs();
}
final double maxExtent = this.maxExtent;
final double paintExtent = maxExtent - _effectiveScrollOffset;
final double layoutExtent = maxExtent - constraints.scrollOffset;
......@@ -414,10 +507,10 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
paintOrigin: math.min(constraints.overlap, 0.0),
paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
layoutExtent: layoutExtent.clamp(0.0, constraints.remainingPaintExtent),
maxPaintExtent: maxExtent,
maxPaintExtent: maxExtent + stretchOffset,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
return math.min(0.0, paintExtent - childExtent);
return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent);
}
/// If the header isn't already fully exposed, then scroll it into view.
......@@ -463,6 +556,7 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
((constraints.scrollOffset < _lastActualScrollOffset) || // we are scrolling back, so should reveal, or
(_effectiveScrollOffset < maxExtent))) { // some part of it is visible, so should shrink or reveal as appropriate.
double delta = _lastActualScrollOffset - constraints.scrollOffset;
final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward;
if (allowFloatingExpansion) {
if (_effectiveScrollOffset > maxExtent) // We're scrolled off-screen, but should reveal, so
......@@ -477,7 +571,12 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
}
excludeFromSemanticsScrolling = _effectiveScrollOffset <= constraints.scrollOffset;
final bool overlapsContent = _effectiveScrollOffset < constraints.scrollOffset;
layoutChild(_effectiveScrollOffset, maxExtent, overlapsContent: overlapsContent);
layoutChild(
_effectiveScrollOffset,
maxExtent,
overlapsContent: overlapsContent,
);
_childPosition = updateGeometry();
_lastActualScrollOffset = constraints.scrollOffset;
}
......@@ -510,22 +609,35 @@ abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFl
RenderSliverFloatingPinnedPersistentHeader({
RenderBox child,
FloatingHeaderSnapConfiguration snapConfiguration,
}) : super(child: child, snapConfiguration: snapConfiguration);
OverScrollHeaderStretchConfiguration stretchConfiguration,
}) : super(
child: child,
snapConfiguration: snapConfiguration,
stretchConfiguration: stretchConfiguration,
);
@override
double updateGeometry() {
final double minExtent = this.minExtent;
final double minAllowedExtent = constraints.remainingPaintExtent > minExtent ? minExtent : constraints.remainingPaintExtent;
final double minAllowedExtent = constraints.remainingPaintExtent > minExtent ?
minExtent :
constraints.remainingPaintExtent;
final double maxExtent = this.maxExtent;
final double paintExtent = maxExtent - _effectiveScrollOffset;
final double clampedPaintExtent = paintExtent.clamp(minAllowedExtent, constraints.remainingPaintExtent);
final double clampedPaintExtent = paintExtent.clamp(
minAllowedExtent,
constraints.remainingPaintExtent,
);
final double layoutExtent = maxExtent - constraints.scrollOffset;
final double stretchOffset = stretchConfiguration != null ?
constraints.overlap.abs() :
0.0;
geometry = SliverGeometry(
scrollExtent: maxExtent,
paintOrigin: math.min(constraints.overlap, 0.0),
paintExtent: clampedPaintExtent,
layoutExtent: layoutExtent.clamp(0.0, clampedPaintExtent),
maxPaintExtent: maxExtent,
maxPaintExtent: maxExtent + stretchOffset,
maxScrollObstructionExtent: maxExtent,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
......
......@@ -68,6 +68,17 @@ abstract class SliverPersistentHeaderDelegate {
/// Defaults to null.
FloatingHeaderSnapConfiguration get snapConfiguration => null;
/// Specifies an [AsyncCallback] and offset for execution.
///
/// If the value of this property is null, then callback will not be
/// triggered.
///
/// This is only used for stretching headers (those with
/// [SliverAppBar.stretch] set to true).
///
/// Defaults to null.
OverScrollHeaderStretchConfiguration get stretchConfiguration => null;
/// Whether this delegate is meaningfully different from the old delegate.
///
/// If this returns false, then the header might not be rebuilt, even though
......@@ -142,7 +153,12 @@ class SliverPersistentHeader extends StatelessWidget {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<SliverPersistentHeaderDelegate>('delegate', delegate));
properties.add(
DiagnosticsProperty<SliverPersistentHeaderDelegate>(
'delegate',
delegate,
)
);
final List<String> flags = <String>[
if (pinned) 'pinned',
if (floating) 'floating',
......@@ -195,7 +211,15 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
void _build(double shrinkOffset, bool overlapsContent) {
owner.buildScope(this, () {
child = updateChild(child, widget.delegate.build(this, shrinkOffset, overlapsContent), null);
child = updateChild(
child,
widget.delegate.build(
this,
shrinkOffset,
overlapsContent
),
null,
);
});
}
......@@ -246,7 +270,12 @@ abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWid
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(DiagnosticsProperty<SliverPersistentHeaderDelegate>('delegate', delegate));
description.add(
DiagnosticsProperty<SliverPersistentHeaderDelegate>(
'delegate',
delegate,
)
);
}
}
......@@ -275,75 +304,128 @@ class _SliverScrollingPersistentHeader extends _SliverPersistentHeaderRenderObje
const _SliverScrollingPersistentHeader({
Key key,
@required SliverPersistentHeaderDelegate delegate,
}) : super(key: key, delegate: delegate);
}) : super(
key: key,
delegate: delegate,
);
@override
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
return _RenderSliverScrollingPersistentHeaderForWidgets();
return _RenderSliverScrollingPersistentHeaderForWidgets(
stretchConfiguration: delegate.stretchConfiguration
);
}
}
class _RenderSliverScrollingPersistentHeaderForWidgets extends RenderSliverScrollingPersistentHeader
with _RenderSliverPersistentHeaderForWidgetsMixin { }
with _RenderSliverPersistentHeaderForWidgetsMixin {
_RenderSliverScrollingPersistentHeaderForWidgets({
RenderBox child,
OverScrollHeaderStretchConfiguration stretchConfiguration,
}) : super(
child: child,
stretchConfiguration: stretchConfiguration,
);
}
class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
const _SliverPinnedPersistentHeader({
Key key,
@required SliverPersistentHeaderDelegate delegate,
}) : super(key: key, delegate: delegate);
}) : super(
key: key,
delegate: delegate,
);
@override
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
return _RenderSliverPinnedPersistentHeaderForWidgets();
return _RenderSliverPinnedPersistentHeaderForWidgets(
stretchConfiguration: delegate.stretchConfiguration
);
}
}
class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin { }
class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPersistentHeader
with _RenderSliverPersistentHeaderForWidgetsMixin {
_RenderSliverPinnedPersistentHeaderForWidgets({
RenderBox child,
OverScrollHeaderStretchConfiguration stretchConfiguration,
}) : super(
child: child,
stretchConfiguration: stretchConfiguration,
);
}
class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
const _SliverFloatingPersistentHeader({
Key key,
@required SliverPersistentHeaderDelegate delegate,
}) : super(key: key, delegate: delegate);
}) : super(
key: key,
delegate: delegate,
);
@override
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
return _RenderSliverFloatingPersistentHeaderForWidgets(snapConfiguration: delegate.snapConfiguration);
return _RenderSliverFloatingPersistentHeaderForWidgets(
snapConfiguration: delegate.snapConfiguration,
stretchConfiguration: delegate.stretchConfiguration,
);
}
@override
void updateRenderObject(BuildContext context, _RenderSliverFloatingPersistentHeaderForWidgets renderObject) {
renderObject.snapConfiguration = delegate.snapConfiguration;
renderObject.stretchConfiguration = delegate.stretchConfiguration;
}
}
class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliverFloatingPinnedPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin {
class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliverFloatingPinnedPersistentHeader
with _RenderSliverPersistentHeaderForWidgetsMixin {
_RenderSliverFloatingPinnedPersistentHeaderForWidgets({
RenderBox child,
FloatingHeaderSnapConfiguration snapConfiguration,
}) : super(child: child, snapConfiguration: snapConfiguration);
OverScrollHeaderStretchConfiguration stretchConfiguration,
}) : super(
child: child,
snapConfiguration: snapConfiguration,
stretchConfiguration: stretchConfiguration,
);
}
class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
const _SliverFloatingPinnedPersistentHeader({
Key key,
@required SliverPersistentHeaderDelegate delegate,
}) : super(key: key, delegate: delegate);
}) : super(
key: key,
delegate: delegate,
);
@override
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
return _RenderSliverFloatingPinnedPersistentHeaderForWidgets(snapConfiguration: delegate.snapConfiguration);
return _RenderSliverFloatingPinnedPersistentHeaderForWidgets(
snapConfiguration: delegate.snapConfiguration,
stretchConfiguration: delegate.stretchConfiguration,
);
}
@override
void updateRenderObject(BuildContext context, _RenderSliverFloatingPinnedPersistentHeaderForWidgets renderObject) {
renderObject.snapConfiguration = delegate.snapConfiguration;
renderObject.stretchConfiguration = delegate.stretchConfiguration;
}
}
class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloatingPersistentHeader with _RenderSliverPersistentHeaderForWidgetsMixin {
class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloatingPersistentHeader
with _RenderSliverPersistentHeaderForWidgetsMixin {
_RenderSliverFloatingPersistentHeaderForWidgets({
RenderBox child,
FloatingHeaderSnapConfiguration snapConfiguration,
}) : super(child: child, snapConfiguration: snapConfiguration);
OverScrollHeaderStretchConfiguration stretchConfiguration,
}) : super(
child: child,
snapConfiguration: snapConfiguration,
stretchConfiguration: stretchConfiguration,
);
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
final Key blockKey = UniqueKey();
const double expandedAppbarHeight = 250.0;
final Key finderKey = UniqueKey();
void main() {
testWidgets('FlexibleSpaceBar stretch mode default zoomBackground', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
key: blockKey,
slivers: <Widget>[
SliverAppBar(
expandedHeight: expandedAppbarHeight,
pinned: true,
stretch: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
key: finderKey,
),
),
),
SliverToBoxAdapter(child: Container(height: 10000.0)),
],
),
),
),
);
// Scrolling up into the overscroll area causes the appBar to expand in size.
// This overscroll effect enlarges the background in step with the appbar.
final Finder appbarContainer = find.byKey(finderKey);
final Size sizeBeforeScroll = tester.getSize(appbarContainer);
await slowDrag(tester, blockKey, const Offset(0.0, 100.0));
final Size sizeAfterScroll = tester.getSize(appbarContainer);
expect(sizeBeforeScroll.height, lessThan(sizeAfterScroll.height));
});
testWidgets('FlexibleSpaceBar stretch mode blurBackground', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
key: blockKey,
slivers: <Widget>[
SliverAppBar(
expandedHeight: expandedAppbarHeight,
pinned: true,
stretch: true,
flexibleSpace: RepaintBoundary(
child: FlexibleSpaceBar(
stretchModes: const <StretchMode>[StretchMode.blurBackground],
background: Container(
child: Row(
children: <Widget>[
Expanded(child: Container(color: Colors.red)),
Expanded(child:Container(color: Colors.blue)),
],
)
),
),
),
),
SliverToBoxAdapter(child: Container(height: 10000.0)),
],
),
),
),
);
// Scrolling up into the overscroll area causes the background to blur.
await slowDrag(tester, blockKey, const Offset(0.0, 100.0));
await expectLater(
find.byType(FlexibleSpaceBar),
matchesGoldenFile('flexible_space_bar_stretch_mode.blur_background.png'),
);
}, skip: isBrowser);
testWidgets('FlexibleSpaceBar stretch mode fadeTitle', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
key: blockKey,
slivers: <Widget>[
SliverAppBar(
expandedHeight: expandedAppbarHeight,
pinned: true,
stretch: true,
flexibleSpace: FlexibleSpaceBar(
stretchModes: const <StretchMode>[StretchMode.fadeTitle],
title: Text(
'Title',
key: finderKey,
),
),
),
SliverToBoxAdapter(child: Container(height: 10000.0)),
],
),
),
),
);
await slowDrag(tester, blockKey, const Offset(0.0, 10.0));
Opacity opacityWidget = tester.widget<Opacity>(
find.ancestor(
of: find.text('Title'),
matching: find.byType(Opacity),
).first,
);
expect(opacityWidget.opacity.round(), equals(1));
await slowDrag(tester, blockKey, const Offset(0.0, 100.0));
opacityWidget = tester.widget<Opacity>(
find.ancestor(
of: find.text('Title'),
matching: find.byType(Opacity),
).first,
);
expect(opacityWidget.opacity, equals(0.0));
});
testWidgets('FlexibleSpaceBar stretch mode ignored for non-overscroll physics', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
physics: const ClampingScrollPhysics(),
key: blockKey,
slivers: <Widget>[
SliverAppBar(
expandedHeight: expandedAppbarHeight,
stretch: true,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
stretchModes: const <StretchMode>[StretchMode.blurBackground],
background: Container(
key: finderKey,
),
),
),
SliverToBoxAdapter(child: Container(height: 10000.0)),
],
),
),
),
);
final Finder appbarContainer = find.byKey(finderKey);
final Size sizeBeforeScroll = tester.getSize(appbarContainer);
await slowDrag(tester, blockKey, const Offset(0.0, 100.0));
final Size sizeAfterScroll = tester.getSize(appbarContainer);
expect(sizeBeforeScroll.height, equals(sizeAfterScroll.height));
});
}
Future<void> slowDrag(WidgetTester tester, Key widget, Offset offset) async {
final Offset target = tester.getCenter(find.byKey(widget));
final TestGesture gesture = await tester.startGesture(target);
await gesture.moveBy(offset);
await tester.pump(const Duration(milliseconds: 10));
await gesture.up();
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
group('SliverAppBar - Stretch', () {
testWidgets('fills overscroll', (WidgetTester tester) async {
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
final RenderSliverScrollingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar)
);
expect(header.child.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child.size.height, equals(200.0));
});
testWidgets('does not stretch without overscroll physics', (WidgetTester tester) async {
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
final RenderSliverScrollingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar)
);
expect(header.child.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100.0));
expect(header.child.size.height, equals(100.0));
});
testWidgets('default trigger offset', (WidgetTester tester) async {
bool didTrigger = false;
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: true,
expandedHeight: 100.0,
onStretchTrigger: () {
didTrigger = true;
return;
},
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
await slowDrag(tester, anchor, const Offset(0.0, 50.0));
expect(didTrigger, isFalse);
await tester.pumpAndSettle();
await slowDrag(tester, anchor, const Offset(0.0, 150.0));
expect(didTrigger, isTrue);
});
testWidgets('custom trigger offset', (WidgetTester tester) async {
bool didTrigger = false;
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: true,
expandedHeight: 100.0,
stretchTriggerOffset: 150.0,
onStretchTrigger: () {
didTrigger = true;
return;
},
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
await slowDrag(tester, anchor, const Offset(0.0, 100.0));
await tester.pumpAndSettle();
expect(didTrigger, isFalse);
await slowDrag(tester, anchor, const Offset(0.0, 300.0));
expect(didTrigger, isTrue);
});
testWidgets('stretch callback not triggered without overscroll physics', (WidgetTester tester) async {
bool didTrigger = false;
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: true,
expandedHeight: 100.0,
stretchTriggerOffset: 150.0,
onStretchTrigger: () {
didTrigger = true;
return;
},
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
await slowDrag(tester, anchor, const Offset(0.0, 100.0));
await tester.pumpAndSettle();
expect(didTrigger, isFalse);
await slowDrag(tester, anchor, const Offset(0.0, 300.0));
expect(didTrigger, isFalse);
});
testWidgets('asserts stretch != null', (WidgetTester tester) async {
expect(
() {
return MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: null,
expandedHeight: 100.0,
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
);
},
throwsAssertionError,
);
});
testWidgets('asserts reasonable trigger offset', (WidgetTester tester) async {
expect(
() {
return MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: true,
expandedHeight: 100.0,
stretchTriggerOffset: -150.0,
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
);
},
throwsAssertionError,
);
});
});
group('SliverAppBar - Stretch, Pinned', () {
testWidgets('fills overscroll', (WidgetTester tester) async {
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
pinned: true,
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
final RenderSliverPinnedPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar)
);
expect(header.child.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child.size.height, equals(200.0));
});
testWidgets('does not stretch without overscroll physics', (WidgetTester tester) async {
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
pinned: true,
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
final RenderSliverPinnedPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar)
);
expect(header.child.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child.size.height, equals(100.0));
});
});
group('SliverAppBar - Stretch, Floating', () {
testWidgets('fills overscroll', (WidgetTester tester) async {
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
floating: true,
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
final RenderSliverFloatingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar)
);
expect(header.child.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child.size.height, equals(200.0));
});
testWidgets('does not fill overscroll without proper physics', (WidgetTester tester) async {
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
floating: true,
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
final RenderSliverFloatingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar)
);
expect(header.child.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child.size.height, equals(100.0));
});
});
group('SliverAppBar - Stretch, Floating, Pinned', () {
testWidgets('fills overscroll', (WidgetTester tester) async {
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
floating: true,
pinned: true,
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
final RenderSliverFloatingPinnedPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar)
);
expect(header.child.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child.size.height, equals(200.0));
});
testWidgets('does not fill overscroll without proper physics', (WidgetTester tester) async {
const Key anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
pinned: true,
floating: true,
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(
child: Container(
key: anchor,
height: 800,
)
),
SliverToBoxAdapter(
child: Container(
height: 800,
)
),
],
),
),
);
final RenderSliverFloatingPinnedPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar)
);
expect(header.child.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child.size.height, equals(100.0));
});
});
}
Future<void> slowDrag(WidgetTester tester, Key widget, Offset offset) async {
final Offset target = tester.getCenter(find.byKey(widget));
final TestGesture gesture = await tester.startGesture(target);
await gesture.moveBy(offset);
await tester.pump(const Duration(milliseconds: 10));
await gesture.up();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment