Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
64f42c0e
Unverified
Commit
64f42c0e
authored
Jun 12, 2020
by
Kate Lovett
Committed by
GitHub
Jun 12, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support floating the header slivers of a NestedScrollView (#59187)
parent
e96b13c7
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
923 additions
and
119 deletions
+923
-119
sliver_persistent_header.dart
...s/flutter/lib/src/rendering/sliver_persistent_header.dart
+1
-1
nested_scroll_view.dart
packages/flutter/lib/src/widgets/nested_scroll_view.dart
+326
-118
sliver_persistent_header_test.dart
...flutter/test/rendering/sliver_persistent_header_test.dart
+30
-0
nested_scroll_view_test.dart
packages/flutter/test/widgets/nested_scroll_view_test.dart
+566
-0
No files found.
packages/flutter/lib/src/rendering/sliver_persistent_header.dart
View file @
64f42c0e
...
@@ -644,7 +644,7 @@ abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFl
...
@@ -644,7 +644,7 @@ abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFl
paintExtent:
clampedPaintExtent
,
paintExtent:
clampedPaintExtent
,
layoutExtent:
layoutExtent
.
clamp
(
0.0
,
clampedPaintExtent
)
as
double
,
layoutExtent:
layoutExtent
.
clamp
(
0.0
,
clampedPaintExtent
)
as
double
,
maxPaintExtent:
maxExtent
+
stretchOffset
,
maxPaintExtent:
maxExtent
+
stretchOffset
,
maxScrollObstructionExtent:
m
ax
Extent
,
maxScrollObstructionExtent:
m
in
Extent
,
hasVisualOverflow:
true
,
// Conservatively say we do have overflow to avoid complexity.
hasVisualOverflow:
true
,
// Conservatively say we do have overflow to avoid complexity.
);
);
return
0.0
;
return
0.0
;
...
...
packages/flutter/lib/src/widgets/nested_scroll_view.dart
View file @
64f42c0e
...
@@ -64,7 +64,7 @@ typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContex
...
@@ -64,7 +64,7 @@ typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContex
/// (those inside the [TabBarView], hooking them together so that they appear,
/// (those inside the [TabBarView], hooking them together so that they appear,
/// to the user, as one coherent scroll view.
/// to the user, as one coherent scroll view.
///
///
/// {@tool s
nippet
}
/// {@tool s
ample --template=stateless_widget_scaffold
}
///
///
/// This example shows a [NestedScrollView] whose header is the combination of a
/// This example shows a [NestedScrollView] whose header is the combination of a
/// [TabBar] in a [SliverAppBar] and whose body is a [TabBarView]. It uses a
/// [TabBar] in a [SliverAppBar] and whose body is a [TabBarView]. It uses a
...
@@ -74,113 +74,292 @@ typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContex
...
@@ -74,113 +74,292 @@ typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContex
/// [PageStorageKey]s are used to remember the scroll position of each tab's
/// [PageStorageKey]s are used to remember the scroll position of each tab's
/// list.
/// list.
///
///
/// In the example below, `_tabs` is a list of strings, one for each tab, giving
/// the tab labels. In a real application, it would be replaced by the actual
/// data model being represented.
///
/// ```dart
/// ```dart
/// DefaultTabController(
/// Widget build(BuildContext context) {
/// length: _tabs.length, // This is the number of tabs.
/// final List<String> _tabs = ['Tab 1', 'Tab 2'];
/// child: NestedScrollView(
/// return DefaultTabController(
/// headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
/// length: _tabs.length, // This is the number of tabs.
/// // These are the slivers that show up in the "outer" scroll view.
/// child: NestedScrollView(
/// return <Widget>[
/// headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
/// SliverOverlapAbsorber(
/// // These are the slivers that show up in the "outer" scroll view.
/// // This widget takes the overlapping behavior of the SliverAppBar,
/// return <Widget>[
/// // and redirects it to the SliverOverlapInjector below. If it is
/// SliverOverlapAbsorber(
/// // missing, then it is possible for the nested "inner" scroll view
/// // This widget takes the overlapping behavior of the SliverAppBar,
/// // below to end up under the SliverAppBar even when the inner
/// // and redirects it to the SliverOverlapInjector below. If it is
/// // scroll view thinks it has not been scrolled.
/// // missing, then it is possible for the nested "inner" scroll view
/// // This is not necessary if the "headerSliverBuilder" only builds
/// // below to end up under the SliverAppBar even when the inner
/// // widgets that do not overlap the next sliver.
/// // scroll view thinks it has not been scrolled.
/// handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
/// // This is not necessary if the "headerSliverBuilder" only builds
/// sliver: SliverAppBar(
/// // widgets that do not overlap the next sliver.
/// title: const Text('Books'), // This is the title in the app bar.
/// handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
/// pinned: true,
/// sliver: SliverAppBar(
/// expandedHeight: 150.0,
/// title: const Text('Books'), // This is the title in the app bar.
/// // The "forceElevated" property causes the SliverAppBar to show
/// pinned: true,
/// // a shadow. The "innerBoxIsScrolled" parameter is true when the
/// expandedHeight: 150.0,
/// // inner scroll view is scrolled beyond its "zero" point, i.e.
/// // The "forceElevated" property causes the SliverAppBar to show
/// // when it appears to be scrolled below the SliverAppBar.
/// // a shadow. The "innerBoxIsScrolled" parameter is true when the
/// // Without this, there are cases where the shadow would appear
/// // inner scroll view is scrolled beyond its "zero" point, i.e.
/// // or not appear inappropriately, because the SliverAppBar is
/// // when it appears to be scrolled below the SliverAppBar.
/// // not actually aware of the precise position of the inner
/// // Without this, there are cases where the shadow would appear
/// // scroll views.
/// // or not appear inappropriately, because the SliverAppBar is
/// forceElevated: innerBoxIsScrolled,
/// // not actually aware of the precise position of the inner
/// bottom: TabBar(
/// // scroll views.
/// // These are the widgets to put in each tab in the tab bar.
/// forceElevated: innerBoxIsScrolled,
/// tabs: _tabs.map((String name) => Tab(text: name)).toList(),
/// bottom: TabBar(
/// // These are the widgets to put in each tab in the tab bar.
/// tabs: _tabs.map((String name) => Tab(text: name)).toList(),
/// ),
/// ),
/// ),
/// ),
/// ),
///
),
///
];
///
];
///
},
///
},
///
body: TabBarView(
///
body: TabBarView(
///
// These are the contents of the tab views, below the tabs.
///
// These are the contents of the tab views, below the tabs.
///
children: _tabs.map((String name) {
///
children: _tabs.map((String name) {
///
return SafeArea(
///
return SafeArea(
///
top: false,
///
top
: false,
///
bottom
: false,
///
bottom: false,
///
child: Builder(
///
child: Builder(
///
// This Builder is needed to provide a BuildContext that is
///
// This Builder is needed to provide a BuildContext that is
///
// "inside" the NestedScrollView, so that
///
// "inside" the NestedScrollView, so that
///
// sliverOverlapAbsorberHandleFor() can find the
///
// sliverOverlapAbsorberHandleFor() can find the
///
// NestedScrollView.
///
// NestedScrollView.
///
builder: (BuildContext context) {
///
builder: (BuildContext context) {
///
return CustomScrollView(
///
return CustomScrollView(
///
// The "controller" and "primary" members should be left
///
// The "controller" and "primary" members should be left
///
// unset, so that the NestedScrollView can control this
///
// unset, so that the NestedScrollView can control this
///
// inner scroll view.
///
// inner scroll view.
///
// If the "controller" property is set, then this scroll
///
// If the "controller" property is set, then this scroll
///
// view will not be associated with the NestedScrollView.
///
// view will not be associated with the NestedScrollView.
///
// The PageStorageKey should be unique to this ScrollView;
///
// The PageStorageKey should be unique to this ScrollView;
///
// it allows the list to remember its scroll position when
///
// it allows the list to remember its scroll position when
///
// the tab view is not on the screen.
///
// the tab view is not on the screen.
///
key: PageStorageKey<String>(name),
///
key: PageStorageKey<String>(name),
///
slivers: <Widget>[
///
slivers: <Widget>[
///
SliverOverlapInjector(
///
SliverOverlapInjector(
///
// This is the flip side of the SliverOverlapAbsorber
///
// This is the flip side of the SliverOverlapAbsorber
///
// above.
///
// above.
///
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
///
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context
),
/// ),
///
),
///
SliverPadding(
///
SliverPadding(
///
padding: const EdgeInsets.all(8.0),
///
padding: const EdgeInsets.all(8.0),
///
// In this example, the inner scroll view has
///
// In this example, the inner scroll view has
///
// fixed-height list items, hence the use of
///
// fixed-height list items, hence the use of
///
// SliverFixedExtentList. However, one could use any
///
// SliverFixedExtentList. However, one could use any
///
// sliver widget here, e.g. SliverList or SliverGrid.
///
// sliver widget here, e.g. SliverList or SliverGrid.
///
sliver: SliverFixedExtentList(
///
sliver: SliverFixedExtentList(
///
// The items in this example are fixed to 48 pixels
///
// The items in this example are fixed to 48 pixels
///
// high. This matches the Material Design spec for
///
// high. This matches the Material Design spec for
///
// ListTile widgets.
///
// ListTile widgets.
///
itemExtent: 48.0,
///
itemExtent: 48.0,
///
delegate: SliverChildBuilderDelegate(
///
delegate: SliverChildBuilderDelegate(
///
(BuildContext context, int index) {
///
(BuildContext context, int index) {
///
// This builder is called for each child.
///
// This builder is called for each child
.
///
// In this example, we just number each list item
.
///
// In this example, we just number each list item.
///
return ListTile(
///
return ListTile(
///
title: Text('Item $index'),
///
title: Text('Item $index'),
///
);
///
);
///
},
///
},
///
// The childCount of the SliverChildBuilderDelegate
///
// The childCount of the SliverChildBuilderDelegate
///
// specifies how many children this inner list
///
// specifies how many children this inner list
///
// has. In this example, each tab has a list of
///
// has. In this example, each tab has a list of
///
// exactly 30 items, but this is arbitrary.
///
// exactly 30 items, but this is arbitrary.
///
childCount: 30,
///
childCount: 30
,
///
)
,
/// ),
/// ),
/// ),
/// ),
///
)
,
///
]
,
///
],
///
);
///
);
///
},
///
}
,
///
)
,
/// )
,
/// )
;
///
);
///
}).toList(),
///
}).toList(
),
/// ),
/// ),
/// ),
/// ),
/// );
/// )
/// }
/// ```
/// {@end-tool}
///
/// ## [SliverAppBar]s with [NestedScrollView]s
///
/// Using a [SliverAppBar] in the outer scroll view, or [headerSliverBuilder],
/// of a [NestedScrollView] may require special configurations in order to work
/// as it would if the outer and inner were one single scroll view, like a
/// [CustomScrollView].
///
/// ### Pinned [SliverAppBar]s
///
/// A pinned [SliverAppBar] works in a [NestedScrollView] exactly as it would in
/// another scroll view, like [CustomScrollView]. When using
/// [SliverAppBar.pinned], the app bar remains visible at the top of the scroll
/// view. The app bar can still expand and contract as the user scrolls, but it
/// will remain visible rather than being scrolled out of view.
///
/// This works naturally in a [NestedScroll] view, as the pinned [SliverAppBar]
/// is not expected to move in or out of the visible portion of the viewport.
/// As the inner or outer [Scrollable]s are moved, the app bar persists as
/// expected.
///
/// If the app bar is floating, pinned, and using an expanded height, follow the
/// floating convention laid out below.
///
/// ### Floating [SliverAppBar]s
///
/// When placed in the outer scrollable, or the [headerSliverBuilder],
/// a [SliverAppBar] that floats, using [SliverAppBar.floating] will not be
/// triggered to float over the inner scroll view, or [body], automatically.
///
/// This is because a floating app bar uses the scroll offset of its own
/// [Scrollable] to dictate the floating action. Being two separate inner and
/// outer [Scrollable]s, a [SliverAppBar] in the outer header is not aware of
/// changes in the scroll offset of the inner body.
///
/// In order to float the outer, use [NestedScrollView.floatHeaderSlivers]. When
/// set to true, the nested scrolling coordinator will prioritize floating in
/// the header slivers before applying the remaining drag to the body.
///
/// Furthermore, the `floatHeaderSlivers` flag should also be used when using an
/// app bar that is floating, pinned, and has an expanded height. In this
/// configuration, the flexible space of the app bar will open and collapse,
/// while the primary portion of the app bar remains pinned.
///
/// {@tool sample --template=stateless_widget_material}
///
/// This simple example shows a [NestedScrollView] whose header contains a
/// floating [SliverAppBar]. By using the [floatHeaderSlivers] property, the
/// floating behavior is coordinated between the outer and inner [Scrollable]s,
/// so it behaves as it would in a single scrollable.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: NestedScrollView(
/// // Setting floatHeaderSlivers to true is required in order to float
/// // the outer slivers over the inner scrollable.
/// floatHeaderSlivers: true,
/// headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
/// return <Widget>[
/// SliverAppBar(
/// title: const Text('Floating Nested SliverAppBar'),
/// floating: true,
/// expandedHeight: 200.0,
/// forceElevated: innerBoxIsScrolled,
/// ),
/// ];
/// },
/// body: ListView.builder(
/// padding: const EdgeInsets.all(8),
/// itemCount: 30,
/// itemBuilder: (BuildContext context, int index) {
/// return Container(
/// height: 50,
/// child: Center(child: Text('Item $index')),
/// );
/// }
/// )
/// )
/// );
/// }
/// ```
/// ```
/// {@end-tool}
/// {@end-tool}
///
/// ### Snapping [SliverAppBar]s
///
/// Floating [SliverAppBars] also have the option to perform a snapping animation.
/// If [SliverAppBar.snap] is true, then a scroll that exposes the floating app
/// bar will trigger an animation that slides the entire app bar into view.
/// Similarly if a scroll dismisses the app bar, the animation will slide the
/// app bar completely out of view.
///
/// It is possible with a [NestedScrollView] to perform just the snapping
/// animation without floating the app bar in and out. By not using the
/// [NestedScrollView.floatHeaderSlivers], the app bar will snap in and out
/// without floating.
///
/// The [SliverAppBar.snap] animation should be used in conjunction with the
/// [SliverOverlapAbsorber] and [SliverOverlapInjector] widgets when
/// implemented in a [NestedScrollView]. These widgets take any overlapping
/// behavior of the [SliverAppBar] in the header and redirect it to the
/// [SliverOverlapInjector] in the body. If it is missing, then it is possible
/// for the nested "inner" scroll view below to end up under the [SliverAppBar]
/// even when the inner scroll view thinks it has not been scrolled.
///
/// {@tool sample --template=stateless_widget_material}
///
/// This simple example shows a [NestedScrollView] whose header contains a
/// snapping, floating [SliverAppBar]. _Without_ setting any additional flags,
/// e.g [NestedScrollView.floatHeaderSlivers] and [SliverAppBar.nestedSnap], the
/// [SliverAppBar] will animate in and out without floating. The
/// [SliverOverlapAbsorber] and [SliverOverlapInjector] maintain the proper
/// alignment between the two separate scroll views.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: NestedScrollView(
/// headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
/// return <Widget>[
/// SliverOverlapAbsorber(
/// handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
/// sliver: SliverAppBar(
/// title: const Text('Snapping Nested SliverAppBar'),
/// floating: true,
/// snap: true,
/// expandedHeight: 200.0,
/// forceElevated: innerBoxIsScrolled,
/// ),
/// )
/// ];
/// },
/// body: Builder(
/// builder: (BuildContext context) {
/// return CustomScrollView(
/// // The "controller" and "primary" members should be left
/// // unset, so that the NestedScrollView can control this
/// // inner scroll view.
/// // If the "controller" property is set, then this scroll
/// // view will not be associated with the NestedScrollView.
/// slivers: <Widget>[
/// SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
/// SliverFixedExtentList(
/// itemExtent: 48.0,
/// delegate: SliverChildBuilderDelegate(
/// (BuildContext context, int index) => ListTile(title: Text('Item $index')),
/// childCount: 30,
/// ),
/// ),
/// ],
/// );
/// }
/// )
/// )
/// );
/// }
/// ```
/// {@end-tool}
///
/// ### Snapping and Floating [SliverAppBar]s
///
// See https://github.com/flutter/flutter/issues/59189
/// Currently, [NestedScrollView] does not support simultaneously floating and
/// snapping the outer scrollable, e.g. when using [SliverAppBar.floating] &
/// [SliverAppBar.snap] at the same time.
///
/// ### Stretching [SliverAppBar]s
///
// TODO(Piinks): Support stretching, https://github.com/flutter/flutter/issues/54059
/// Currently, [NestedScrollView] does not support stretching the outer
/// scrollable, e.g. when using [SliverAppBar.stretch].
///
/// See also:
///
/// * [SliverAppBar], for examples on different configurations like floating,
/// pinned and snap behaviors.
/// * [SliverOverlapAbsorber], a sliver that wraps another, forcing its layout
/// extent to be treated as overlap.
/// * [SliverOverlapInjector], a sliver that has a sliver geometry based on
/// the values stored in a [SliverOverlapAbsorberHandle].
class
NestedScrollView
extends
StatefulWidget
{
class
NestedScrollView
extends
StatefulWidget
{
/// Creates a nested scroll view.
/// Creates a nested scroll view.
///
///
...
@@ -195,10 +374,12 @@ class NestedScrollView extends StatefulWidget {
...
@@ -195,10 +374,12 @@ class NestedScrollView extends StatefulWidget {
@required
this
.
headerSliverBuilder
,
@required
this
.
headerSliverBuilder
,
@required
this
.
body
,
@required
this
.
body
,
this
.
dragStartBehavior
=
DragStartBehavior
.
start
,
this
.
dragStartBehavior
=
DragStartBehavior
.
start
,
this
.
floatHeaderSlivers
=
false
,
})
:
assert
(
scrollDirection
!=
null
),
})
:
assert
(
scrollDirection
!=
null
),
assert
(
reverse
!=
null
),
assert
(
reverse
!=
null
),
assert
(
headerSliverBuilder
!=
null
),
assert
(
headerSliverBuilder
!=
null
),
assert
(
body
!=
null
),
assert
(
body
!=
null
),
assert
(
floatHeaderSlivers
!=
null
),
super
(
key:
key
);
super
(
key:
key
);
/// An object that can be used to control the position to which the outer
/// An object that can be used to control the position to which the outer
...
@@ -262,6 +443,13 @@ class NestedScrollView extends StatefulWidget {
...
@@ -262,6 +443,13 @@ class NestedScrollView extends StatefulWidget {
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final
DragStartBehavior
dragStartBehavior
;
final
DragStartBehavior
dragStartBehavior
;
/// Whether or not the [NestedScrollView]'s coordinator should prioritize the
/// outer scrollable over the inner when scrolling back.
///
/// This is useful for an outer scrollable containing a [SliverAppBar] that
/// is expected to float. This cannot be null.
final
bool
floatHeaderSlivers
;
/// Returns the [SliverOverlapAbsorberHandle] of the nearest ancestor
/// Returns the [SliverOverlapAbsorberHandle] of the nearest ancestor
/// [NestedScrollView].
/// [NestedScrollView].
///
///
...
@@ -378,6 +566,7 @@ class NestedScrollViewState extends State<NestedScrollView> {
...
@@ -378,6 +566,7 @@ class NestedScrollViewState extends State<NestedScrollView> {
_coordinator
=
_NestedScrollCoordinator
(
_coordinator
=
_NestedScrollCoordinator
(
this
,
widget
.
controller
,
this
,
widget
.
controller
,
_handleHasScrolledBodyChanged
,
_handleHasScrolledBodyChanged
,
widget
.
floatHeaderSlivers
,
);
);
}
}
...
@@ -549,7 +738,12 @@ class _NestedScrollMetrics extends FixedScrollMetrics {
...
@@ -549,7 +738,12 @@ class _NestedScrollMetrics extends FixedScrollMetrics {
typedef
_NestedScrollActivityGetter
=
ScrollActivity
Function
(
_NestedScrollPosition
position
);
typedef
_NestedScrollActivityGetter
=
ScrollActivity
Function
(
_NestedScrollPosition
position
);
class
_NestedScrollCoordinator
implements
ScrollActivityDelegate
,
ScrollHoldController
{
class
_NestedScrollCoordinator
implements
ScrollActivityDelegate
,
ScrollHoldController
{
_NestedScrollCoordinator
(
this
.
_state
,
this
.
_parent
,
this
.
_onHasScrolledBodyChanged
)
{
_NestedScrollCoordinator
(
this
.
_state
,
this
.
_parent
,
this
.
_onHasScrolledBodyChanged
,
this
.
_floatHeaderSlivers
,
)
{
final
double
initialScrollOffset
=
_parent
?.
initialScrollOffset
??
0.0
;
final
double
initialScrollOffset
=
_parent
?.
initialScrollOffset
??
0.0
;
_outerController
=
_NestedScrollController
(
_outerController
=
_NestedScrollController
(
this
,
this
,
...
@@ -566,6 +760,7 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
...
@@ -566,6 +760,7 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
final
NestedScrollViewState
_state
;
final
NestedScrollViewState
_state
;
ScrollController
_parent
;
ScrollController
_parent
;
final
VoidCallback
_onHasScrolledBodyChanged
;
final
VoidCallback
_onHasScrolledBodyChanged
;
final
bool
_floatHeaderSlivers
;
_NestedScrollController
_outerController
;
_NestedScrollController
_outerController
;
_NestedScrollController
_innerController
;
_NestedScrollController
_innerController
;
...
@@ -935,23 +1130,36 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
...
@@ -935,23 +1130,36 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
}
}
}
else
{
}
else
{
// Dragging "down" - delta is positive
// Dragging "down" - delta is positive
// Prioritize the inner views, so that the inner content will move before
double
innerDelta
=
delta
;
// the app bar grows
// Apply delta to the outer header first if it is configured to float.
double
outerDelta
=
0.0
;
// it will go positive if it changes
if
(
_floatHeaderSlivers
)
final
List
<
double
>
overscrolls
=
<
double
>[];
innerDelta
=
_outerPosition
.
applyClampedDragUpdate
(
delta
);
final
List
<
_NestedScrollPosition
>
innerPositions
=
_innerPositions
.
toList
();
for
(
final
_NestedScrollPosition
position
in
innerPositions
)
{
if
(
innerDelta
!=
0.0
)
{
final
double
overscroll
=
position
.
applyClampedDragUpdate
(
delta
);
// Apply the innerDelta, if we have not floated in the outer scrollable,
outerDelta
=
math
.
max
(
outerDelta
,
overscroll
);
// any leftover delta after this will be passed on to the outer
overscrolls
.
add
(
overscroll
);
// scrollable by the outerDelta.
}
double
outerDelta
=
0.0
;
// it will go positive if it changes
if
(
outerDelta
!=
0.0
)
final
List
<
double
>
overscrolls
=
<
double
>[];
outerDelta
-=
_outerPosition
.
applyClampedDragUpdate
(
outerDelta
);
final
List
<
_NestedScrollPosition
>
innerPositions
=
_innerPositions
.
toList
();
// now deal with any overscroll
for
(
final
_NestedScrollPosition
position
in
innerPositions
)
{
for
(
int
i
=
0
;
i
<
innerPositions
.
length
;
++
i
)
{
final
double
overscroll
=
position
.
applyClampedDragUpdate
(
innerDelta
);
final
double
remainingDelta
=
overscrolls
[
i
]
-
outerDelta
;
outerDelta
=
math
.
max
(
outerDelta
,
overscroll
);
if
(
remainingDelta
>
0.0
)
overscrolls
.
add
(
overscroll
);
innerPositions
[
i
].
applyFullDragUpdate
(
remainingDelta
);
}
if
(
outerDelta
!=
0.0
)
outerDelta
-=
_outerPosition
.
applyClampedDragUpdate
(
outerDelta
);
// Now deal with any overscroll
// TODO(Piinks): Configure which scrollable receives overscroll to
// support stretching app bars. createOuterBallisticScrollActivity will
// need to be updated as it currently assumes the outer position will
// never overscroll, https://github.com/flutter/flutter/issues/54059
for
(
int
i
=
0
;
i
<
innerPositions
.
length
;
++
i
)
{
final
double
remainingDelta
=
overscrolls
[
i
]
-
outerDelta
;
if
(
remainingDelta
>
0.0
)
innerPositions
[
i
].
applyFullDragUpdate
(
remainingDelta
);
}
}
}
}
}
}
}
...
...
packages/flutter/test/rendering/sliver_persistent_header_test.dart
View file @
64f42c0e
...
@@ -26,6 +26,24 @@ void main() {
...
@@ -26,6 +26,24 @@ void main() {
expect
(
header
.
geometry
.
maxScrollObstructionExtent
,
0
);
expect
(
header
.
geometry
.
maxScrollObstructionExtent
,
0
);
});
});
test
(
'RenderSliverFloatingPinnedPersistentHeader maxScrollObstructionExtent is minExtent'
,
()
{
final
TestRenderSliverFloatingPinnedPersistentHeader
header
=
TestRenderSliverFloatingPinnedPersistentHeader
(
child:
RenderSizedBox
(
const
Size
(
400.0
,
100.0
)
));
final
RenderViewport
root
=
RenderViewport
(
axisDirection:
AxisDirection
.
down
,
crossAxisDirection:
AxisDirection
.
right
,
offset:
ViewportOffset
.
zero
(),
cacheExtent:
0
,
children:
<
RenderSliver
>[
header
,
],
);
layout
(
root
);
expect
(
header
.
geometry
.
maxScrollObstructionExtent
,
100.0
);
});
}
}
class
TestRenderSliverFloatingPersistentHeader
extends
RenderSliverFloatingPersistentHeader
{
class
TestRenderSliverFloatingPersistentHeader
extends
RenderSliverFloatingPersistentHeader
{
...
@@ -39,3 +57,15 @@ class TestRenderSliverFloatingPersistentHeader extends RenderSliverFloatingPersi
...
@@ -39,3 +57,15 @@ class TestRenderSliverFloatingPersistentHeader extends RenderSliverFloatingPersi
@override
@override
double
get
minExtent
=>
100
;
double
get
minExtent
=>
100
;
}
}
class
TestRenderSliverFloatingPinnedPersistentHeader
extends
RenderSliverFloatingPinnedPersistentHeader
{
TestRenderSliverFloatingPinnedPersistentHeader
({
RenderBox
child
,
})
:
super
(
child:
child
);
@override
double
get
maxExtent
=>
200
;
@override
double
get
minExtent
=>
100
;
}
packages/flutter/test/widgets/nested_scroll_view_test.dart
View file @
64f42c0e
...
@@ -1200,6 +1200,572 @@ void main() {
...
@@ -1200,6 +1200,572 @@ void main() {
await
tester
.
pumpWidget
(
const
_TestLayoutExtentIsNegative
(
1
));
await
tester
.
pumpWidget
(
const
_TestLayoutExtentIsNegative
(
1
));
await
tester
.
pumpWidget
(
const
_TestLayoutExtentIsNegative
(
10
));
await
tester
.
pumpWidget
(
const
_TestLayoutExtentIsNegative
(
10
));
});
});
group
(
'NestedScrollView can float outer sliver with inner scroll view:'
,
()
{
Widget
buildFloatTest
({
GlobalKey
appBarKey
,
GlobalKey
nestedKey
,
ScrollController
controller
,
bool
floating
=
false
,
bool
pinned
=
false
,
bool
snap
=
false
,
bool
nestedFloat
=
false
,
bool
expanded
=
false
,
})
{
return
MaterialApp
(
home:
Scaffold
(
body:
NestedScrollView
(
key:
nestedKey
,
controller:
controller
,
floatHeaderSlivers:
nestedFloat
,
headerSliverBuilder:
(
BuildContext
context
,
bool
innerBoxIsScrolled
)
{
return
<
Widget
>[
SliverOverlapAbsorber
(
handle:
NestedScrollView
.
sliverOverlapAbsorberHandleFor
(
context
),
sliver:
SliverAppBar
(
key:
appBarKey
,
title:
const
Text
(
'Test Title'
),
floating:
floating
,
pinned:
pinned
,
snap:
snap
,
expandedHeight:
expanded
?
200.0
:
0.0
,
),
),
];
},
body:
Builder
(
builder:
(
BuildContext
context
)
{
return
CustomScrollView
(
slivers:
<
Widget
>[
SliverOverlapInjector
(
handle:
NestedScrollView
.
sliverOverlapAbsorberHandleFor
(
context
)),
SliverFixedExtentList
(
itemExtent:
50.0
,
delegate:
SliverChildBuilderDelegate
(
(
BuildContext
context
,
int
index
)
=>
ListTile
(
title:
Text
(
'Item
$index
'
)),
childCount:
30
,
)
),
],
);
}
)
)
)
);
}
double
verifyGeometry
({
GlobalKey
key
,
double
paintExtent
,
bool
extentGreaterThan
=
false
,
bool
extentLessThan
=
false
,
bool
visible
,
})
{
final
RenderSliver
target
=
key
.
currentContext
.
findRenderObject
()
as
RenderSliver
;
final
SliverGeometry
geometry
=
target
.
geometry
;
expect
(
target
.
parent
,
isA
<
RenderSliverOverlapAbsorber
>());
expect
(
geometry
.
visible
,
visible
);
if
(
extentGreaterThan
)
expect
(
geometry
.
paintExtent
,
greaterThan
(
paintExtent
));
else
if
(
extentLessThan
)
expect
(
geometry
.
paintExtent
,
lessThan
(
paintExtent
));
else
expect
(
geometry
.
paintExtent
,
paintExtent
);
return
geometry
.
paintExtent
;
}
testWidgets
(
'float'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
appBarKey
=
GlobalKey
();
await
tester
.
pumpWidget
(
buildFloatTest
(
floating:
true
,
nestedFloat:
true
,
appBarKey:
appBarKey
,
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
56.0
,
visible:
true
);
// Scroll away the outer scroll view and some of the inner scroll view.
// We will not scroll back the same amount to indicate that we are
// floating in before reaching the top of the inner scrollable.
final
Offset
point1
=
tester
.
getCenter
(
find
.
text
(
'Item 5'
));
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
-
300.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
// The outer scrollable should float back in, inner should not change
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
50.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
50.0
,
visible:
true
);
// Float the rest of the way in.
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
150.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
56.0
,
visible:
true
);
});
testWidgets
(
'float expanded'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
appBarKey
=
GlobalKey
();
await
tester
.
pumpWidget
(
buildFloatTest
(
floating:
true
,
nestedFloat:
true
,
expanded:
true
,
appBarKey:
appBarKey
,
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
200.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
200.0
,
visible:
true
);
// Scroll away the outer scroll view and some of the inner scroll view.
// We will not scroll back the same amount to indicate that we are
// floating in before reaching the top of the inner scrollable.
final
Offset
point1
=
tester
.
getCenter
(
find
.
text
(
'Item 5'
));
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
-
300.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
// The outer scrollable should float back in, inner should not change
// On initial float in, the app bar is collapsed.
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
50.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
50.0
,
visible:
true
);
// The inner scrollable should receive leftover delta after the outer has
// been scrolled back in fully.
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
200.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
200.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
200.0
,
visible:
true
);
});
testWidgets
(
'only snap'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
appBarKey
=
GlobalKey
();
final
GlobalKey
<
NestedScrollViewState
>
nestedKey
=
GlobalKey
();
await
tester
.
pumpWidget
(
buildFloatTest
(
floating:
true
,
snap:
true
,
appBarKey:
appBarKey
,
nestedKey:
nestedKey
,
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
56.0
,
visible:
true
);
// Scroll down the list, the app bar should scroll away and no longer be
// visible.
final
Offset
point1
=
tester
.
getCenter
(
find
.
text
(
'Item 5'
));
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
-
300.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
// The outer scroll view should be at its full extent, here the size of
// the app bar.
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
56.0
);
// Animate In
// Drag the scrollable up and down. The app bar should not snap open, nor
// should it float in.
final
TestGesture
animateInGesture
=
await
tester
.
startGesture
(
point1
);
await
animateInGesture
.
moveBy
(
const
Offset
(
0.0
,
100.0
));
// Should not float in
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
56.0
);
await
animateInGesture
.
moveBy
(
const
Offset
(
0.0
,
-
50.0
));
// No float out
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
56.0
);
// Trigger the snap open animation: drag down and release
await
animateInGesture
.
moveBy
(
const
Offset
(
0.0
,
10.0
));
await
animateInGesture
.
up
();
// Now verify that the appbar is animating open
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
double
lastExtent
=
verifyGeometry
(
key:
appBarKey
,
paintExtent:
10.0
,
// >10.0 since 0.0 + 10.0
extentGreaterThan:
true
,
visible:
true
,
);
// The outer scroll offset should remain unchanged.
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
56.0
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
lastExtent
,
extentGreaterThan:
true
,
visible:
true
,
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
56.0
);
// The animation finishes when the appbar is full height.
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
56.0
,
visible:
true
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
56.0
);
// Animate Out
// Trigger the snap close animation: drag up and release
final
TestGesture
animateOutGesture
=
await
tester
.
startGesture
(
point1
);
await
animateOutGesture
.
moveBy
(
const
Offset
(
0.0
,
-
10.0
));
await
animateOutGesture
.
up
();
// Now verify that the appbar is animating closed
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
lastExtent
=
verifyGeometry
(
key:
appBarKey
,
paintExtent:
46.0
,
// <46.0 since 56.0 - 10.0
extentLessThan:
true
,
visible:
true
,
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
56.0
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
lastExtent
,
extentLessThan:
true
,
visible:
true
,
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
56.0
);
// The animation finishes when the appbar is no longer in view.
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
56.0
);
});
testWidgets
(
'only snap expanded'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
appBarKey
=
GlobalKey
();
final
GlobalKey
<
NestedScrollViewState
>
nestedKey
=
GlobalKey
();
await
tester
.
pumpWidget
(
buildFloatTest
(
floating:
true
,
snap:
true
,
expanded:
true
,
appBarKey:
appBarKey
,
nestedKey:
nestedKey
,
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
200.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
200.0
,
visible:
true
);
// Scroll down the list, the app bar should scroll away and no longer be
// visible.
final
Offset
point1
=
tester
.
getCenter
(
find
.
text
(
'Item 5'
));
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
-
400.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
// The outer scroll view should be at its full extent, here the size of
// the app bar.
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
200.0
);
// Animate In
// Drag the scrollable up and down. The app bar should not snap open, nor
// should it float in.
final
TestGesture
animateInGesture
=
await
tester
.
startGesture
(
point1
);
await
animateInGesture
.
moveBy
(
const
Offset
(
0.0
,
100.0
));
// Should not float in
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
200.0
);
await
animateInGesture
.
moveBy
(
const
Offset
(
0.0
,
-
50.0
));
// No float out
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
200.0
);
// Trigger the snap open animation: drag down and release
await
animateInGesture
.
moveBy
(
const
Offset
(
0.0
,
10.0
));
await
animateInGesture
.
up
();
// Now verify that the appbar is animating open
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
double
lastExtent
=
verifyGeometry
(
key:
appBarKey
,
paintExtent:
10.0
,
// >10.0 since 0.0 + 10.0
extentGreaterThan:
true
,
visible:
true
,
);
// The outer scroll offset should remain unchanged.
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
200.0
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
lastExtent
,
extentGreaterThan:
true
,
visible:
true
,
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
200.0
);
// The animation finishes when the appbar is full height.
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
200.0
,
visible:
true
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
200.0
);
// Animate Out
// Trigger the snap close animation: drag up and release
final
TestGesture
animateOutGesture
=
await
tester
.
startGesture
(
point1
);
await
animateOutGesture
.
moveBy
(
const
Offset
(
0.0
,
-
10.0
));
await
animateOutGesture
.
up
();
// Now verify that the appbar is animating closed
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
lastExtent
=
verifyGeometry
(
key:
appBarKey
,
paintExtent:
190.0
,
// <190.0 since 200.0 - 10.0
extentLessThan:
true
,
visible:
true
,
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
200.0
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
lastExtent
,
extentLessThan:
true
,
visible:
true
,
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
200.0
);
// The animation finishes when the appbar is no longer in view.
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Test Title'
),
findsNothing
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
0.0
,
visible:
false
);
expect
(
nestedKey
.
currentState
.
outerController
.
offset
,
200.0
);
});
testWidgets
(
'float pinned'
,
(
WidgetTester
tester
)
async
{
// This configuration should have the same behavior of a pinned app bar.
// No floating should happen, and the app bar should persist.
final
GlobalKey
appBarKey
=
GlobalKey
();
await
tester
.
pumpWidget
(
buildFloatTest
(
floating:
true
,
pinned:
true
,
nestedFloat:
true
,
appBarKey:
appBarKey
,
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
56.0
,
visible:
true
);
// Scroll away the outer scroll view and some of the inner scroll view.
final
Offset
point1
=
tester
.
getCenter
(
find
.
text
(
'Item 5'
));
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
-
300.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
56.0
,
visible:
true
);
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
50.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
56.0
,
visible:
true
);
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
150.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
56.0
,
visible:
true
);
});
testWidgets
(
'float pinned expanded'
,
(
WidgetTester
tester
)
async
{
// Only the expanded portion (flexible space) of the app bar should float
// in and out.
final
GlobalKey
appBarKey
=
GlobalKey
();
await
tester
.
pumpWidget
(
buildFloatTest
(
floating:
true
,
pinned:
true
,
expanded:
true
,
nestedFloat:
true
,
appBarKey:
appBarKey
,
));
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
200.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
200.0
,
visible:
true
);
// Scroll away the outer scroll view and some of the inner scroll view.
// The expanded portion of the app bar should collapse.
final
Offset
point1
=
tester
.
getCenter
(
find
.
text
(
'Item 5'
));
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
-
300.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
56.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
56.0
,
visible:
true
);
// Scroll back some, the app bar should expand.
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
50.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
106.0
,
// 56.0 + 50.0
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
106.0
,
visible:
true
);
// Finish scrolling the rest of the way in.
await
tester
.
dragFrom
(
point1
,
const
Offset
(
0.0
,
150.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Test Title'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 5'
),
findsOneWidget
);
expect
(
tester
.
renderObject
<
RenderBox
>(
find
.
byType
(
AppBar
)).
size
.
height
,
200.0
,
);
verifyGeometry
(
key:
appBarKey
,
paintExtent:
200.0
,
visible:
true
);
});
});
}
}
class
TestHeader
extends
SliverPersistentHeaderDelegate
{
class
TestHeader
extends
SliverPersistentHeaderDelegate
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment