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
28bb89c6
Commit
28bb89c6
authored
Apr 12, 2017
by
Hans Muller
Committed by
GitHub
Apr 12, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support for snapping floating app bars (#9156)
parent
bf017b79
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
571 additions
and
42 deletions
+571
-42
app_bar.dart
packages/flutter/lib/src/material/app_bar.dart
+138
-23
sliver_persistent_header.dart
...s/flutter/lib/src/rendering/sliver_persistent_header.dart
+111
-2
scroll_position.dart
packages/flutter/lib/src/widgets/scroll_position.dart
+8
-0
scrollable.dart
packages/flutter/lib/src/widgets/scrollable.dart
+29
-2
sliver_persistent_header.dart
...ges/flutter/lib/src/widgets/sliver_persistent_header.dart
+25
-2
app_bar_test.dart
packages/flutter/test/material/app_bar_test.dart
+175
-13
scrollable_of_test.dart
packages/flutter/test/widgets/scrollable_of_test.dart
+85
-0
No files found.
packages/flutter/lib/src/material/app_bar.dart
View file @
28bb89c6
...
...
@@ -5,6 +5,7 @@
import
'dart:math'
as
math
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
...
...
@@ -494,6 +495,58 @@ class _AppBarState extends State<AppBar> {
}
}
class
_FloatingAppBar
extends
StatefulWidget
{
_FloatingAppBar
({
Key
key
,
this
.
child
})
:
super
(
key:
key
);
final
Widget
child
;
@override
_FloatingAppBarState
createState
()
=>
new
_FloatingAppBarState
();
}
// A wrapper for the widget created by _SliverAppBarDelegate that starts and
/// stops the floating appbar's snap-into-view or snap-out-of-view animation.
class
_FloatingAppBarState
extends
State
<
_FloatingAppBar
>
{
ScrollPosition
_position
;
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
if
(
_position
!=
null
)
_position
.
isScrollingNotifier
.
removeListener
(
_isScrollingListener
);
_position
=
Scrollable
.
of
(
context
)?.
position
;
if
(
_position
!=
null
)
_position
.
isScrollingNotifier
.
addListener
(
_isScrollingListener
);
}
@override
void
dispose
()
{
if
(
_position
!=
null
)
_position
.
isScrollingNotifier
.
removeListener
(
_isScrollingListener
);
super
.
dispose
();
}
RenderSliverFloatingPersistentHeader
_headerRenderer
()
{
return
context
.
ancestorRenderObjectOfType
(
const
TypeMatcher
<
RenderSliverFloatingPersistentHeader
>());
}
void
_isScrollingListener
()
{
if
(
_position
==
null
)
return
;
// When a scroll stops, then maybe snap the appbar into view.
// Similarly, when a scroll starts, then maybe stop the snap animation.
final
RenderSliverFloatingPersistentHeader
header
=
_headerRenderer
();
if
(
_position
.
isScrollingNotifier
.
value
)
header
?.
maybeStopSnapAnimation
(
_position
.
userScrollDirection
);
else
header
?.
maybeStartSnapAnimation
(
_position
.
userScrollDirection
);
}
@override
Widget
build
(
BuildContext
context
)
=>
widget
.
child
;
}
class
_SliverAppBarDelegate
extends
SliverPersistentHeaderDelegate
{
_SliverAppBarDelegate
({
@required
this
.
leading
,
...
...
@@ -513,6 +566,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required
this
.
topPadding
,
@required
this
.
floating
,
@required
this
.
pinned
,
@required
this
.
snapConfiguration
,
})
:
_bottomHeight
=
bottom
?.
bottomHeight
??
0.0
{
assert
(
primary
||
topPadding
==
0.0
);
}
...
...
@@ -543,12 +597,15 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@override
double
get
maxExtent
=>
math
.
max
(
topPadding
+
(
expandedHeight
??
kToolbarHeight
+
_bottomHeight
),
minExtent
);
@override
final
FloatingHeaderSnapConfiguration
snapConfiguration
;
@override
Widget
build
(
BuildContext
context
,
double
shrinkOffset
,
bool
overlapsContent
)
{
final
double
visibleMainHeight
=
maxExtent
-
shrinkOffset
-
topPadding
;
final
double
toolbarOpacity
=
pinned
&&
!
floating
?
1.0
:
((
visibleMainHeight
-
_bottomHeight
)
/
kToolbarHeight
).
clamp
(
0.0
,
1.0
);
return
FlexibleSpaceBar
.
createSettings
(
final
Widget
appBar
=
FlexibleSpaceBar
.
createSettings
(
minExtent:
minExtent
,
maxExtent:
maxExtent
,
currentExtent:
math
.
max
(
minExtent
,
maxExtent
-
shrinkOffset
),
...
...
@@ -570,6 +627,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
bottomOpacity:
pinned
?
1.0
:
(
visibleMainHeight
/
_bottomHeight
).
clamp
(
0.0
,
1.0
),
),
);
return
floating
?
new
_FloatingAppBar
(
child:
appBar
)
:
appBar
;
}
@override
...
...
@@ -590,7 +648,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
||
expandedHeight
!=
oldDelegate
.
expandedHeight
||
topPadding
!=
oldDelegate
.
topPadding
||
pinned
!=
oldDelegate
.
pinned
||
floating
!=
oldDelegate
.
floating
;
||
floating
!=
oldDelegate
.
floating
||
snapConfiguration
!=
oldDelegate
.
snapConfiguration
;
}
@override
...
...
@@ -628,7 +687,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
/// * [FlexibleSpaceBar], which is used with [flexibleSpace] when the app bar
/// can expand and collapse.
/// * <https://material.google.com/layout/structure.html#structure-toolbars>
class
SliverAppBar
extends
State
less
Widget
{
class
SliverAppBar
extends
State
ful
Widget
{
/// Creates a material design app bar that can be placed in a [CustomScrollView].
SliverAppBar
({
Key
key
,
...
...
@@ -647,11 +706,13 @@ class SliverAppBar extends StatelessWidget {
this
.
expandedHeight
,
this
.
floating
:
false
,
this
.
pinned
:
false
,
this
.
snap
:
false
,
})
:
super
(
key:
key
)
{
assert
(
primary
!=
null
);
assert
(
floating
!=
null
);
assert
(
pinned
!=
null
);
assert
(
pinned
&&
floating
?
bottom
!=
null
:
true
);
assert
(
snap
!=
null
);
}
/// A widget to display before the [title].
...
...
@@ -776,6 +837,13 @@ class SliverAppBar extends StatelessWidget {
///
/// Otherwise, the user will need to scroll near the top of the scroll view to
/// reveal the app bar.
///
/// See also:
///
/// * If [snap] is true then a scroll that exposes the 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 it completely
/// out of view.
final
bool
floating
;
/// Whether the app bar should remain visible at the start of the scroll view.
...
...
@@ -784,33 +852,80 @@ class SliverAppBar extends StatelessWidget {
/// remain visible rather than being scrolled out of view.
final
bool
pinned
;
/// If [snap] and [floating] are true then the floating app bar will "snap"
/// into view.
///
/// If [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.
///
/// Snapping only applies when the app bar is floating, not when the appbar
/// appears at the top of its scroll view.
final
bool
snap
;
@override
_SliverAppBarState
createState
()
=>
new
_SliverAppBarState
();
}
// This class is only Stateful because it owns the TickerProvider used
// by the floating appbar snap animation (via FloatingHeaderSnapConfiguration).
class
_SliverAppBarState
extends
State
<
SliverAppBar
>
with
TickerProviderStateMixin
{
FloatingHeaderSnapConfiguration
_snapConfiguration
;
void
_updateSnapConfiguration
()
{
if
(
widget
.
snap
&&
widget
.
floating
)
{
_snapConfiguration
=
new
FloatingHeaderSnapConfiguration
(
vsync:
this
,
curve:
Curves
.
easeOut
,
duration:
const
Duration
(
milliseconds:
200
),
);
}
else
{
_snapConfiguration
=
null
;
}
}
@override
void
initState
()
{
super
.
initState
();
_updateSnapConfiguration
();
}
@override
void
didUpdateWidget
(
SliverAppBar
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
snap
!=
oldWidget
.
snap
||
widget
.
floating
!=
oldWidget
.
floating
)
_updateSnapConfiguration
();
}
@override
Widget
build
(
BuildContext
context
)
{
final
double
topPadding
=
primary
?
MediaQuery
.
of
(
context
).
padding
.
top
:
0.0
;
final
double
collapsedHeight
=
(
pinned
&&
floating
&&
bottom
!=
null
)
?
bottom
.
bottomHeight
+
topPadding
:
null
;
final
double
topPadding
=
widget
.
primary
?
MediaQuery
.
of
(
context
).
padding
.
top
:
0.0
;
final
double
collapsedHeight
=
(
widget
.
pinned
&&
widget
.
floating
&&
widget
.
bottom
!=
null
)
?
widget
.
bottom
.
bottomHeight
+
topPadding
:
null
;
return
new
SliverPersistentHeader
(
floating:
floating
,
pinned:
pinned
,
floating:
widget
.
floating
,
pinned:
widget
.
pinned
,
delegate:
new
_SliverAppBarDelegate
(
leading:
leading
,
title:
title
,
actions:
actions
,
flexibleSpace:
flexibleSpace
,
bottom:
bottom
,
elevation:
elevation
,
backgroundColor:
backgroundColor
,
brightness:
brightness
,
iconTheme:
iconTheme
,
textTheme:
textTheme
,
primary:
primary
,
centerTitle:
centerTitle
,
expandedHeight:
expandedHeight
,
leading:
widget
.
leading
,
title:
widget
.
title
,
actions:
widget
.
actions
,
flexibleSpace:
widget
.
flexibleSpace
,
bottom:
widget
.
bottom
,
elevation:
widget
.
elevation
,
backgroundColor:
widget
.
backgroundColor
,
brightness:
widget
.
brightness
,
iconTheme:
widget
.
iconTheme
,
textTheme:
widget
.
textTheme
,
primary:
widget
.
primary
,
centerTitle:
widget
.
centerTitle
,
expandedHeight:
widget
.
expandedHeight
,
collapsedHeight:
collapsedHeight
,
topPadding:
topPadding
,
floating:
floating
,
pinned:
pinned
,
floating:
widget
.
floating
,
pinned:
widget
.
pinned
,
snapConfiguration:
_snapConfiguration
,
),
);
}
...
...
packages/flutter/lib/src/rendering/sliver_persistent_header.dart
View file @
28bb89c6
...
...
@@ -4,8 +4,10 @@
import
'dart:math'
as
math
;
import
'package:flutter/animation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:vector_math/vector_math_64.dart'
;
import
'binding.dart'
;
...
...
@@ -251,11 +253,48 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
double
childMainAxisPosition
(
RenderBox
child
)
=>
0.0
;
}
/// Specifies how a floating header is to be "snapped" (animated) into or out
/// of view.
///
/// See also:
///
/// * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and
/// [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which
/// start or stop the floating header's animation.
/// * [SliverAppBar], which creates a header that can be pinned, floating,
/// and snapped into view via the corresponding parameters.
class
FloatingHeaderSnapConfiguration
{
/// Creates an object that specifies how a floating header is to be "snapped"
/// (animated) into or out of view.
FloatingHeaderSnapConfiguration
({
@required
this
.
vsync
,
this
.
curve
:
Curves
.
ease
,
this
.
duration
:
const
Duration
(
milliseconds:
300
),
})
{
assert
(
vsync
!=
null
);
assert
(
curve
!=
null
);
assert
(
duration
!=
null
);
}
/// The [TickerProvider] for the [AnimationController] that causes a
/// floating header to snap in or out of view.
final
TickerProvider
vsync
;
/// The snap animation curve.
final
Curve
curve
;
/// The snap animation's duration.
final
Duration
duration
;
}
abstract
class
RenderSliverFloatingPersistentHeader
extends
RenderSliverPersistentHeader
{
RenderSliverFloatingPersistentHeader
({
RenderBox
child
,
})
:
super
(
child:
child
);
FloatingHeaderSnapConfiguration
snapConfiguration
,
})
:
_snapConfiguration
=
snapConfiguration
,
super
(
child:
child
);
AnimationController
_controller
;
Animation
<
double
>
_animation
;
double
_lastActualScrollOffset
;
double
_effectiveScrollOffset
;
...
...
@@ -263,6 +302,39 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
// direction. Negative if we're scrolled off the top.
double
_childPosition
;
@override
void
detach
()
{
_controller
?.
dispose
();
_controller
=
null
;
// lazily recreated if we're reattached.
super
.
detach
();
}
/// Defines the parameters used to snap (animate) the floating header in and
/// out of view.
///
/// If [snapConfiguration] is null then the floating header does not snap.
///
/// See also:
///
/// * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and
/// [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which
/// start or stop the floating header's animation.
/// * [SliverAppBar], which creates a header that can be pinned, floating,
/// and snapped into view via the corresponding parameters.
FloatingHeaderSnapConfiguration
get
snapConfiguration
=>
_snapConfiguration
;
FloatingHeaderSnapConfiguration
_snapConfiguration
;
set
snapConfiguration
(
FloatingHeaderSnapConfiguration
value
)
{
if
(
value
==
_snapConfiguration
)
return
;
if
(
value
==
null
)
{
_controller
?.
dispose
();
}
else
{
if
(
_snapConfiguration
!=
null
&&
value
.
vsync
!=
_snapConfiguration
.
vsync
)
_controller
?.
resync
(
value
.
vsync
);
}
_snapConfiguration
=
value
;
}
// Update [geometry] and return the new value for [childMainAxisPosition].
@protected
double
updateGeometry
()
{
...
...
@@ -280,6 +352,42 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
return
math
.
min
(
0.0
,
paintExtent
-
childExtent
);
}
/// If the header isn't already fully exposed, then scroll it into view.
void
maybeStartSnapAnimation
(
ScrollDirection
direction
)
{
if
(
snapConfiguration
==
null
)
return
;
if
(
direction
==
ScrollDirection
.
forward
&&
_effectiveScrollOffset
<=
0.0
)
return
;
if
(
direction
==
ScrollDirection
.
reverse
&&
_effectiveScrollOffset
>=
maxExtent
)
return
;
final
TickerProvider
vsync
=
snapConfiguration
.
vsync
;
final
Duration
duration
=
snapConfiguration
.
duration
;
_controller
??=
new
AnimationController
(
vsync:
vsync
,
duration:
duration
)
..
addListener
(()
{
if
(
_effectiveScrollOffset
==
_animation
.
value
)
return
;
_effectiveScrollOffset
=
_animation
.
value
;
markNeedsLayout
();
});
// Recreating the animation rather than updating a cached value, only
// to avoid the extra complexity of managing the animation's lifetime.
_animation
=
new
Tween
<
double
>(
begin:
_effectiveScrollOffset
,
end:
direction
==
ScrollDirection
.
forward
?
0.0
:
maxExtent
,
).
animate
(
new
CurvedAnimation
(
parent:
_controller
,
curve:
snapConfiguration
.
curve
,
));
_controller
.
forward
(
from:
0.0
);
}
/// If a header snap animation is underway then stop it.
void
maybeStopSnapAnimation
(
ScrollDirection
direction
)
{
_controller
?.
stop
();
}
@override
void
performLayout
()
{
...
...
@@ -321,7 +429,8 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
abstract
class
RenderSliverFloatingPinnedPersistentHeader
extends
RenderSliverFloatingPersistentHeader
{
RenderSliverFloatingPinnedPersistentHeader
({
RenderBox
child
,
})
:
super
(
child:
child
);
FloatingHeaderSnapConfiguration
snapConfiguration
,
})
:
super
(
child:
child
,
snapConfiguration:
snapConfiguration
);
@override
double
updateGeometry
()
{
...
...
packages/flutter/lib/src/widgets/scroll_position.dart
View file @
28bb89c6
...
...
@@ -466,6 +466,13 @@ class ScrollPosition extends ViewportOffset {
ScrollActivity
get
activity
=>
_activity
;
ScrollActivity
_activity
;
/// This notifier's value is true if a scroll is underway and false if the scroll
/// position is idle.
///
/// Listeners added by stateful widgets should be in the widget's
/// [State.dispose] method.
final
ValueNotifier
<
bool
>
isScrollingNotifier
=
new
ValueNotifier
<
bool
>(
false
);
/// Change the current [activity], disposing of the old one and
/// sending scroll notifications as necessary.
///
...
...
@@ -490,6 +497,7 @@ class ScrollPosition extends ViewportOffset {
_activity
=
newActivity
;
if
(
oldIgnorePointer
!=
shouldIgnorePointer
)
state
.
setIgnorePointer
(
shouldIgnorePointer
);
isScrollingNotifier
.
value
=
_activity
?.
isScrolling
??
false
;
if
(!
activity
.
isScrolling
)
updateUserScrollDirection
(
ScrollDirection
.
idle
);
if
(!
wasScrolling
&&
activity
.
isScrolling
)
...
...
packages/flutter/lib/src/widgets/scrollable.dart
View file @
28bb89c6
...
...
@@ -65,7 +65,8 @@ class Scrollable extends StatefulWidget {
/// ScrollableState scrollable = Scrollable.of(context);
/// ```
static
ScrollableState
of
(
BuildContext
context
)
{
return
context
.
ancestorStateOfType
(
const
TypeMatcher
<
ScrollableState
>());
final
_ScrollableScope
widget
=
context
.
inheritFromWidgetOfExactType
(
_ScrollableScope
);
return
widget
?.
scrollable
;
}
/// Scrolls the closest enclosing scrollable to make the given context visible.
...
...
@@ -96,6 +97,28 @@ class Scrollable extends StatefulWidget {
}
}
// Enable Scrollable.of() to work as if ScrollableState was an inherited widget.
// ScrollableState.build() always rebuilds its _ScrollableScope.
class
_ScrollableScope
extends
InheritedWidget
{
_ScrollableScope
({
Key
key
,
@required
this
.
scrollable
,
@required
this
.
position
,
@required
Widget
child
})
:
super
(
key:
key
,
child:
child
)
{
assert
(
scrollable
!=
null
);
assert
(
child
!=
null
);
}
final
ScrollableState
scrollable
;
final
ScrollPosition
position
;
@override
bool
updateShouldNotify
(
_ScrollableScope
old
)
{
return
position
!=
old
.
position
;
}
}
/// State object for a [Scrollable] widget.
///
/// To manipulate a [Scrollable] widget's scroll position, use the object
...
...
@@ -312,7 +335,11 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
child:
new
IgnorePointer
(
key:
_ignorePointerKey
,
ignoring:
_shouldIgnorePointer
,
child:
widget
.
viewportBuilder
(
context
,
position
),
child:
new
_ScrollableScope
(
scrollable:
this
,
position:
position
,
child:
widget
.
viewportBuilder
(
context
,
position
),
),
),
);
return
_configuration
.
buildViewportChrome
(
context
,
result
,
widget
.
axisDirection
);
...
...
packages/flutter/lib/src/widgets/sliver_persistent_header.dart
View file @
28bb89c6
...
...
@@ -19,6 +19,13 @@ abstract class SliverPersistentHeaderDelegate {
double
get
maxExtent
;
bool
shouldRebuild
(
covariant
SliverPersistentHeaderDelegate
oldDelegate
);
/// Specifies how floating headers should animate in and out of view.
///
/// If the value of this property is null, then floating headers will
/// not animate into place.
@protected
FloatingHeaderSnapConfiguration
get
snapConfiguration
=>
null
;
}
class
SliverPersistentHeader
extends
StatelessWidget
{
...
...
@@ -224,7 +231,15 @@ class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjec
@override
_RenderSliverPersistentHeaderForWidgetsMixin
createRenderObject
(
BuildContext
context
)
{
return
new
_RenderSliverFloatingPersistentHeaderForWidgets
();
// Not passing this snapConfiguration as a constructor parameter to avoid the
// additional layers added due to https://github.com/dart-lang/sdk/issues/15101
return
new
_RenderSliverFloatingPersistentHeaderForWidgets
()
..
snapConfiguration
=
delegate
.
snapConfiguration
;
}
@override
void
updateRenderObject
(
BuildContext
context
,
_RenderSliverFloatingPersistentHeaderForWidgets
renderObject
)
{
renderObject
.
snapConfiguration
=
delegate
.
snapConfiguration
;
}
}
...
...
@@ -241,7 +256,15 @@ class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRende
@override
_RenderSliverPersistentHeaderForWidgetsMixin
createRenderObject
(
BuildContext
context
)
{
return
new
_RenderSliverFloatingPinnedPersistentHeaderForWidgets
();
// Not passing this snapConfiguration as a constructor parameter to avoid the
// additional layers added due to https://github.com/dart-lang/sdk/issues/15101
return
new
_RenderSliverFloatingPinnedPersistentHeaderForWidgets
()
..
snapConfiguration
=
delegate
.
snapConfiguration
;
}
@override
void
updateRenderObject
(
BuildContext
context
,
_RenderSliverFloatingPinnedPersistentHeaderForWidgets
renderObject
)
{
renderObject
.
snapConfiguration
=
delegate
.
snapConfiguration
;
}
}
...
...
packages/flutter/test/material/app_bar_test.dart
View file @
28bb89c6
...
...
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
Widget
buildSliverAppBarApp
(
{
bool
floating
,
bool
pinned
,
double
expandedHeight
})
{
Widget
buildSliverAppBarApp
(
{
bool
floating
,
bool
pinned
,
double
expandedHeight
,
bool
snap:
false
})
{
return
new
Scaffold
(
body:
new
DefaultTabController
(
length:
3
,
...
...
@@ -19,6 +19,7 @@ Widget buildSliverAppBarApp({ bool floating, bool pinned, double expandedHeight
floating:
floating
,
pinned:
pinned
,
expandedHeight:
expandedHeight
,
snap:
snap
,
bottom:
new
TabBar
(
tabs:
<
String
>[
'A'
,
'B'
,
'C'
].
map
((
String
t
)
=>
new
Tab
(
text:
'TAB
$t
'
)).
toList
(),
),
...
...
@@ -44,17 +45,11 @@ bool appBarIsVisible(WidgetTester tester) {
return
sliver
.
geometry
.
visible
;
}
double
appBarHeight
(
WidgetTester
tester
)
{
final
Element
element
=
tester
.
element
(
find
.
byType
(
AppBar
));
final
RenderBox
box
=
element
.
findRenderObject
();
return
box
.
size
.
height
;
}
double
appBarHeight
(
WidgetTester
tester
)
=>
tester
.
getSize
(
find
.
byType
(
AppBar
)).
height
;
double
appBarTop
(
WidgetTester
tester
)
=>
tester
.
getTopLeft
(
find
.
byType
(
AppBar
)).
y
;
double
appBarBottom
(
WidgetTester
tester
)
=>
tester
.
getBottomLeft
(
find
.
byType
(
AppBar
)).
y
;
double
tabBarHeight
(
WidgetTester
tester
)
{
final
Element
element
=
tester
.
element
(
find
.
byType
(
TabBar
));
final
RenderBox
box
=
element
.
findRenderObject
();
return
box
.
size
.
height
;
}
double
tabBarHeight
(
WidgetTester
tester
)
=>
tester
.
getSize
(
find
.
byType
(
TabBar
)).
height
;
void
main
(
)
{
testWidgets
(
'AppBar centers title on iOS'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -459,7 +454,7 @@ void main() {
final
double
initialAppBarHeight
=
128.0
;
final
double
initialTabBarHeight
=
tabBarHeight
(
tester
);
// Scroll the
not
-pinned appbar, collapsing the expanded height. At this
// Scroll the
floating
-pinned appbar, collapsing the expanded height. At this
// point only the tabBar is visible.
controller
.
jumpTo
(
600.0
);
await
tester
.
pump
();
...
...
@@ -468,11 +463,178 @@ void main() {
expect
(
appBarHeight
(
tester
),
lessThan
(
initialAppBarHeight
));
expect
(
appBarHeight
(
tester
),
initialTabBarHeight
);
// Scroll the
not
-pinned appbar back into view
// Scroll the
floating
-pinned appbar back into view
controller
.
jumpTo
(
0.0
);
await
tester
.
pump
();
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
appBarHeight
(
tester
),
initialAppBarHeight
);
expect
(
tabBarHeight
(
tester
),
initialTabBarHeight
);
});
testWidgets
(
'SliverAppBar expandedHeight, floating with snap:true'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
buildSliverAppBarApp
(
floating:
true
,
pinned:
false
,
snap:
true
,
expandedHeight:
128.0
,
));
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
appBarTop
(
tester
),
0.0
);
expect
(
appBarHeight
(
tester
),
128.0
);
expect
(
appBarBottom
(
tester
),
128.0
);
// Scroll to the middle of the list. The (floating) appbar is no longer visible.
final
ScrollPosition
position
=
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
;
position
.
jumpTo
(
256.00
);
await
tester
.
pumpAndSettle
();
expect
(
appBarIsVisible
(
tester
),
false
);
expect
(
appBarTop
(
tester
),
lessThanOrEqualTo
(-
128.0
));
// Drag the scrollable up and down. The app bar should not snap open, its
// height should just track the the drag offset.
TestGesture
gesture
=
await
tester
.
startGesture
(
const
Point
(
50.0
,
256.0
));
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
128.0
));
// drag the appbar all the way open
await
tester
.
pump
();
expect
(
appBarTop
(
tester
),
0.0
);
expect
(
appBarHeight
(
tester
),
128.0
);
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
50.0
));
await
tester
.
pump
();
expect
(
appBarBottom
(
tester
),
78.0
);
// 78 == 128 - 50
// Trigger the snap open animation: drag down and release
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
10.0
));
await
gesture
.
up
();
// Now verify that the appbar is animating open
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
double
bottom
=
appBarBottom
(
tester
);
expect
(
bottom
,
greaterThan
(
88.0
));
// 88 = 78 + 10
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
appBarBottom
(
tester
),
greaterThan
(
bottom
));
// The animation finishes when the appbar is full height.
await
tester
.
pumpAndSettle
();
expect
(
appBarHeight
(
tester
),
128.0
);
// Now that the app bar is open, perform the same drag scenario
// in reverse: drag the appbar up and down and then trigger the
// snap closed animation.
gesture
=
await
tester
.
startGesture
(
const
Point
(
50.0
,
256.0
));
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
128.0
));
// drag the appbar closed
await
tester
.
pump
();
expect
(
appBarBottom
(
tester
),
0.0
);
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
100.0
));
await
tester
.
pump
();
expect
(
appBarBottom
(
tester
),
100.0
);
// Trigger the snap close animation: drag upwards and release
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
10.0
));
await
gesture
.
up
();
// Now verify that the appbar is animating closed
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
bottom
=
appBarBottom
(
tester
);
expect
(
bottom
,
lessThan
(
90.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
appBarBottom
(
tester
),
lessThan
(
bottom
));
// The animation finishes when the appbar is off screen.
await
tester
.
pumpAndSettle
();
expect
(
appBarTop
(
tester
),
lessThanOrEqualTo
(
0.0
));
expect
(
appBarBottom
(
tester
),
lessThanOrEqualTo
(
0.0
));
});
testWidgets
(
'SliverAppBar expandedHeight, floating and pinned with snap:true'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
buildSliverAppBarApp
(
floating:
true
,
pinned:
true
,
snap:
true
,
expandedHeight:
128.0
,
));
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
appBarTop
(
tester
),
0.0
);
expect
(
appBarHeight
(
tester
),
128.0
);
expect
(
appBarBottom
(
tester
),
128.0
);
// Scroll to the middle of the list. The only the tab bar is visible
// because this is a pinned appbar.
final
ScrollPosition
position
=
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
;
position
.
jumpTo
(
256.0
);
await
tester
.
pumpAndSettle
();
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
appBarTop
(
tester
),
0.0
);
expect
(
appBarHeight
(
tester
),
kTextTabBarHeight
);
// Drag the scrollable up and down. The app bar should not snap open, the
// bottof of the appbar should just track the drag offset.
TestGesture
gesture
=
await
tester
.
startGesture
(
const
Point
(
50.0
,
200.0
));
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
100.0
));
await
tester
.
pump
();
expect
(
appBarHeight
(
tester
),
100.0
);
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
25.0
));
await
tester
.
pump
();
expect
(
appBarHeight
(
tester
),
75.0
);
// Trigger the snap animation: drag down and release
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
10.0
));
await
gesture
.
up
();
// Now verify that the appbar is animating open
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
final
double
height
=
appBarHeight
(
tester
);
expect
(
height
,
greaterThan
(
85.0
));
expect
(
height
,
lessThan
(
128.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
appBarHeight
(
tester
),
greaterThan
(
height
));
expect
(
appBarHeight
(
tester
),
lessThan
(
128.0
));
// The animation finishes when the appbar is fully expanded
await
tester
.
pumpAndSettle
();
expect
(
appBarTop
(
tester
),
0.0
);
expect
(
appBarHeight
(
tester
),
128.0
);
expect
(
appBarBottom
(
tester
),
128.0
);
// Now that the appbar is fully expanded, Perform the same drag
// scenario in reverse: drag the appbar up and down and then trigger
// the snap closed animation.
gesture
=
await
tester
.
startGesture
(
const
Point
(
50.0
,
256.0
));
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
128.0
));
await
tester
.
pump
();
expect
(
appBarBottom
(
tester
),
kTextTabBarHeight
);
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
100.0
));
await
tester
.
pump
();
expect
(
appBarBottom
(
tester
),
100.0
);
// Trigger the snap close animation: drag upwards and release
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
10.0
));
await
gesture
.
up
();
// Now verify that the appbar is animating closed
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
final
double
bottom
=
appBarBottom
(
tester
);
expect
(
bottom
,
lessThan
(
90.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
appBarBottom
(
tester
),
lessThan
(
bottom
));
// The animation finishes when the appbar shrinks back to its pinned height
await
tester
.
pumpAndSettle
();
expect
(
appBarTop
(
tester
),
lessThanOrEqualTo
(
0.0
));
expect
(
appBarBottom
(
tester
),
kTextTabBarHeight
);
});
}
packages/flutter/test/widgets/scrollable_of_test.dart
0 → 100644
View file @
28bb89c6
// Copyright 2016 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_test/flutter_test.dart'
;
import
'package:flutter/widgets.dart'
;
class
ScrollPositionListener
extends
StatefulWidget
{
ScrollPositionListener
({
Key
key
,
this
.
child
,
this
.
log
})
:
super
(
key:
key
);
final
Widget
child
;
final
ValueChanged
<
String
>
log
;
@override
_ScrollPositionListenerState
createState
()
=>
new
_ScrollPositionListenerState
();
}
class
_ScrollPositionListenerState
extends
State
<
ScrollPositionListener
>
{
ScrollPosition
_position
;
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
_position
?.
removeListener
(
listener
);
_position
=
Scrollable
.
of
(
context
)?.
position
;
_position
?.
addListener
(
listener
);
widget
.
log
(
"didChangeDependencies
${_position?.pixels}
"
);
}
@override
void
dispose
()
{
_position
?.
removeListener
(
listener
);
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
=>
widget
.
child
;
void
listener
()
{
widget
.
log
(
"listener
${_position?.pixels}
"
);
}
}
void
main
(
)
{
testWidgets
(
'Scrollable.of() dependent rebuilds when Scrollable position changes'
,
(
WidgetTester
tester
)
async
{
String
logValue
;
final
ScrollController
controller
=
new
ScrollController
();
// Changing the SingleChildScrollView's physics causes the
// ScrollController's ScrollPosition to be rebuilt.
Widget
buildFrame
(
ScrollPhysics
physics
)
{
return
new
SingleChildScrollView
(
controller:
controller
,
physics:
physics
,
child:
new
ScrollPositionListener
(
log:
(
String
s
)
{
logValue
=
s
;
},
child:
const
SizedBox
(
height:
400.0
),
),
);
}
await
tester
.
pumpWidget
(
buildFrame
(
null
));
expect
(
logValue
,
"didChangeDependencies 0.0"
);
controller
.
jumpTo
(
100.0
);
expect
(
logValue
,
"listener 100.0"
);
await
tester
.
pumpWidget
(
buildFrame
(
const
ClampingScrollPhysics
()));
expect
(
logValue
,
"didChangeDependencies 100.0"
);
controller
.
jumpTo
(
200.0
);
expect
(
logValue
,
"listener 200.0"
);
controller
.
jumpTo
(
300.0
);
expect
(
logValue
,
"listener 300.0"
);
await
tester
.
pumpWidget
(
buildFrame
(
const
BouncingScrollPhysics
()));
expect
(
logValue
,
"didChangeDependencies 300.0"
);
controller
.
jumpTo
(
400.0
);
expect
(
logValue
,
"listener 400.0"
);
});
}
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