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
3a0b83b1
Commit
3a0b83b1
authored
Feb 23, 2017
by
Hans Muller
Committed by
GitHub
Feb 23, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added support for a pinned floating SliverAppBar (#8345)
parent
9ada90a1
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
272 additions
and
28 deletions
+272
-28
app_bar.dart
packages/flutter/lib/src/material/app_bar.dart
+61
-17
shifted_box.dart
packages/flutter/lib/src/rendering/shifted_box.dart
+4
-0
sliver_persistent_header.dart
...s/flutter/lib/src/rendering/sliver_persistent_header.dart
+40
-10
sliver_persistent_header.dart
...ges/flutter/lib/src/widgets/sliver_persistent_header.dart
+19
-1
app_bar_test.dart
packages/flutter/test/material/app_bar_test.dart
+148
-0
No files found.
packages/flutter/lib/src/material/app_bar.dart
View file @
3a0b83b1
...
...
@@ -96,6 +96,29 @@ class _ToolbarLayout extends MultiChildLayoutDelegate {
bool
shouldRelayout
(
_ToolbarLayout
oldDelegate
)
=>
centerTitle
!=
oldDelegate
.
centerTitle
;
}
// Bottom justify the kToolbarHeight child which may overflow the top.
class
_ToolbarContainerLayout
extends
SingleChildLayoutDelegate
{
const
_ToolbarContainerLayout
();
@override
BoxConstraints
getConstraintsForChild
(
BoxConstraints
constraints
)
{
return
constraints
.
tighten
(
height:
kToolbarHeight
);
}
@override
Size
getSize
(
BoxConstraints
constraints
)
{
return
new
Size
(
constraints
.
maxWidth
,
kToolbarHeight
);
}
@override
Offset
getPositionForChild
(
Size
size
,
Size
childSize
)
{
return
new
Offset
(
0.0
,
size
.
height
-
childSize
.
height
);
}
@override
bool
shouldRelayout
(
_ToolbarContainerLayout
oldDelegate
)
=>
false
;
}
// TODO(eseidel) Toolbar needs to change size based on orientation:
// http://material.google.com/layout/structure.html#structure-app-bar
// Mobile Landscape: 48dp
...
...
@@ -425,24 +448,32 @@ class _AppBarState extends State<AppBar> {
),
);
Widget
appBar
=
new
SizedBox
(
height:
kToolbarHeight
,
child:
new
IconTheme
.
merge
(
context:
context
,
data:
appBarIconTheme
,
child:
new
DefaultTextStyle
(
style:
sideStyle
,
child:
toolbar
,
// If the toolbar is allocated less than kToolbarHeight make it
// appear to scroll upwards within its shrinking container.
Widget
appBar
=
new
ClipRect
(
child:
new
CustomSingleChildLayout
(
delegate:
const
_ToolbarContainerLayout
(),
child:
new
IconTheme
.
merge
(
context:
context
,
data:
appBarIconTheme
,
child:
new
DefaultTextStyle
(
style:
sideStyle
,
child:
toolbar
,
),
),
),
);
if
(
config
.
bottom
!=
null
)
{
appBar
=
new
Column
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
<
Widget
>[
appBar
,
new
Flexible
(
child:
new
ConstrainedBox
(
constraints:
new
BoxConstraints
(
maxHeight:
kToolbarHeight
),
child:
appBar
,
),
),
config
.
bottomOpacity
==
1.0
?
config
.
bottom
:
new
Opacity
(
opacity:
const
Interval
(
0.25
,
1.0
,
curve:
Curves
.
fastOutSlowIn
).
transform
(
config
.
bottomOpacity
),
child:
config
.
bottom
,
...
...
@@ -494,7 +525,9 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required
this
.
primary
,
@required
this
.
centerTitle
,
@required
this
.
expandedHeight
,
@required
this
.
collapsedHeight
,
@required
this
.
topPadding
,
@required
this
.
floating
,
@required
this
.
pinned
,
})
:
bottom
=
bottom
,
_bottomHeight
=
bottom
?.
bottomHeight
??
0.0
{
...
...
@@ -514,21 +547,24 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final
bool
primary
;
final
bool
centerTitle
;
final
double
expandedHeight
;
final
double
collapsedHeight
;
final
double
topPadding
;
final
bool
floating
;
final
bool
pinned
;
final
double
_bottomHeight
;
@override
double
get
minExtent
=>
topPadding
+
kToolbarHeight
+
_bottomHeight
;
double
get
minExtent
=>
collapsedHeight
??
(
topPadding
+
kToolbarHeight
+
_bottomHeight
)
;
@override
double
get
maxExtent
=>
math
.
max
(
topPadding
+
(
expandedHeight
??
kToolbarHeight
),
minExtent
);
double
get
maxExtent
=>
math
.
max
(
topPadding
+
(
expandedHeight
??
kToolbarHeight
+
_bottomHeight
),
minExtent
);
@override
Widget
build
(
BuildContext
context
,
double
shrinkOffset
,
bool
overlapsContent
)
{
double
visibleMainHeight
=
maxExtent
-
shrinkOffset
-
topPadding
;
double
toolbarOpacity
=
pinned
?
1.0
:
((
visibleMainHeight
-
_bottomHeight
)
/
kToolbarHeight
).
clamp
(
0.0
,
1.0
);
final
double
visibleMainHeight
=
maxExtent
-
shrinkOffset
-
topPadding
;
final
double
toolbarOpacity
=
pinned
&&
!
floating
?
1.0
:
((
visibleMainHeight
-
_bottomHeight
)
/
kToolbarHeight
).
clamp
(
0.0
,
1.0
);
return
FlexibleSpaceBar
.
createSettings
(
minExtent:
minExtent
,
maxExtent:
maxExtent
,
...
...
@@ -569,7 +605,9 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
||
primary
!=
oldDelegate
.
primary
||
centerTitle
!=
oldDelegate
.
centerTitle
||
expandedHeight
!=
oldDelegate
.
expandedHeight
||
topPadding
!=
oldDelegate
.
topPadding
;
||
topPadding
!=
oldDelegate
.
topPadding
||
pinned
!=
oldDelegate
.
pinned
||
floating
!=
oldDelegate
.
floating
;
}
@override
...
...
@@ -630,7 +668,7 @@ class SliverAppBar extends StatelessWidget {
assert
(
primary
!=
null
);
assert
(
floating
!=
null
);
assert
(
pinned
!=
null
);
assert
(
!
floating
||
!
pinned
);
assert
(
pinned
&&
floating
?
bottom
!=
null
:
true
);
}
/// A widget to display before the [title].
...
...
@@ -763,6 +801,10 @@ class SliverAppBar extends StatelessWidget {
@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
;
return
new
SliverPersistentHeader
(
floating:
floating
,
pinned:
pinned
,
...
...
@@ -780,7 +822,9 @@ class SliverAppBar extends StatelessWidget {
primary:
primary
,
centerTitle:
centerTitle
,
expandedHeight:
expandedHeight
,
topPadding:
primary
?
MediaQuery
.
of
(
context
).
padding
.
top
:
0.0
,
collapsedHeight:
collapsedHeight
,
topPadding:
topPadding
,
floating:
floating
,
pinned:
pinned
,
),
);
...
...
packages/flutter/lib/src/rendering/shifted_box.dart
View file @
3a0b83b1
...
...
@@ -700,6 +700,10 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
/// A delegate for computing the layout of a render object with a single child.
abstract
class
SingleChildLayoutDelegate
{
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const
SingleChildLayoutDelegate
();
// TODO(abarth): This class should take a Listenable to drive relayout.
/// The size of this object given the incoming constraints.
...
...
packages/flutter/lib/src/rendering/sliver_persistent_header.dart
View file @
3a0b83b1
...
...
@@ -265,6 +265,23 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
// direction. Negative if we're scrolled off the top.
double
_childPosition
;
// Update [geometry] and return the new value for [childMainAxisPosition].
@protected
double
updateGeometry
()
{
final
double
maxExtent
=
this
.
maxExtent
;
final
double
paintExtent
=
maxExtent
-
_effectiveScrollOffset
;
final
double
layoutExtent
=
maxExtent
-
constraints
.
scrollOffset
;
geometry
=
new
SliverGeometry
(
scrollExtent:
maxExtent
,
paintExtent:
paintExtent
.
clamp
(
0.0
,
constraints
.
remainingPaintExtent
),
layoutExtent:
layoutExtent
.
clamp
(
0.0
,
constraints
.
remainingPaintExtent
),
maxPaintExtent:
maxExtent
,
hasVisualOverflow:
true
,
// Conservatively say we do have overflow to avoid complexity.
);
return
math
.
min
(
0.0
,
paintExtent
-
childExtent
);
}
@override
void
performLayout
()
{
final
double
maxExtent
=
this
.
maxExtent
;
...
...
@@ -285,16 +302,7 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
_effectiveScrollOffset
=
constraints
.
scrollOffset
;
}
layoutChild
(
_effectiveScrollOffset
,
maxExtent
,
overlapsContent:
_effectiveScrollOffset
<
constraints
.
scrollOffset
);
final
double
paintExtent
=
maxExtent
-
_effectiveScrollOffset
;
final
double
layoutExtent
=
(
maxExtent
-
constraints
.
scrollOffset
).
clamp
(
0.0
,
constraints
.
remainingPaintExtent
);
geometry
=
new
SliverGeometry
(
scrollExtent:
maxExtent
,
paintExtent:
paintExtent
.
clamp
(
0.0
,
constraints
.
remainingPaintExtent
),
layoutExtent:
layoutExtent
,
maxPaintExtent:
maxExtent
,
hasVisualOverflow:
true
,
// Conservatively say we do have overflow to avoid complexity.
);
_childPosition
=
math
.
min
(
0.0
,
paintExtent
-
childExtent
);
_childPosition
=
updateGeometry
();
_lastActualScrollOffset
=
constraints
.
scrollOffset
;
}
...
...
@@ -310,3 +318,25 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
description
.
add
(
'effective scroll offset:
${_effectiveScrollOffset?.toStringAsFixed(1)}
'
);
}
}
abstract
class
RenderSliverFloatingPinnedPersistentHeader
extends
RenderSliverFloatingPersistentHeader
{
RenderSliverFloatingPinnedPersistentHeader
({
RenderBox
child
,
})
:
super
(
child:
child
);
@override
double
updateGeometry
()
{
final
double
minExtent
=
this
.
maxExtent
;
final
double
maxExtent
=
this
.
maxExtent
;
final
double
paintExtent
=
(
maxExtent
-
_effectiveScrollOffset
);
final
double
layoutExtent
=
(
maxExtent
-
constraints
.
scrollOffset
);
geometry
=
new
SliverGeometry
(
scrollExtent:
maxExtent
,
paintExtent:
paintExtent
.
clamp
(
minExtent
,
constraints
.
remainingPaintExtent
),
layoutExtent:
layoutExtent
.
clamp
(
0.0
,
constraints
.
remainingPaintExtent
-
minExtent
),
maxPaintExtent:
maxExtent
,
hasVisualOverflow:
true
,
// Conservatively say we do have overflow to avoid complexity.
);
return
0.0
;
}
}
packages/flutter/lib/src/widgets/sliver_persistent_header.dart
View file @
3a0b83b1
...
...
@@ -31,7 +31,6 @@ class SliverPersistentHeader extends StatelessWidget {
assert
(
delegate
!=
null
);
assert
(
pinned
!=
null
);
assert
(
floating
!=
null
);
assert
(!
pinned
||
!
floating
);
}
final
SliverPersistentHeaderDelegate
delegate
;
...
...
@@ -42,6 +41,8 @@ class SliverPersistentHeader extends StatelessWidget {
@override
Widget
build
(
BuildContext
context
)
{
if
(
floating
&&
pinned
)
return
new
_SliverFloatingPinnedPersistentHeader
(
delegate:
delegate
);
if
(
pinned
)
return
new
_SliverPinnedPersistentHeader
(
delegate:
delegate
);
if
(
floating
)
...
...
@@ -227,6 +228,23 @@ class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjec
}
}
// This class exists to work around https://github.com/dart-lang/sdk/issues/15101
abstract
class
_RenderSliverFloatingPinnedPersistentHeader
extends
RenderSliverFloatingPinnedPersistentHeader
{
}
class
_RenderSliverFloatingPinnedPersistentHeaderForWidgets
extends
_RenderSliverFloatingPinnedPersistentHeader
with
_RenderSliverPersistentHeaderForWidgetsMixin
{
}
class
_SliverFloatingPinnedPersistentHeader
extends
_SliverPersistentHeaderRenderObjectWidget
{
_SliverFloatingPinnedPersistentHeader
({
Key
key
,
@required
SliverPersistentHeaderDelegate
delegate
,
})
:
super
(
key:
key
,
delegate:
delegate
);
@override
_RenderSliverPersistentHeaderForWidgetsMixin
createRenderObject
(
BuildContext
context
)
{
return
new
_RenderSliverFloatingPinnedPersistentHeaderForWidgets
();
}
}
// This class exists to work around https://github.com/dart-lang/sdk/issues/15101
abstract
class
_RenderSliverFloatingPersistentHeader
extends
RenderSliverFloatingPersistentHeader
{
}
...
...
packages/flutter/test/material/app_bar_test.dart
View file @
3a0b83b1
...
...
@@ -4,8 +4,55 @@
import
'package:flutter/foundation.dart'
;
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
})
{
return
new
Scaffold
(
body:
new
CustomScrollView
(
primary:
true
,
slivers:
<
Widget
>[
new
SliverAppBar
(
title:
new
Text
(
'AppBar Title'
),
floating:
floating
,
pinned:
pinned
,
expandedHeight:
expandedHeight
,
bottom:
new
TabBar
(
tabs:
<
String
>[
'A'
,
'B'
,
'C'
].
map
((
String
t
)
=>
new
Tab
(
text:
'TAB
$t
'
)).
toList
(),
),
),
new
SliverToBoxAdapter
(
child:
new
Container
(
height:
1200.0
,
decoration:
new
BoxDecoration
(
backgroundColor:
Colors
.
orange
[
400
]),
),
),
],
),
);
}
ScrollController
primaryScrollController
(
WidgetTester
tester
)
{
return
PrimaryScrollController
.
of
(
tester
.
element
(
find
.
byType
(
CustomScrollView
)));
}
bool
appBarIsVisible
(
WidgetTester
tester
)
{
final
RenderSliver
sliver
=
tester
.
element
(
find
.
byType
(
SliverAppBar
)).
findRenderObject
();
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
tabBarHeight
(
WidgetTester
tester
)
{
final
Element
element
=
tester
.
element
(
find
.
byType
(
TabBar
));
final
RenderBox
box
=
element
.
findRenderObject
();
return
box
.
size
.
height
;
}
void
main
(
)
{
testWidgets
(
'AppBar centers title on iOS'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
...
...
@@ -305,4 +352,105 @@ void main() {
expect
(
tester
.
getSize
(
shareButton
),
new
Size
(
48.0
,
56.0
));
});
testWidgets
(
'SliverAppBar default configuration'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
buildSliverAppBarApp
(
floating:
false
,
pinned:
false
,
expandedHeight:
null
,
));
ScrollController
controller
=
primaryScrollController
(
tester
);
expect
(
controller
.
offset
,
0.0
);
expect
(
appBarIsVisible
(
tester
),
true
);
final
double
initialAppBarHeight
=
appBarHeight
(
tester
);
final
double
initialTabBarHeight
=
tabBarHeight
(
tester
);
// Scroll the not-pinned appbar partially out of view
controller
.
jumpTo
(
50.0
);
await
tester
.
pump
();
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
appBarHeight
(
tester
),
initialAppBarHeight
);
expect
(
tabBarHeight
(
tester
),
initialTabBarHeight
);
// Scroll the not-pinned appbar out of view
controller
.
jumpTo
(
600.0
);
await
tester
.
pump
();
expect
(
appBarIsVisible
(
tester
),
false
);
expect
(
appBarHeight
(
tester
),
initialAppBarHeight
);
expect
(
tabBarHeight
(
tester
),
initialTabBarHeight
);
// Scroll the not-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, pinned'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
buildSliverAppBarApp
(
floating:
false
,
pinned:
true
,
expandedHeight:
128.0
,
));
ScrollController
controller
=
primaryScrollController
(
tester
);
expect
(
controller
.
offset
,
0.0
);
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
appBarHeight
(
tester
),
128.0
);
final
double
initialAppBarHeight
=
128.0
;
final
double
initialTabBarHeight
=
tabBarHeight
(
tester
);
// Scroll the not-pinned appbar, collapsing the expanded height. At this
// point both the toolbar and the tabbar are visible.
controller
.
jumpTo
(
600.0
);
await
tester
.
pump
();
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
tabBarHeight
(
tester
),
initialTabBarHeight
);
expect
(
appBarHeight
(
tester
),
lessThan
(
initialAppBarHeight
));
expect
(
appBarHeight
(
tester
),
greaterThan
(
initialTabBarHeight
));
// Scroll the not-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, pinned and floating'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
buildSliverAppBarApp
(
floating:
true
,
pinned:
true
,
expandedHeight:
128.0
,
));
ScrollController
controller
=
primaryScrollController
(
tester
);
expect
(
controller
.
offset
,
0.0
);
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
appBarHeight
(
tester
),
128.0
);
final
double
initialAppBarHeight
=
128.0
;
final
double
initialTabBarHeight
=
tabBarHeight
(
tester
);
// Scroll the not-pinned appbar, collapsing the expanded height. At this
// point only the tabBar is visible.
controller
.
jumpTo
(
600.0
);
await
tester
.
pump
();
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
tabBarHeight
(
tester
),
initialTabBarHeight
);
expect
(
appBarHeight
(
tester
),
lessThan
(
initialAppBarHeight
));
expect
(
appBarHeight
(
tester
),
initialTabBarHeight
);
// Scroll the not-pinned appbar back into view
controller
.
jumpTo
(
0.0
);
await
tester
.
pump
();
expect
(
appBarIsVisible
(
tester
),
true
);
expect
(
appBarHeight
(
tester
),
initialAppBarHeight
);
expect
(
tabBarHeight
(
tester
),
initialTabBarHeight
);
});
}
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