Commit 83a4cf26 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Port AppBar to Scrollable2 (#7996)

Move the back button and drawer opening logic into the app bar.

Move the tap-status-bar-to-scroll-to-top logic to using
ScrollControllers. Provide a PrimaryScrollController and a `primary`
flag on scroll views.

Make it possible to track when a route becomes or stops being poppable.
parent 8838a8fb
...@@ -86,24 +86,27 @@ class _RecipeGridPageState extends State<RecipeGridPage> { ...@@ -86,24 +86,27 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
data: _kTheme.copyWith(platform: Theme.of(context).platform), data: _kTheme.copyWith(platform: Theme.of(context).platform),
child: new Scaffold( child: new Scaffold(
key: scaffoldKey, key: scaffoldKey,
scrollableKey: config.scrollableKey,
appBarBehavior: AppBarBehavior.under,
appBar: _buildAppBar(context, statusBarHeight),
floatingActionButton: new FloatingActionButton( floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.edit), child: new Icon(Icons.edit),
onPressed: () { onPressed: () {
scaffoldKey.currentState.showSnackBar(new SnackBar( scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('Not supported.') content: new Text('Not supported.'),
)); ));
}, },
), ),
body: _buildBody(context, statusBarHeight), body: new CustomScrollView(
slivers: <Widget>[
_buildAppBar(context, statusBarHeight),
_buildBody(context, statusBarHeight),
],
),
) )
); );
} }
Widget _buildAppBar(BuildContext context, double statusBarHeight) { Widget _buildAppBar(BuildContext context, double statusBarHeight) {
return new AppBar( return new SliverAppBar(
pinned: true,
expandedHeight: _kAppBarHeight, expandedHeight: _kAppBarHeight,
actions: <Widget>[ actions: <Widget>[
new IconButton( new IconButton(
...@@ -111,7 +114,7 @@ class _RecipeGridPageState extends State<RecipeGridPage> { ...@@ -111,7 +114,7 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
tooltip: 'Search', tooltip: 'Search',
onPressed: () { onPressed: () {
scaffoldKey.currentState.showSnackBar(new SnackBar( scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('Not supported.') content: new Text('Not supported.'),
)); ));
}, },
), ),
...@@ -138,22 +141,25 @@ class _RecipeGridPageState extends State<RecipeGridPage> { ...@@ -138,22 +141,25 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
} }
Widget _buildBody(BuildContext context, double statusBarHeight) { Widget _buildBody(BuildContext context, double statusBarHeight) {
final EdgeInsets padding = new EdgeInsets.fromLTRB(8.0, 8.0 + _kAppBarHeight + statusBarHeight, 8.0, 8.0); final EdgeInsets padding = const EdgeInsets.all(8.0);
return new SliverPadding(
return new ScrollableGrid( padding: padding,
scrollableKey: config.scrollableKey, child: new SliverGrid(
delegate: new MaxTileWidthGridDelegate( gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxTileWidth: _kRecipePageMaxWidth, maxCrossAxisExtent: _kRecipePageMaxWidth,
rowSpacing: 8.0, crossAxisSpacing: 8.0,
columnSpacing: 8.0, mainAxisSpacing: 8.0,
padding: padding ),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
Recipe recipe = config.recipes[index];
return new RecipeCard(
recipe: recipe,
onTap: () { showRecipePage(context, recipe); },
);
}
),
), ),
children: config.recipes.map((Recipe recipe) {
return new RecipeCard(
recipe: recipe,
onTap: () { showRecipePage(context, recipe); }
);
}),
); );
} }
...@@ -297,84 +303,69 @@ class RecipePage extends StatefulWidget { ...@@ -297,84 +303,69 @@ class RecipePage extends StatefulWidget {
class _RecipePageState extends State<RecipePage> { class _RecipePageState extends State<RecipePage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final GlobalKey<ScrollableState> _scrollableKey = new GlobalKey<ScrollableState>();
final TextStyle menuItemStyle = new PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0); final TextStyle menuItemStyle = new PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0);
final Object _disableHeroTransition = new Object();
double _getAppBarHeight(BuildContext context) => MediaQuery.of(context).size.height * 0.3; double _getAppBarHeight(BuildContext context) => MediaQuery.of(context).size.height * 0.3;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( // The full page content with the recipe's image behind it. This
key: _scaffoldKey, // adjusts based on the size of the screen. If the recipe sheet touches
scrollableKey: _scrollableKey, // the edge of the screen, use a slightly different layout.
appBarBehavior: AppBarBehavior.scroll, final double appBarHeight = _getAppBarHeight(context);
appBar: new AppBar(
heroTag: _disableHeroTransition,
expandedHeight: _getAppBarHeight(context) - _kFabHalfSize,
backgroundColor: Colors.transparent,
elevation: 0,
actions: <Widget>[
new PopupMenuButton<String>(
onSelected: (String item) {},
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
_buildMenuItem(Icons.share, 'Tweet recipe'),
_buildMenuItem(Icons.email, 'Email recipe'),
_buildMenuItem(Icons.message, 'Message recipe'),
_buildMenuItem(Icons.people, 'Share on Facebook'),
]
)
],
flexibleSpace: new FlexibleSpaceBar(
background: new DecoratedBox(
decoration: new BoxDecoration(
gradient: new LinearGradient(
begin: const FractionalOffset(0.5, 0.0),
end: const FractionalOffset(0.5, 0.40),
colors: <Color>[const Color(0x60000000), const Color(0x00000000)],
)
)
)
),
),
body: _buildBody(context),
);
}
// The full page content with the recipe's image behind it. This
// adjusts based on the size of the screen. If the recipe sheet touches
// the edge of the screen, use a slightly different layout.
Widget _buildBody(BuildContext context) {
final bool isFavorite = _favoriteRecipes.contains(config.recipe);
final Size screenSize = MediaQuery.of(context).size; final Size screenSize = MediaQuery.of(context).size;
final bool fullWidth = (screenSize.width < _kRecipePageMaxWidth); final bool fullWidth = (screenSize.width < _kRecipePageMaxWidth);
final double appBarHeight = _getAppBarHeight(context); final bool isFavorite = _favoriteRecipes.contains(config.recipe);
return new Stack( return new Scaffold(
children: <Widget>[ key: _scaffoldKey,
new Positioned( body: new Stack(
top: 0.0, children: <Widget>[
left: 0.0, new Positioned(
right: 0.0, top: 0.0,
height: appBarHeight + _kFabHalfSize, left: 0.0,
child: new Hero( right: 0.0,
tag: config.recipe.imagePath, height: appBarHeight + _kFabHalfSize,
child: new Image.asset( child: new Hero(
config.recipe.imagePath, tag: config.recipe.imagePath,
fit: fullWidth ? ImageFit.fitWidth : ImageFit.cover, child: new Image.asset(
config.recipe.imagePath,
fit: fullWidth ? ImageFit.fitWidth : ImageFit.cover,
),
), ),
), ),
), new CustomScrollView(
new ClampOverscrolls( slivers: <Widget>[
edge: ScrollableEdge.both, new SliverAppBar(
child: new ScrollableViewport( expandedHeight: appBarHeight - _kFabHalfSize,
scrollableKey: _scrollableKey, backgroundColor: Colors.transparent,
child: new RepaintBoundary( actions: <Widget>[
child: new Padding( new PopupMenuButton<String>(
padding: new EdgeInsets.only(top: appBarHeight), onSelected: (String item) {},
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
_buildMenuItem(Icons.share, 'Tweet recipe'),
_buildMenuItem(Icons.email, 'Email recipe'),
_buildMenuItem(Icons.message, 'Message recipe'),
_buildMenuItem(Icons.people, 'Share on Facebook'),
],
),
],
flexibleSpace: new FlexibleSpaceBar(
background: new DecoratedBox(
decoration: new BoxDecoration(
gradient: new LinearGradient(
begin: const FractionalOffset(0.5, 0.0),
end: const FractionalOffset(0.5, 0.40),
colors: <Color>[const Color(0x60000000), const Color(0x00000000)],
),
),
),
),
),
new SliverToBoxAdapter(
child: new Stack( child: new Stack(
children: <Widget>[ children: <Widget>[
new Container( new Container(
padding: new EdgeInsets.only(top: _kFabHalfSize), padding: const EdgeInsets.only(top: _kFabHalfSize),
width: fullWidth ? null : _kRecipePageMaxWidth, width: fullWidth ? null : _kRecipePageMaxWidth,
child: new RecipeSheet(recipe: config.recipe), child: new RecipeSheet(recipe: config.recipe),
), ),
...@@ -386,12 +377,12 @@ class _RecipePageState extends State<RecipePage> { ...@@ -386,12 +377,12 @@ class _RecipePageState extends State<RecipePage> {
), ),
), ),
], ],
), )
), ),
), ],
), ),
), ],
], ),
); );
} }
......
...@@ -40,8 +40,8 @@ class ShrinePage extends StatefulWidget { ...@@ -40,8 +40,8 @@ class ShrinePage extends StatefulWidget {
class ShrinePageState extends State<ShrinePage> { class ShrinePageState extends State<ShrinePage> {
int _appBarElevation = 0; int _appBarElevation = 0;
bool _handleScrollNotification(ScrollNotification notification) { bool _handleScrollNotification(ScrollNotification2 notification) {
int elevation = notification.scrollable.scrollOffset <= 0.0 ? 0 : 1; int elevation = notification.metrics.extentBefore <= 0.0 ? 0 : 1;
if (elevation != _appBarElevation) { if (elevation != _appBarElevation) {
setState(() { setState(() {
_appBarElevation = elevation; _appBarElevation = elevation;
...@@ -141,7 +141,7 @@ class ShrinePageState extends State<ShrinePage> { ...@@ -141,7 +141,7 @@ class ShrinePageState extends State<ShrinePage> {
] ]
), ),
floatingActionButton: config.floatingActionButton, floatingActionButton: config.floatingActionButton,
body: new NotificationListener<ScrollNotification>( body: new NotificationListener<ScrollNotification2>(
onNotification: _handleScrollNotification, onNotification: _handleScrollNotification,
child: config.body child: config.body
) )
......
...@@ -41,16 +41,13 @@ class _AppBarBackground extends StatelessWidget { ...@@ -41,16 +41,13 @@ class _AppBarBackground extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// TODO(abarth): Wire up to the parallax of the FlexibleSpaceBar in a way
// that doesn't pop during hero transition.
Animation<double> effectiveAnimation = kAlwaysDismissedAnimation;
return new AnimatedBuilder( return new AnimatedBuilder(
animation: effectiveAnimation, animation: animation,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
return new Stack( return new Stack(
children: _kBackgroundLayers.map((_BackgroundLayer layer) { children: _kBackgroundLayers.map((_BackgroundLayer layer) {
return new Positioned( return new Positioned(
top: -layer.parallaxTween.evaluate(effectiveAnimation), top: -layer.parallaxTween.evaluate(animation),
left: 0.0, left: 0.0,
right: 0.0, right: 0.0,
bottom: 0.0, bottom: 0.0,
...@@ -107,7 +104,6 @@ class GalleryHome extends StatefulWidget { ...@@ -107,7 +104,6 @@ class GalleryHome extends StatefulWidget {
class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin { class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin {
static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
static final GlobalKey<ScrollableState> _scrollableKey = new GlobalKey<ScrollableState>();
AnimationController _controller; AnimationController _controller;
...@@ -153,10 +149,8 @@ class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderState ...@@ -153,10 +149,8 @@ class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
Widget home = new Scaffold( Widget home = new Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
scrollableKey: _scrollableKey,
drawer: new GalleryDrawer( drawer: new GalleryDrawer(
useLightTheme: config.useLightTheme, useLightTheme: config.useLightTheme,
onThemeChanged: config.onThemeChanged, onThemeChanged: config.onThemeChanged,
...@@ -169,27 +163,19 @@ class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderState ...@@ -169,27 +163,19 @@ class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderState
onPlatformChanged: config.onPlatformChanged, onPlatformChanged: config.onPlatformChanged,
onSendFeedback: config.onSendFeedback, onSendFeedback: config.onSendFeedback,
), ),
appBar: new AppBar( body: new CustomScrollView(
expandedHeight: _kFlexibleSpaceMaxHeight, slivers: <Widget>[
flexibleSpace: new FlexibleSpaceBar( new SliverAppBar(
title: new Text('Flutter Gallery'), pinned: true,
background: new Builder( expandedHeight: _kFlexibleSpaceMaxHeight,
builder: (BuildContext context) { flexibleSpace: new FlexibleSpaceBar(
return new _AppBarBackground( title: new Text('Flutter Gallery'),
animation: _scaffoldKey.currentState.appBarAnimation // TODO(abarth): Wire up to the parallax in a way that doesn't pop during hero transition.
); background: new _AppBarBackground(animation: kAlwaysDismissedAnimation),
} ),
) ),
) new SliverList(delegate: new SliverChildListDelegate(_galleryListItems())),
), ],
appBarBehavior: AppBarBehavior.under,
// The block's padding just exists to occupy the space behind the flexible app bar.
// As the block's padded region is scrolled upwards, the app bar's height will
// shrink keep it above the block content's and over the padded region.
body: new Block(
scrollableKey: _scrollableKey,
padding: new EdgeInsets.only(top: _kFlexibleSpaceMaxHeight + statusBarHeight),
children: _galleryListItems()
) )
); );
......
...@@ -21,11 +21,12 @@ void main() { ...@@ -21,11 +21,12 @@ void main() {
// Scroll the Buttons demo into view so that a tap will succeed // Scroll the Buttons demo into view so that a tap will succeed
final Point allDemosOrigin = tester.getTopRight(find.text('Demos')); final Point allDemosOrigin = tester.getTopRight(find.text('Demos'));
final Point buttonsDemoOrigin = tester.getTopRight(find.text('Buttons')); final Finder button = find.text('Buttons');
final double scrollDelta = buttonsDemoOrigin.y - allDemosOrigin.y; while (button.evaluate().isEmpty) {
await tester.scrollAt(allDemosOrigin, new Offset(0.0, -scrollDelta)); await tester.scrollAt(allDemosOrigin, new Offset(0.0, -100.0));
await tester.pump(); // start the scroll await tester.pump(); // start the scroll
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
}
// Launch the buttons demo and then prove that showing the example // Launch the buttons demo and then prove that showing the example
// code dialog does not crash. // code dialog does not crash.
......
...@@ -38,6 +38,7 @@ Future<Null> smokeDemo(WidgetTester tester, String routeName) async { ...@@ -38,6 +38,7 @@ Future<Null> smokeDemo(WidgetTester tester, String routeName) async {
await tester.pump(const Duration(seconds: 1)); // Wait until the demo has opened. await tester.pump(const Duration(seconds: 1)); // Wait until the demo has opened.
expect(find.text(kCaption), findsNothing); expect(find.text(kCaption), findsNothing);
await tester.pump(const Duration(seconds: 1)); // Leave the demo on the screen briefly for manual testing.
// Go back // Go back
Finder backButton = find.byTooltip('Back'); Finder backButton = find.byTooltip('Back');
...@@ -61,22 +62,12 @@ Future<Null> runSmokeTest(WidgetTester tester) async { ...@@ -61,22 +62,12 @@ Future<Null> runSmokeTest(WidgetTester tester) async {
expect(find.text(kCaption), findsOneWidget); expect(find.text(kCaption), findsOneWidget);
final List<double> scrollDeltas = new List<double>();
double previousY = tester.getTopRight(find.text(demoCategories[0])).y;
for (String routeName in routeNames) { for (String routeName in routeNames) {
final double y = tester.getTopRight(findGalleryItemByRouteName(tester, routeName)).y; Finder finder = findGalleryItemByRouteName(tester, routeName);
scrollDeltas.add(previousY - y); Scrollable2.ensureVisible(tester.element(finder), alignment: 0.5);
previousY = y; await tester.pump();
} await tester.pumpUntilNoTransientCallbacks();
// Launch each demo and then scroll that item out of the way.
for (int i = 0; i < routeNames.length; i += 1) {
final String routeName = routeNames[i];
await smokeDemo(tester, routeName); await smokeDemo(tester, routeName);
await tester.scroll(findGalleryItemByRouteName(tester, routeName), new Offset(0.0, scrollDeltas[i]));
await tester.pump(); // start the scroll
await tester.pump(const Duration(milliseconds: 500)); // wait for overscroll to timeout, if necessary
await tester.pump(const Duration(seconds: 3)); // wait for overscroll to fade away, if necessary
tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after leaving route $routeName'); tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after leaving route $routeName');
} }
......
...@@ -4,33 +4,32 @@ ...@@ -4,33 +4,32 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'app_bar.dart';
import 'constants.dart'; import 'constants.dart';
import 'scaffold.dart';
import 'theme.dart'; import 'theme.dart';
/// The part of a material design [AppBar] that expands and collapses. /// The part of a material design [AppBar] that expands and collapses.
/// ///
/// Most commonly used in in the [AppBar.flexibleSpace] field, a flexible space /// Most commonly used in in the [SliverAppBar.flexibleSpace] field, a flexible
/// bar expands and contracts as the app scrolls so that the [AppBar] reaches /// space bar expands and contracts as the app scrolls so that the [AppBar]
/// from the top of the app to the top of the scrolling contents of the app. /// reaches from the top of the app to the top of the scrolling contents of the
/// app.
/// ///
/// Requires one of its ancestors to be a [Scaffold] widget because the /// The widget that sizes the [AppBar] must wrap it in the widget returned by
/// [Scaffold] coordinates the scrolling effect between the flexible space and /// [FlexibleSpaceBar.createSettings], to convey sizing information down to the
/// its body. /// [FlexibleSpaceBar].
/// ///
/// See also: /// See also:
/// ///
/// * [AppBar] /// * [SliverAppBar], which implements the expanding and contracting.
/// * [Scaffold] /// * [AppBar], which is used by [SliverAppBar].
/// * <https://material.google.com/patterns/scrolling-techniques.html> /// * <https://material.google.com/patterns/scrolling-techniques.html>
class FlexibleSpaceBar extends StatefulWidget { class FlexibleSpaceBar extends StatefulWidget {
/// Creates a flexible space bar. /// Creates a flexible space bar.
/// ///
/// Most commonly used in the [AppBar.flexibleSpace] field. Requires one of /// Most commonly used in the [AppBar.flexibleSpace] field.
/// its ancestors to be a [Scaffold] widget.
FlexibleSpaceBar({ FlexibleSpaceBar({
Key key, Key key,
this.title, this.title,
...@@ -53,6 +52,23 @@ class FlexibleSpaceBar extends StatefulWidget { ...@@ -53,6 +52,23 @@ class FlexibleSpaceBar extends StatefulWidget {
/// Defaults to being adapted to the current [TargetPlatform]. /// Defaults to being adapted to the current [TargetPlatform].
final bool centerTitle; final bool centerTitle;
static Widget createSettings({
double toolbarOpacity,
double minExtent,
double maxExtent,
@required double currentExtent,
@required Widget child,
}) {
assert(currentExtent != null);
return new _FlexibleSpaceBarSettings(
toolbarOpacity: toolbarOpacity ?? 1.0,
minExtent: minExtent ?? currentExtent,
maxExtent: maxExtent ?? currentExtent,
currentExtent: currentExtent,
child: child,
);
}
@override @override
_FlexibleSpaceBarState createState() => new _FlexibleSpaceBarState(); _FlexibleSpaceBarState createState() => new _FlexibleSpaceBarState();
} }
...@@ -72,34 +88,32 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> { ...@@ -72,34 +88,32 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
return null; return null;
} }
Widget _buildContent(BuildContext context, BoxConstraints constraints) { @override
final Size size = constraints.biggest; Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top; _FlexibleSpaceBarSettings settings = context.inheritFromWidgetOfExactType(_FlexibleSpaceBarSettings);
assert(settings != null, 'A FlexibleSpaceBar must be wrapped in the widget returned by FlexibleSpaceBar.createSettings().');
final List<Widget> children = <Widget>[];
final double currentHeight = size.height; final double deltaExtent = settings.maxExtent - settings.minExtent;
final double maxHeight = statusBarHeight + AppBar.getExpandedHeightFor(context);
final double minHeight = statusBarHeight + kToolbarHeight;
final double deltaHeight = maxHeight - minHeight;
// 0.0 -> Expanded // 0.0 -> Expanded
// 1.0 -> Collapsed to toolbar // 1.0 -> Collapsed to toolbar
final double t = (1.0 - (currentHeight - minHeight) / deltaHeight).clamp(0.0, 1.0); final double t = (1.0 - (settings.currentExtent - settings.minExtent) / (deltaExtent)).clamp(0.0, 1.0);
final List<Widget> children = <Widget>[];
// background image // background image
if (config.background != null) { if (config.background != null) {
final double fadeStart = math.max(0.0, 1.0 - kToolbarHeight / deltaHeight); final double fadeStart = math.max(0.0, 1.0 - kToolbarHeight / deltaExtent);
final double fadeEnd = 1.0; final double fadeEnd = 1.0;
assert(fadeStart <= fadeEnd); assert(fadeStart <= fadeEnd);
final double opacity = 1.0 - new Interval(fadeStart, fadeEnd).transform(t); final double opacity = 1.0 - new Interval(fadeStart, fadeEnd).transform(t);
final double parallax = new Tween<double>(begin: 0.0, end: deltaHeight / 4.0).lerp(t); final double parallax = new Tween<double>(begin: 0.0, end: deltaExtent / 4.0).lerp(t);
if (opacity > 0.0) { if (opacity > 0.0) {
children.add(new Positioned( children.add(new Positioned(
top: -parallax, top: -parallax,
left: 0.0, left: 0.0,
right: 0.0, right: 0.0,
height: maxHeight, height: settings.maxExtent,
child: new Opacity( child: new Opacity(
opacity: opacity, opacity: opacity,
child: config.background child: config.background
...@@ -110,7 +124,7 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> { ...@@ -110,7 +124,7 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
if (config.title != null) { if (config.title != null) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final double opacity = (1.0 - (minHeight - currentHeight) / (kToolbarHeight - statusBarHeight)).clamp(0.0, 1.0); final double opacity = settings.toolbarOpacity;
if (opacity > 0.0) { if (opacity > 0.0) {
TextStyle titleStyle = theme.primaryTextTheme.title; TextStyle titleStyle = theme.primaryTextTheme.title;
titleStyle = titleStyle.copyWith( titleStyle = titleStyle.copyWith(
...@@ -140,9 +154,28 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> { ...@@ -140,9 +154,28 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
return new ClipRect(child: new Stack(children: children)); return new ClipRect(child: new Stack(children: children));
} }
}
class _FlexibleSpaceBarSettings extends InheritedWidget {
_FlexibleSpaceBarSettings({
Key key,
this.toolbarOpacity,
this.minExtent,
this.maxExtent,
this.currentExtent,
Widget child,
}) : super(key: key, child: child);
final double toolbarOpacity;
final double minExtent;
final double maxExtent;
final double currentExtent;
@override @override
Widget build(BuildContext context) { bool updateShouldNotify(_FlexibleSpaceBarSettings oldWidget) {
return new LayoutBuilder(builder: _buildContent); return toolbarOpacity != oldWidget.toolbarOpacity
|| minExtent != oldWidget.minExtent
|| maxExtent != oldWidget.maxExtent
|| currentExtent != oldWidget.currentExtent;
} }
} }
...@@ -473,6 +473,7 @@ class SemanticsNode extends AbstractNode { ...@@ -473,6 +473,7 @@ class SemanticsNode extends AbstractNode {
owner._nodes.remove(id); owner._nodes.remove(id);
owner._detachedNodes.add(this); owner._detachedNodes.add(this);
super.detach(); super.detach();
assert(owner == null);
if (_children != null) { if (_children != null) {
for (SemanticsNode child in _children) { for (SemanticsNode child in _children) {
// The list of children may be stale and may contain nodes that have // The list of children may be stale and may contain nodes that have
...@@ -481,6 +482,10 @@ class SemanticsNode extends AbstractNode { ...@@ -481,6 +482,10 @@ class SemanticsNode extends AbstractNode {
child.detach(); child.detach();
} }
} }
// The other side will have forgotten this node if we ever send
// it again, so make sure to mark it dirty so that it'll get
// sent if it is resurrected.
_markDirty();
} }
bool _dirty = false; bool _dirty = false;
...@@ -563,7 +568,7 @@ class SemanticsNode extends AbstractNode { ...@@ -563,7 +568,7 @@ class SemanticsNode extends AbstractNode {
StringBuffer buffer = new StringBuffer(); StringBuffer buffer = new StringBuffer();
buffer.write('$runtimeType($id'); buffer.write('$runtimeType($id');
if (_dirty) if (_dirty)
buffer.write(" (${ owner != null && owner._dirtyNodes.contains(this) ? 'dirty' : 'STALE' })"); buffer.write(' (${ owner != null && owner._dirtyNodes.contains(this) ? "dirty" : "STALE; owner=$owner" })');
if (_shouldMergeAllDescendantsIntoThisNode) if (_shouldMergeAllDescendantsIntoThisNode)
buffer.write(' (leaf merge)'); buffer.write(' (leaf merge)');
buffer.write('; $rect'); buffer.write('; $rect');
...@@ -624,19 +629,13 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -624,19 +629,13 @@ class SemanticsOwner extends ChangeNotifier {
/// Update the semantics using [ui.window.updateSemantics]. /// Update the semantics using [ui.window.updateSemantics].
void sendSemanticsUpdate() { void sendSemanticsUpdate() {
for (SemanticsNode oldNode in _detachedNodes) {
// The other side will have forgotten this node if we even send
// it again, so make sure to mark it dirty so that it'll get
// sent if it is resurrected.
oldNode._dirty = true;
}
_detachedNodes.clear();
if (_dirtyNodes.isEmpty) if (_dirtyNodes.isEmpty)
return; return;
List<SemanticsNode> visitedNodes = <SemanticsNode>[]; List<SemanticsNode> visitedNodes = <SemanticsNode>[];
while (_dirtyNodes.isNotEmpty) { while (_dirtyNodes.isNotEmpty) {
List<SemanticsNode> localDirtyNodes = _dirtyNodes.toList(); List<SemanticsNode> localDirtyNodes = _dirtyNodes.where((SemanticsNode node) => !_detachedNodes.contains(node)).toList();
_dirtyNodes.clear(); _dirtyNodes.clear();
_detachedNodes.clear();
localDirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth); localDirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
visitedNodes.addAll(localDirtyNodes); visitedNodes.addAll(localDirtyNodes);
for (SemanticsNode node in localDirtyNodes) { for (SemanticsNode node in localDirtyNodes) {
...@@ -759,4 +758,7 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -759,4 +758,7 @@ class SemanticsOwner extends ChangeNotifier {
SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action); SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action);
handler?.performAction(action); handler?.performAction(action);
} }
@override
String toString() => '$runtimeType@$hashCode';
} }
...@@ -20,16 +20,19 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje ...@@ -20,16 +20,19 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
this.child = child; this.child = child;
} }
/// The biggest that this render object can become, in the main axis direction.
///
/// This value should not be based on the child. If it changes, call
/// [markNeedsLayout].
double get maxExtent; double get maxExtent;
/// The intrinsic size of the child as of the last time the sliver was laid out. /// The smallest that this render object can become, in the main axis direction.
/// ///
/// If the render object is dirty (i.e. if [markNeedsLayout] has been called, /// If this is based on the intrinsic dimensions of the child, the child
/// or if the object was newly created), then the returned value will be stale /// should be measured during [updateChild] and the value cached and returned
/// until [layoutChild] has been called. /// here. The [updateChild] method will automatically be invoked any time the
@protected /// child changes its intrinsic dimensions.
double get minExtent => _minExtent; double get minExtent;
double _minExtent;
@protected @protected
double get childExtent { double get childExtent {
...@@ -46,69 +49,65 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje ...@@ -46,69 +49,65 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
return null; return null;
} }
@protected bool _needsUpdateChild = true;
double _getChildIntrinsicExtent() { double _lastShrinkOffset = 0.0;
if (child == null) bool _lastOverlapsContent = false;
return 0.0;
assert(child != null);
assert(constraints.axis != null);
switch (constraints.axis) {
case Axis.vertical:
return child.getMinIntrinsicHeight(constraints.crossAxisExtent);
case Axis.horizontal:
return child.getMinIntrinsicWidth(constraints.crossAxisExtent);
}
return null;
}
/// The last value that we passed to updateChild(). /// Update the child render object if necessary.
double _lastShrinkOffset;
/// Called during layout if the shrink offset has changed.
/// ///
/// During this callback, the [child] can be set, mutated, or replaced. /// Called before the first layout, any time [markNeedsLayout] is called, and
@protected /// any time the scroll offset changes. The `shrinkOffset` is the difference
void updateChild(double shrinkOffset) { } /// between the [maxExtent] and the current size. Zero means the header is
/// fully expanded, any greater number up to [maxExtent] means that the header
/// Flag the current child as stale and needing updating even if the shrink /// has been scrolled by that much. The `overlapsContent` argument is true if
/// offset has not changed. /// the sliver's leading edge is beyond its normal place in the viewport
/// contents, and false otherwise. It may still paint beyond its normal place
/// if the [minExtent] after this call is greater than the amount of space that
/// would normally be left.
///
/// The render object will size itself to the larger of (a) the [maxExtent]
/// minus the child's intrinsic height and (b) the [maxExtent] minus the
/// shrink offset.
/// ///
/// Call this whenever [updateChild] would change or mutate the child even if /// When this method is called by [layoutChild], the [child] can be set,
/// given the same `shrinkOffset` as the last time it was called. /// mutated, or replaced. (It should not be called outside [layoutChild].)
/// ///
/// This must be implemented by [RenderSliverPersistentHeader] subclasses such /// Any time this method would mutate the child, call [markNeedsLayout].
/// that the next layout after this call will result in [updateChild] being
/// called.
@protected @protected
void markNeedsUpdate() { void updateChild(double shrinkOffset, bool overlapsContent) { }
markNeedsLayout();
_lastShrinkOffset = null; @override
void markNeedsLayout() {
// This is automatically called whenever the child's intrinsic dimensions
// change, at which point we should remeasure them during the next layout.
_needsUpdateChild = true;
super.markNeedsLayout();
} }
void layoutChild(double scrollOffset, double maxExtent) { void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent: false }) {
assert(maxExtent != null); assert(maxExtent != null);
final double shrinkOffset = math.min(scrollOffset, maxExtent); final double shrinkOffset = math.min(scrollOffset, maxExtent);
if (shrinkOffset != _lastShrinkOffset) { if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints); assert(constraints == this.constraints);
updateChild(shrinkOffset); updateChild(shrinkOffset, overlapsContent);
_minExtent = _getChildIntrinsicExtent();
}); });
_lastShrinkOffset = shrinkOffset; _lastShrinkOffset = shrinkOffset;
_lastOverlapsContent = overlapsContent;
_needsUpdateChild = false;
} }
assert(_minExtent != null); assert(minExtent != null);
assert(() { assert(() {
if (_minExtent <= maxExtent) if (minExtent <= maxExtent)
return true; return true;
throw new FlutterError( throw new FlutterError(
'The maxExtent for this $runtimeType is less than the child\'s intrinsic extent.\n' 'The maxExtent for this $runtimeType is less than its minExtent.\n'
'The specified maxExtent was: ${maxExtent.toStringAsFixed(1)}\n' 'The specified maxExtent was: ${maxExtent.toStringAsFixed(1)}\n'
'The child was updated with shrink offset: ${shrinkOffset.toStringAsFixed(1)}\n' 'The specified minExtent was: ${minExtent.toStringAsFixed(1)}\n'
'The actual measured intrinsic extent of the child was: ${_minExtent.toStringAsFixed(1)}\n'
); );
}); });
child?.layout( child?.layout(
constraints.asBoxConstraints(maxExtent: math.max(_minExtent, maxExtent - shrinkOffset)), constraints.asBoxConstraints(maxExtent: math.max(minExtent, maxExtent - shrinkOffset)),
parentUsesSize: true, parentUsesSize: true,
); );
} }
...@@ -237,7 +236,7 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent ...@@ -237,7 +236,7 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
@override @override
void performLayout() { void performLayout() {
final double maxExtent = this.maxExtent; final double maxExtent = this.maxExtent;
layoutChild(constraints.scrollOffset + constraints.overlap, maxExtent); layoutChild(constraints.scrollOffset + constraints.overlap, maxExtent, overlapsContent: constraints.overlap > 0.0);
geometry = new SliverGeometry( geometry = new SliverGeometry(
scrollExtent: maxExtent, scrollExtent: maxExtent,
paintExtent: math.min(constraints.overlap + childExtent, constraints.remainingPaintExtent), paintExtent: math.min(constraints.overlap + childExtent, constraints.remainingPaintExtent),
...@@ -285,7 +284,7 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste ...@@ -285,7 +284,7 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
} else { } else {
_effectiveScrollOffset = constraints.scrollOffset; _effectiveScrollOffset = constraints.scrollOffset;
} }
layoutChild(_effectiveScrollOffset, maxExtent); layoutChild(_effectiveScrollOffset, maxExtent, overlapsContent: _effectiveScrollOffset < constraints.scrollOffset);
final double paintExtent = maxExtent - _effectiveScrollOffset; final double paintExtent = maxExtent - _effectiveScrollOffset;
final double layoutExtent = (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent); final double layoutExtent = (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);
geometry = new SliverGeometry( geometry = new SliverGeometry(
......
...@@ -155,9 +155,7 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -155,9 +155,7 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
assert(mounted); assert(mounted);
NavigatorState navigator = _navigator.currentState; NavigatorState navigator = _navigator.currentState;
assert(navigator != null); assert(navigator != null);
if (!await navigator.willPop()) return await navigator.maybePop();
return true;
return mounted && navigator.pop();
} }
@override @override
......
// Copyright 2015 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/foundation.dart';
import 'framework.dart';
import 'scroll_controller.dart';
class PrimaryScrollController extends InheritedWidget {
PrimaryScrollController({
Key key,
@required this.controller,
@required Widget child
}) : super(key: key, child: child) {
assert(controller != null);
}
final ScrollController controller;
static ScrollController of(BuildContext context) {
PrimaryScrollController result = context.inheritFromWidgetOfExactType(PrimaryScrollController);
return result?.controller;
}
@override
bool updateShouldNotify(PrimaryScrollController old) => controller != old.controller;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$controller');
}
}
...@@ -295,7 +295,10 @@ abstract class LocalHistoryRoute<T> extends Route<T> { ...@@ -295,7 +295,10 @@ abstract class LocalHistoryRoute<T> extends Route<T> {
assert(entry._owner == null); assert(entry._owner == null);
entry._owner = this; entry._owner = this;
_localHistory ??= <LocalHistoryEntry>[]; _localHistory ??= <LocalHistoryEntry>[];
final bool wasEmpty = _localHistory.isEmpty;
_localHistory.add(entry); _localHistory.add(entry);
if (wasEmpty)
changedInternalState();
} }
/// Remove a local history entry from this route. /// Remove a local history entry from this route.
...@@ -308,6 +311,8 @@ abstract class LocalHistoryRoute<T> extends Route<T> { ...@@ -308,6 +311,8 @@ abstract class LocalHistoryRoute<T> extends Route<T> {
_localHistory.remove(entry); _localHistory.remove(entry);
entry._owner = null; entry._owner = null;
entry._notifyRemoved(); entry._notifyRemoved();
if (_localHistory.isEmpty)
changedInternalState();
} }
@override @override
...@@ -317,6 +322,8 @@ abstract class LocalHistoryRoute<T> extends Route<T> { ...@@ -317,6 +322,8 @@ abstract class LocalHistoryRoute<T> extends Route<T> {
assert(entry._owner == this); assert(entry._owner == this);
entry._owner = null; entry._owner = null;
entry._notifyRemoved(); entry._notifyRemoved();
if (_localHistory.isEmpty)
changedInternalState();
return false; return false;
} }
return super.didPop(result); return super.didPop(result);
...@@ -326,26 +333,40 @@ abstract class LocalHistoryRoute<T> extends Route<T> { ...@@ -326,26 +333,40 @@ abstract class LocalHistoryRoute<T> extends Route<T> {
bool get willHandlePopInternally { bool get willHandlePopInternally {
return _localHistory != null && _localHistory.isNotEmpty; return _localHistory != null && _localHistory.isNotEmpty;
} }
/// Called whenever the internal state of the route has changed.
///
/// This should be called whenever [willHandlePopInternally] and [didPop]
/// might change the value they return. It is used by [ModalRoute], for
/// example, to report the new information via its inherited widget to any
/// children of the route.
@protected
@mustCallSuper
void changedInternalState() { }
} }
class _ModalScopeStatus extends InheritedWidget { class _ModalScopeStatus extends InheritedWidget {
_ModalScopeStatus({ _ModalScopeStatus({
Key key, Key key,
this.isCurrent, @required this.isCurrent,
this.route, @required this.canPop,
@required this.route,
@required Widget child @required Widget child
}) : super(key: key, child: child) { }) : super(key: key, child: child) {
assert(isCurrent != null); assert(isCurrent != null);
assert(canPop != null);
assert(route != null); assert(route != null);
assert(child != null); assert(child != null);
} }
final bool isCurrent; final bool isCurrent;
final bool canPop;
final Route<dynamic> route; final Route<dynamic> route;
@override @override
bool updateShouldNotify(_ModalScopeStatus old) { bool updateShouldNotify(_ModalScopeStatus old) {
return isCurrent != old.isCurrent || return isCurrent != old.isCurrent ||
canPop != old.canPop ||
route != old.route; route != old.route;
} }
...@@ -353,6 +374,8 @@ class _ModalScopeStatus extends InheritedWidget { ...@@ -353,6 +374,8 @@ class _ModalScopeStatus extends InheritedWidget {
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('${isCurrent ? "active" : "inactive"}'); description.add('${isCurrent ? "active" : "inactive"}');
if (canPop)
description.add('can pop');
} }
} }
...@@ -396,7 +419,6 @@ class _ModalScopeState extends State<_ModalScope> { ...@@ -396,7 +419,6 @@ class _ModalScopeState extends State<_ModalScope> {
void dispose() { void dispose() {
config.route.animation?.removeStatusListener(_animationStatusChanged); config.route.animation?.removeStatusListener(_animationStatusChanged);
config.route.forwardAnimation?.removeStatusListener(_animationStatusChanged); config.route.forwardAnimation?.removeStatusListener(_animationStatusChanged);
willPopCallbacks = null;
super.dispose(); super.dispose();
} }
...@@ -439,13 +461,14 @@ class _ModalScopeState extends State<_ModalScope> { ...@@ -439,13 +461,14 @@ class _ModalScopeState extends State<_ModalScope> {
child: new _ModalScopeStatus( child: new _ModalScopeStatus(
route: config.route, route: config.route,
isCurrent: config.route.isCurrent, isCurrent: config.route.isCurrent,
child: config.page canPop: config.route.canPop(),
) child: config.page,
) ),
) ),
) ),
) ),
) ),
),
); );
} }
} }
...@@ -734,6 +757,24 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -734,6 +757,24 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return _scopeKey.currentState == null || _scopeKey.currentState.willPopCallbacks.length > 0; return _scopeKey.currentState == null || _scopeKey.currentState.willPopCallbacks.length > 0;
} }
@override
void changedInternalState() {
super.changedInternalState();
setState(() { /* internal state already changed */ });
}
@override
void didChangePrevious(Route<dynamic> route) {
super.didChangePrevious(route);
setState(() { /* this might affect canPop */ });
}
/// Whether this route can be popped.
///
/// When this changes, the route will rebuild, and any widgets that used
/// [ModalRoute.of] will be notified.
bool canPop() => !isFirst || willHandlePopInternally;
// Internals // Internals
final GlobalKey<_ModalScopeState> _scopeKey = new GlobalKey<_ModalScopeState>(); final GlobalKey<_ModalScopeState> _scopeKey = new GlobalKey<_ModalScopeState>();
......
...@@ -22,6 +22,14 @@ class ScrollController { ...@@ -22,6 +22,14 @@ class ScrollController {
final List<ScrollPosition> _positions = <ScrollPosition>[]; final List<ScrollPosition> _positions = <ScrollPosition>[];
/// Whether any [ScrollPosition] objects have attached themselves to the
/// [ScrollController] using the [attach] method.
///
/// If this is false, then members that interact with the [ScrollPosition],
/// such as [position], [offset], [animateTo], and [jumpTo], must not be
/// called.
bool get hasClients => _positions.isNotEmpty;
ScrollPosition get position { ScrollPosition get position {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.'); assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
......
...@@ -5,8 +5,9 @@ ...@@ -5,8 +5,9 @@
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'framework.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart';
import 'primary_scroll_controller.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scroll_position.dart'; import 'scroll_position.dart';
...@@ -20,11 +21,16 @@ abstract class ScrollView extends StatelessWidget { ...@@ -20,11 +21,16 @@ abstract class ScrollView extends StatelessWidget {
this.scrollDirection: Axis.vertical, this.scrollDirection: Axis.vertical,
this.reverse: false, this.reverse: false,
this.controller, this.controller,
this.primary: false,
this.physics, this.physics,
this.shrinkWrap: false, this.shrinkWrap: false,
}) : super(key: key) { }) : super(key: key) {
assert(reverse != null); assert(reverse != null);
assert(shrinkWrap != null); assert(shrinkWrap != null);
assert(primary != null);
assert(controller == null || !primary,
'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
'You cannot both set primary to true and pass an explicit controller.');
} }
final Axis scrollDirection; final Axis scrollDirection;
...@@ -33,6 +39,8 @@ abstract class ScrollView extends StatelessWidget { ...@@ -33,6 +39,8 @@ abstract class ScrollView extends StatelessWidget {
final ScrollController controller; final ScrollController controller;
final bool primary;
final ScrollPhysics physics; final ScrollPhysics physics;
final bool shrinkWrap; final bool shrinkWrap;
...@@ -58,7 +66,7 @@ abstract class ScrollView extends StatelessWidget { ...@@ -58,7 +66,7 @@ abstract class ScrollView extends StatelessWidget {
AxisDirection axisDirection = getDirection(context); AxisDirection axisDirection = getDirection(context);
return new Scrollable2( return new Scrollable2(
axisDirection: axisDirection, axisDirection: axisDirection,
controller: controller, controller: controller ?? (primary ? PrimaryScrollController.of(context) : null),
physics: physics, physics: physics,
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
if (shrinkWrap) { if (shrinkWrap) {
...@@ -82,6 +90,14 @@ abstract class ScrollView extends StatelessWidget { ...@@ -82,6 +90,14 @@ abstract class ScrollView extends StatelessWidget {
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('$scrollDirection'); description.add('$scrollDirection');
if (reverse)
description.add('reversed');
if (controller != null)
description.add('$controller');
if (primary)
description.add('using primary controller');
if (physics != null)
description.add('$physics');
if (shrinkWrap) if (shrinkWrap)
description.add('shrink-wrapping'); description.add('shrink-wrapping');
} }
...@@ -93,6 +109,7 @@ class CustomScrollView extends ScrollView { ...@@ -93,6 +109,7 @@ class CustomScrollView extends ScrollView {
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
bool reverse: false, bool reverse: false,
ScrollController controller, ScrollController controller,
bool primary: false,
ScrollPhysics physics, ScrollPhysics physics,
bool shrinkWrap: false, bool shrinkWrap: false,
this.slivers: const <Widget>[], this.slivers: const <Widget>[],
...@@ -101,6 +118,7 @@ class CustomScrollView extends ScrollView { ...@@ -101,6 +118,7 @@ class CustomScrollView extends ScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
controller: controller, controller: controller,
primary: primary,
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
); );
...@@ -117,6 +135,7 @@ abstract class BoxScrollView extends ScrollView { ...@@ -117,6 +135,7 @@ abstract class BoxScrollView extends ScrollView {
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
bool reverse: false, bool reverse: false,
ScrollController controller, ScrollController controller,
bool primary: false,
ScrollPhysics physics, ScrollPhysics physics,
bool shrinkWrap: false, bool shrinkWrap: false,
this.padding, this.padding,
...@@ -125,6 +144,7 @@ abstract class BoxScrollView extends ScrollView { ...@@ -125,6 +144,7 @@ abstract class BoxScrollView extends ScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
controller: controller, controller: controller,
primary: primary,
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
); );
...@@ -164,6 +184,7 @@ class ListView extends BoxScrollView { ...@@ -164,6 +184,7 @@ class ListView extends BoxScrollView {
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
bool reverse: false, bool reverse: false,
ScrollController controller, ScrollController controller,
bool primary: false,
ScrollPhysics physics, ScrollPhysics physics,
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
...@@ -174,6 +195,7 @@ class ListView extends BoxScrollView { ...@@ -174,6 +195,7 @@ class ListView extends BoxScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
controller: controller, controller: controller,
primary: primary,
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
...@@ -184,6 +206,7 @@ class ListView extends BoxScrollView { ...@@ -184,6 +206,7 @@ class ListView extends BoxScrollView {
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
bool reverse: false, bool reverse: false,
ScrollController controller, ScrollController controller,
bool primary: false,
ScrollPhysics physics, ScrollPhysics physics,
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
...@@ -195,6 +218,7 @@ class ListView extends BoxScrollView { ...@@ -195,6 +218,7 @@ class ListView extends BoxScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
controller: controller, controller: controller,
primary: primary,
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
...@@ -205,6 +229,7 @@ class ListView extends BoxScrollView { ...@@ -205,6 +229,7 @@ class ListView extends BoxScrollView {
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
bool reverse: false, bool reverse: false,
ScrollController controller, ScrollController controller,
bool primary: false,
ScrollPhysics physics, ScrollPhysics physics,
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
...@@ -215,6 +240,7 @@ class ListView extends BoxScrollView { ...@@ -215,6 +240,7 @@ class ListView extends BoxScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
controller: controller, controller: controller,
primary: primary,
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
...@@ -259,6 +285,7 @@ class GridView extends BoxScrollView { ...@@ -259,6 +285,7 @@ class GridView extends BoxScrollView {
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
bool reverse: false, bool reverse: false,
ScrollController controller, ScrollController controller,
bool primary: false,
ScrollPhysics physics, ScrollPhysics physics,
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
...@@ -269,6 +296,7 @@ class GridView extends BoxScrollView { ...@@ -269,6 +296,7 @@ class GridView extends BoxScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
controller: controller, controller: controller,
primary: primary,
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
...@@ -281,6 +309,7 @@ class GridView extends BoxScrollView { ...@@ -281,6 +309,7 @@ class GridView extends BoxScrollView {
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
bool reverse: false, bool reverse: false,
ScrollController controller, ScrollController controller,
bool primary: false,
ScrollPhysics physics, ScrollPhysics physics,
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
...@@ -291,6 +320,7 @@ class GridView extends BoxScrollView { ...@@ -291,6 +320,7 @@ class GridView extends BoxScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
controller: controller, controller: controller,
primary: primary,
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
...@@ -304,6 +334,7 @@ class GridView extends BoxScrollView { ...@@ -304,6 +334,7 @@ class GridView extends BoxScrollView {
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
bool reverse: false, bool reverse: false,
ScrollController controller, ScrollController controller,
bool primary: false,
ScrollPhysics physics, ScrollPhysics physics,
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
...@@ -323,6 +354,7 @@ class GridView extends BoxScrollView { ...@@ -323,6 +354,7 @@ class GridView extends BoxScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
controller: controller, controller: controller,
primary: primary,
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
...@@ -333,6 +365,7 @@ class GridView extends BoxScrollView { ...@@ -333,6 +365,7 @@ class GridView extends BoxScrollView {
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
bool reverse: false, bool reverse: false,
ScrollController controller, ScrollController controller,
bool primary: false,
ScrollPhysics physics, ScrollPhysics physics,
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
...@@ -352,6 +385,7 @@ class GridView extends BoxScrollView { ...@@ -352,6 +385,7 @@ class GridView extends BoxScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
controller: controller, controller: controller,
primary: primary,
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
......
...@@ -86,7 +86,12 @@ class Scrollable2 extends StatefulWidget { ...@@ -86,7 +86,12 @@ class Scrollable2 extends StatefulWidget {
Scrollable2State scrollable = Scrollable2.of(context); Scrollable2State scrollable = Scrollable2.of(context);
while (scrollable != null) { while (scrollable != null) {
futures.add(scrollable.position.ensureVisible(context.findRenderObject(), alignment: alignment)); futures.add(scrollable.position.ensureVisible(
context.findRenderObject(),
alignment: alignment,
duration: duration,
curve: curve,
));
context = scrollable.context; context = scrollable.context;
scrollable = Scrollable2.of(context); scrollable = Scrollable2.of(context);
} }
......
...@@ -12,7 +12,9 @@ abstract class SliverPersistentHeaderDelegate { ...@@ -12,7 +12,9 @@ abstract class SliverPersistentHeaderDelegate {
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
const SliverPersistentHeaderDelegate(); const SliverPersistentHeaderDelegate();
Widget build(BuildContext context, double shrinkOffset); Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);
double get minExtent;
double get maxExtent; double get maxExtent;
...@@ -101,9 +103,9 @@ class _SliverPersistentHeaderElement extends RenderObjectElement { ...@@ -101,9 +103,9 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
Element child; Element child;
void _build(double shrinkOffset) { void _build(double shrinkOffset, bool overlapsContent) {
owner.buildScope(this, () { owner.buildScope(this, () {
child = updateChild(child, widget.delegate.build(this, shrinkOffset), null); child = updateChild(child, widget.delegate.build(this, shrinkOffset, overlapsContent), null);
}); });
} }
...@@ -160,18 +162,21 @@ abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWid ...@@ -160,18 +162,21 @@ abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWid
abstract class _RenderSliverPersistentHeaderForWidgetsMixin implements RenderSliverPersistentHeader { abstract class _RenderSliverPersistentHeaderForWidgetsMixin implements RenderSliverPersistentHeader {
_SliverPersistentHeaderElement _element; _SliverPersistentHeaderElement _element;
@override
double get minExtent => _element.widget.delegate.minExtent;
@override @override
double get maxExtent => _element.widget.delegate.maxExtent; double get maxExtent => _element.widget.delegate.maxExtent;
@override @override
void updateChild(double shrinkOffset) { void updateChild(double shrinkOffset, bool overlapsContent) {
assert(_element != null); assert(_element != null);
_element._build(shrinkOffset); _element._build(shrinkOffset, overlapsContent);
} }
@protected @protected
void triggerRebuild() { void triggerRebuild() {
markNeedsUpdate(); markNeedsLayout();
} }
} }
......
...@@ -43,6 +43,7 @@ export 'src/widgets/page_view.dart'; ...@@ -43,6 +43,7 @@ export 'src/widgets/page_view.dart';
export 'src/widgets/pages.dart'; export 'src/widgets/pages.dart';
export 'src/widgets/performance_overlay.dart'; export 'src/widgets/performance_overlay.dart';
export 'src/widgets/placeholder.dart'; export 'src/widgets/placeholder.dart';
export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/scroll_behavior.dart'; export 'src/widgets/scroll_behavior.dart';
......
...@@ -80,32 +80,39 @@ void main() { ...@@ -80,32 +80,39 @@ void main() {
testWidgets('Drawer scrolling', (WidgetTester tester) async { testWidgets('Drawer scrolling', (WidgetTester tester) async {
GlobalKey<ScrollableState<Scrollable>> drawerKey = GlobalKey<ScrollableState<Scrollable>> drawerKey =
new GlobalKey<ScrollableState<Scrollable>>(debugLabel: 'drawer'); new GlobalKey<ScrollableState<Scrollable>>(debugLabel: 'drawer');
Key appBarKey = new Key('appBar');
const double appBarHeight = 256.0; const double appBarHeight = 256.0;
ScrollController scrollOffset = new ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
home: new Scaffold( home: new Scaffold(
appBarBehavior: AppBarBehavior.under,
appBar: new AppBar(
key: appBarKey,
expandedHeight: appBarHeight,
title: new Text('Title'),
flexibleSpace: new FlexibleSpaceBar(title: new Text('Title')),
),
drawer: new Drawer( drawer: new Drawer(
child: new Block( key: drawerKey,
scrollableKey: drawerKey, child: new ListView(
controller: scrollOffset,
children: new List<Widget>.generate(10, children: new List<Widget>.generate(10,
(int index) => new SizedBox(height: 100.0, child: new Text('D$index')) (int index) => new SizedBox(height: 100.0, child: new Text('D$index'))
) )
) )
), ),
body: new Block( body: new CustomScrollView(
padding: const EdgeInsets.only(top: appBarHeight), slivers: <Widget>[
children: new List<Widget>.generate(10, new SliverAppBar(
(int index) => new SizedBox(height: 100.0, child: new Text('B$index')) pinned: true,
), expandedHeight: appBarHeight,
title: new Text('Title'),
flexibleSpace: new FlexibleSpaceBar(title: new Text('Title')),
),
new SliverPadding(
padding: const EdgeInsets.only(top: appBarHeight),
child: new SliverList(
delegate: new SliverChildListDelegate(new List<Widget>.generate(
10, (int index) => new SizedBox(height: 100.0, child: new Text('B$index')),
)),
),
),
],
), ),
) )
) )
...@@ -117,86 +124,62 @@ void main() { ...@@ -117,86 +124,62 @@ void main() {
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(drawerKey.currentState.scrollOffset, equals(0)); expect(scrollOffset.offset, 0.0);
const double scrollDelta = 80.0; const double scrollDelta = 80.0;
await tester.scroll(find.byKey(drawerKey), const Offset(0.0, -scrollDelta)); await tester.scroll(find.byKey(drawerKey), const Offset(0.0, -scrollDelta));
await tester.pump(); await tester.pump();
expect(drawerKey.currentState.scrollOffset, equals(scrollDelta)); expect(scrollOffset.offset, scrollDelta);
RenderBox renderBox = tester.renderObject(find.byKey(appBarKey)); RenderBox renderBox = tester.renderObject(find.byType(AppBar));
expect(renderBox.size.height, equals(appBarHeight)); expect(renderBox.size.height, equals(appBarHeight));
}); });
testWidgets('Tapping the status bar scrolls to top on iOS', (WidgetTester tester) async { Widget _buildStatusBarTestApp(TargetPlatform platform) {
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>(); return new MaterialApp(
final Key appBarKey = new UniqueKey(); theme: new ThemeData(platform: platform),
home: new MediaQuery(
await tester.pumpWidget( data: const MediaQueryData(padding: const EdgeInsets.only(top: 25.0)), // status bar
new MaterialApp( child: new Scaffold(
theme: new ThemeData(platform: TargetPlatform.iOS), body: new CustomScrollView(
home: new MediaQuery( primary: true,
data: const MediaQueryData(padding: const EdgeInsets.only(top: 25.0)), // status bar slivers: <Widget>[
child: new Scaffold( new SliverAppBar(
scrollableKey: scrollableKey, title: new Text('Title')
appBar: new AppBar( ),
key: appBarKey, new SliverList(
title: new Text('Title') delegate: new SliverChildListDelegate(new List<Widget>.generate(
), 20, (int index) => new SizedBox(height: 100.0, child: new Text('$index')),
body: new Block( )),
scrollableKey: scrollableKey, ),
initialScrollOffset: 500.0, ],
children: new List<Widget>.generate(20, ),
(int index) => new SizedBox(height: 100.0, child: new Text('$index')) ),
) ),
)
)
)
)
); );
}
final ScrollableState scrollable = tester.state(find.byType(Scrollable)); testWidgets('Tapping the status bar scrolls to top on iOS', (WidgetTester tester) async {
expect(scrollable.scrollOffset, equals(500.0)); await tester.pumpWidget(_buildStatusBarTestApp(TargetPlatform.iOS));
final Scrollable2State scrollable = tester.state(find.byType(Scrollable2));
scrollable.position.jumpTo(500.0);
expect(scrollable.position.pixels, equals(500.0));
await tester.tapAt(const Point(100.0, 10.0)); await tester.tapAt(const Point(100.0, 10.0));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(scrollable.scrollOffset, equals(0.0)); expect(scrollable.position.pixels, equals(0.0));
}); });
testWidgets('Tapping the status bar does not scroll to top on Android', (WidgetTester tester) async { testWidgets('Tapping the status bar does not scroll to top on Android', (WidgetTester tester) async {
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>(); await tester.pumpWidget(_buildStatusBarTestApp(TargetPlatform.android));
final Key appBarKey = new UniqueKey(); final Scrollable2State scrollable = tester.state(find.byType(Scrollable2));
scrollable.position.jumpTo(500.0);
await tester.pumpWidget( expect(scrollable.position.pixels, equals(500.0));
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
home: new MediaQuery(
data: const MediaQueryData(padding: const EdgeInsets.only(top: 25.0)), // status bar
child: new Scaffold(
scrollableKey: scrollableKey,
appBar: new AppBar(
key: appBarKey,
title: new Text('Title')
),
body: new Block(
scrollableKey: scrollableKey,
initialScrollOffset: 500.0,
children: new List<Widget>.generate(20,
(int index) => new SizedBox(height: 100.0, child: new Text('$index'))
)
)
)
)
)
);
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
expect(scrollable.scrollOffset, equals(500.0));
await tester.tapAt(const Point(100.0, 10.0)); await tester.tapAt(const Point(100.0, 10.0));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(scrollable.scrollOffset, equals(500.0)); expect(scrollable.position.pixels, equals(500.0));
}); });
testWidgets('Bottom sheet cannot overlap app bar', (WidgetTester tester) async { testWidgets('Bottom sheet cannot overlap app bar', (WidgetTester tester) async {
......
...@@ -13,12 +13,22 @@ class SamplePage extends StatefulWidget { ...@@ -13,12 +13,22 @@ class SamplePage extends StatefulWidget {
} }
class SamplePageState extends State<SamplePage> { class SamplePageState extends State<SamplePage> {
ModalRoute<Null> _route;
Future<bool> _callback() async => willPopValue;
@override @override
void dependenciesChanged() { void dependenciesChanged() {
super.dependenciesChanged(); super.dependenciesChanged();
final ModalRoute<Null> route = ModalRoute.of(context); _route?.removeScopedWillPopCallback(_callback);
if (route.isCurrent) _route = ModalRoute.of(context);
route.addScopedWillPopCallback(() async => willPopValue); _route?.addScopedWillPopCallback(_callback);
}
@override
void dispose() {
super.dispose();
_route?.removeScopedWillPopCallback(_callback);
} }
@override @override
...@@ -87,6 +97,9 @@ void main() { ...@@ -87,6 +97,9 @@ void main() {
), ),
); );
expect(find.byTooltip('Back'), findsNothing);
expect(find.text('Sample Page'), findsNothing);
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
......
...@@ -189,8 +189,11 @@ class TestDelegate extends SliverPersistentHeaderDelegate { ...@@ -189,8 +189,11 @@ class TestDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => 200.0; double get maxExtent => 200.0;
@override @override
Widget build(BuildContext context, double shrinkOffset) { double get minExtent => 100.0;
return new Container(constraints: new BoxConstraints(minHeight: maxExtent / 2.0, maxHeight: maxExtent));
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(constraints: new BoxConstraints(minHeight: minExtent, maxHeight: maxExtent));
} }
@override @override
......
...@@ -184,8 +184,11 @@ class TestDelegate extends SliverPersistentHeaderDelegate { ...@@ -184,8 +184,11 @@ class TestDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => 200.0; double get maxExtent => 200.0;
@override @override
Widget build(BuildContext context, double shrinkOffset) { double get minExtent => 100.0;
return new Container(constraints: new BoxConstraints(minHeight: maxExtent / 2.0, maxHeight: maxExtent));
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(constraints: new BoxConstraints(minHeight: minExtent, maxHeight: maxExtent));
} }
@override @override
......
...@@ -74,7 +74,10 @@ class TestDelegate extends SliverPersistentHeaderDelegate { ...@@ -74,7 +74,10 @@ class TestDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => 200.0; double get maxExtent => 200.0;
@override @override
Widget build(BuildContext context, double shrinkOffset) { double get minExtent => 200.0;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(height: maxExtent); return new Container(height: maxExtent);
} }
......
...@@ -16,10 +16,13 @@ class TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate ...@@ -16,10 +16,13 @@ class TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate
double get maxExtent => _maxExtent; double get maxExtent => _maxExtent;
@override @override
Widget build(BuildContext context, double shrinkOffset) { double get minExtent => 16.0;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Column( return new Column(
children: <Widget>[ children: <Widget>[
new Container(height: 16.0), new Container(height: minExtent),
new Expanded(child: new Container()), new Expanded(child: new Container()),
], ],
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment