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,
),
),
);
......
......@@ -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();
}
This diff is collapsed.
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