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> {
data: _kTheme.copyWith(platform: Theme.of(context).platform),
child: new Scaffold(
key: scaffoldKey,
scrollableKey: config.scrollableKey,
appBarBehavior: AppBarBehavior.under,
appBar: _buildAppBar(context, statusBarHeight),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.edit),
onPressed: () {
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) {
return new AppBar(
return new SliverAppBar(
pinned: true,
expandedHeight: _kAppBarHeight,
actions: <Widget>[
new IconButton(
......@@ -111,7 +114,7 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
tooltip: 'Search',
onPressed: () {
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('Not supported.')
content: new Text('Not supported.'),
));
},
),
......@@ -138,22 +141,25 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
}
Widget _buildBody(BuildContext context, double statusBarHeight) {
final EdgeInsets padding = new EdgeInsets.fromLTRB(8.0, 8.0 + _kAppBarHeight + statusBarHeight, 8.0, 8.0);
return new ScrollableGrid(
scrollableKey: config.scrollableKey,
delegate: new MaxTileWidthGridDelegate(
maxTileWidth: _kRecipePageMaxWidth,
rowSpacing: 8.0,
columnSpacing: 8.0,
padding: padding
final EdgeInsets padding = const EdgeInsets.all(8.0);
return new SliverPadding(
padding: padding,
child: new SliverGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: _kRecipePageMaxWidth,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
),
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 {
class _RecipePageState extends State<RecipePage> {
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 Object _disableHeroTransition = new Object();
double _getAppBarHeight(BuildContext context) => MediaQuery.of(context).size.height * 0.3;
@override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
scrollableKey: _scrollableKey,
appBarBehavior: AppBarBehavior.scroll,
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);
// 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.
final double appBarHeight = _getAppBarHeight(context);
final Size screenSize = MediaQuery.of(context).size;
final bool fullWidth = (screenSize.width < _kRecipePageMaxWidth);
final double appBarHeight = _getAppBarHeight(context);
return new Stack(
children: <Widget>[
new Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
height: appBarHeight + _kFabHalfSize,
child: new Hero(
tag: config.recipe.imagePath,
child: new Image.asset(
config.recipe.imagePath,
fit: fullWidth ? ImageFit.fitWidth : ImageFit.cover,
final bool isFavorite = _favoriteRecipes.contains(config.recipe);
return new Scaffold(
key: _scaffoldKey,
body: new Stack(
children: <Widget>[
new Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
height: appBarHeight + _kFabHalfSize,
child: new Hero(
tag: config.recipe.imagePath,
child: new Image.asset(
config.recipe.imagePath,
fit: fullWidth ? ImageFit.fitWidth : ImageFit.cover,
),
),
),
),
new ClampOverscrolls(
edge: ScrollableEdge.both,
child: new ScrollableViewport(
scrollableKey: _scrollableKey,
child: new RepaintBoundary(
child: new Padding(
padding: new EdgeInsets.only(top: appBarHeight),
new CustomScrollView(
slivers: <Widget>[
new SliverAppBar(
expandedHeight: appBarHeight - _kFabHalfSize,
backgroundColor: Colors.transparent,
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)],
),
),
),
),
),
new SliverToBoxAdapter(
child: new Stack(
children: <Widget>[
new Container(
padding: new EdgeInsets.only(top: _kFabHalfSize),
padding: const EdgeInsets.only(top: _kFabHalfSize),
width: fullWidth ? null : _kRecipePageMaxWidth,
child: new RecipeSheet(recipe: config.recipe),
),
......@@ -386,12 +377,12 @@ class _RecipePageState extends State<RecipePage> {
),
),
],
),
)
),
),
],
),
),
],
],
),
);
}
......
......@@ -40,8 +40,8 @@ class ShrinePage extends StatefulWidget {
class ShrinePageState extends State<ShrinePage> {
int _appBarElevation = 0;
bool _handleScrollNotification(ScrollNotification notification) {
int elevation = notification.scrollable.scrollOffset <= 0.0 ? 0 : 1;
bool _handleScrollNotification(ScrollNotification2 notification) {
int elevation = notification.metrics.extentBefore <= 0.0 ? 0 : 1;
if (elevation != _appBarElevation) {
setState(() {
_appBarElevation = elevation;
......@@ -141,7 +141,7 @@ class ShrinePageState extends State<ShrinePage> {
]
),
floatingActionButton: config.floatingActionButton,
body: new NotificationListener<ScrollNotification>(
body: new NotificationListener<ScrollNotification2>(
onNotification: _handleScrollNotification,
child: config.body
)
......
......@@ -41,16 +41,13 @@ class _AppBarBackground extends StatelessWidget {
@override
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(
animation: effectiveAnimation,
animation: animation,
builder: (BuildContext context, Widget child) {
return new Stack(
children: _kBackgroundLayers.map((_BackgroundLayer layer) {
return new Positioned(
top: -layer.parallaxTween.evaluate(effectiveAnimation),
top: -layer.parallaxTween.evaluate(animation),
left: 0.0,
right: 0.0,
bottom: 0.0,
......@@ -107,7 +104,6 @@ class GalleryHome extends StatefulWidget {
class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin {
static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
static final GlobalKey<ScrollableState> _scrollableKey = new GlobalKey<ScrollableState>();
AnimationController _controller;
......@@ -153,10 +149,8 @@ class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderState
@override
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
Widget home = new Scaffold(
key: _scaffoldKey,
scrollableKey: _scrollableKey,
drawer: new GalleryDrawer(
useLightTheme: config.useLightTheme,
onThemeChanged: config.onThemeChanged,
......@@ -169,27 +163,19 @@ class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderState
onPlatformChanged: config.onPlatformChanged,
onSendFeedback: config.onSendFeedback,
),
appBar: new AppBar(
expandedHeight: _kFlexibleSpaceMaxHeight,
flexibleSpace: new FlexibleSpaceBar(
title: new Text('Flutter Gallery'),
background: new Builder(
builder: (BuildContext context) {
return new _AppBarBackground(
animation: _scaffoldKey.currentState.appBarAnimation
);
}
)
)
),
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()
body: new CustomScrollView(
slivers: <Widget>[
new SliverAppBar(
pinned: true,
expandedHeight: _kFlexibleSpaceMaxHeight,
flexibleSpace: new FlexibleSpaceBar(
title: new Text('Flutter Gallery'),
// 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())),
],
)
);
......
......@@ -21,11 +21,12 @@ void main() {
// Scroll the Buttons demo into view so that a tap will succeed
final Point allDemosOrigin = tester.getTopRight(find.text('Demos'));
final Point buttonsDemoOrigin = tester.getTopRight(find.text('Buttons'));
final double scrollDelta = buttonsDemoOrigin.y - allDemosOrigin.y;
await tester.scrollAt(allDemosOrigin, new Offset(0.0, -scrollDelta));
await tester.pump(); // start the scroll
await tester.pump(const Duration(seconds: 1));
final Finder button = find.text('Buttons');
while (button.evaluate().isEmpty) {
await tester.scrollAt(allDemosOrigin, new Offset(0.0, -100.0));
await tester.pump(); // start the scroll
await tester.pump(const Duration(seconds: 1));
}
// Launch the buttons demo and then prove that showing the example
// code dialog does not crash.
......
......@@ -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.
expect(find.text(kCaption), findsNothing);
await tester.pump(const Duration(seconds: 1)); // Leave the demo on the screen briefly for manual testing.
// Go back
Finder backButton = find.byTooltip('Back');
......@@ -61,22 +62,12 @@ Future<Null> runSmokeTest(WidgetTester tester) async {
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) {
final double y = tester.getTopRight(findGalleryItemByRouteName(tester, routeName)).y;
scrollDeltas.add(previousY - y);
previousY = y;
}
// 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];
Finder finder = findGalleryItemByRouteName(tester, routeName);
Scrollable2.ensureVisible(tester.element(finder), alignment: 0.5);
await tester.pump();
await tester.pumpUntilNoTransientCallbacks();
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');
}
......
......@@ -4,33 +4,32 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'app_bar.dart';
import 'constants.dart';
import 'scaffold.dart';
import 'theme.dart';
/// The part of a material design [AppBar] that expands and collapses.
///
/// Most commonly used in in the [AppBar.flexibleSpace] field, a flexible space
/// bar expands and contracts as the app scrolls so that the [AppBar] reaches
/// from the top of the app to the top of the scrolling contents of the app.
/// Most commonly used in in the [SliverAppBar.flexibleSpace] field, a flexible
/// space bar expands and contracts as the app scrolls so that the [AppBar]
/// 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
/// [Scaffold] coordinates the scrolling effect between the flexible space and
/// its body.
/// The widget that sizes the [AppBar] must wrap it in the widget returned by
/// [FlexibleSpaceBar.createSettings], to convey sizing information down to the
/// [FlexibleSpaceBar].
///
/// See also:
///
/// * [AppBar]
/// * [Scaffold]
/// * [SliverAppBar], which implements the expanding and contracting.
/// * [AppBar], which is used by [SliverAppBar].
/// * <https://material.google.com/patterns/scrolling-techniques.html>
class FlexibleSpaceBar extends StatefulWidget {
/// Creates a flexible space bar.
///
/// Most commonly used in the [AppBar.flexibleSpace] field. Requires one of
/// its ancestors to be a [Scaffold] widget.
/// Most commonly used in the [AppBar.flexibleSpace] field.
FlexibleSpaceBar({
Key key,
this.title,
......@@ -53,6 +52,23 @@ class FlexibleSpaceBar extends StatefulWidget {
/// Defaults to being adapted to the current [TargetPlatform].
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
_FlexibleSpaceBarState createState() => new _FlexibleSpaceBarState();
}
......@@ -72,34 +88,32 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
return null;
}
Widget _buildContent(BuildContext context, BoxConstraints constraints) {
final Size size = constraints.biggest;
final double statusBarHeight = MediaQuery.of(context).padding.top;
@override
Widget build(BuildContext context) {
_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 maxHeight = statusBarHeight + AppBar.getExpandedHeightFor(context);
final double minHeight = statusBarHeight + kToolbarHeight;
final double deltaHeight = maxHeight - minHeight;
final double deltaExtent = settings.maxExtent - settings.minExtent;
// 0.0 -> Expanded
// 1.0 -> Collapsed to toolbar
final double t = (1.0 - (currentHeight - minHeight) / deltaHeight).clamp(0.0, 1.0);
final List<Widget> children = <Widget>[];
final double t = (1.0 - (settings.currentExtent - settings.minExtent) / (deltaExtent)).clamp(0.0, 1.0);
// background image
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;
assert(fadeStart <= fadeEnd);
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) {
children.add(new Positioned(
top: -parallax,
left: 0.0,
right: 0.0,
height: maxHeight,
height: settings.maxExtent,
child: new Opacity(
opacity: opacity,
child: config.background
......@@ -110,7 +124,7 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
if (config.title != null) {
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) {
TextStyle titleStyle = theme.primaryTextTheme.title;
titleStyle = titleStyle.copyWith(
......@@ -140,9 +154,28 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
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
Widget build(BuildContext context) {
return new LayoutBuilder(builder: _buildContent);
bool updateShouldNotify(_FlexibleSpaceBarSettings oldWidget) {
return toolbarOpacity != oldWidget.toolbarOpacity
|| minExtent != oldWidget.minExtent
|| maxExtent != oldWidget.maxExtent
|| currentExtent != oldWidget.currentExtent;
}
}
......@@ -473,6 +473,7 @@ class SemanticsNode extends AbstractNode {
owner._nodes.remove(id);
owner._detachedNodes.add(this);
super.detach();
assert(owner == null);
if (_children != null) {
for (SemanticsNode child in _children) {
// The list of children may be stale and may contain nodes that have
......@@ -481,6 +482,10 @@ class SemanticsNode extends AbstractNode {
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;
......@@ -563,7 +568,7 @@ class SemanticsNode extends AbstractNode {
StringBuffer buffer = new StringBuffer();
buffer.write('$runtimeType($id');
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)
buffer.write(' (leaf merge)');
buffer.write('; $rect');
......@@ -624,19 +629,13 @@ class SemanticsOwner extends ChangeNotifier {
/// Update the semantics using [ui.window.updateSemantics].
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)
return;
List<SemanticsNode> visitedNodes = <SemanticsNode>[];
while (_dirtyNodes.isNotEmpty) {
List<SemanticsNode> localDirtyNodes = _dirtyNodes.toList();
List<SemanticsNode> localDirtyNodes = _dirtyNodes.where((SemanticsNode node) => !_detachedNodes.contains(node)).toList();
_dirtyNodes.clear();
_detachedNodes.clear();
localDirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
visitedNodes.addAll(localDirtyNodes);
for (SemanticsNode node in localDirtyNodes) {
......@@ -759,4 +758,7 @@ class SemanticsOwner extends ChangeNotifier {
SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action);
handler?.performAction(action);
}
@override
String toString() => '$runtimeType@$hashCode';
}
......@@ -20,16 +20,19 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
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;
/// 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,
/// or if the object was newly created), then the returned value will be stale
/// until [layoutChild] has been called.
@protected
double get minExtent => _minExtent;
double _minExtent;
/// If this is based on the intrinsic dimensions of the child, the child
/// should be measured during [updateChild] and the value cached and returned
/// here. The [updateChild] method will automatically be invoked any time the
/// child changes its intrinsic dimensions.
double get minExtent;
@protected
double get childExtent {
......@@ -46,69 +49,65 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
return null;
}
@protected
double _getChildIntrinsicExtent() {
if (child == null)
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;
}
bool _needsUpdateChild = true;
double _lastShrinkOffset = 0.0;
bool _lastOverlapsContent = false;
/// The last value that we passed to updateChild().
double _lastShrinkOffset;
/// Called during layout if the shrink offset has changed.
/// Update the child render object if necessary.
///
/// During this callback, the [child] can be set, mutated, or replaced.
@protected
void updateChild(double shrinkOffset) { }
/// Flag the current child as stale and needing updating even if the shrink
/// offset has not changed.
/// Called before the first layout, any time [markNeedsLayout] is called, and
/// any time the scroll offset changes. The `shrinkOffset` is the difference
/// between the [maxExtent] and the current size. Zero means the header is
/// fully expanded, any greater number up to [maxExtent] means that the header
/// has been scrolled by that much. The `overlapsContent` argument is true if
/// 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
/// given the same `shrinkOffset` as the last time it was called.
/// When this method is called by [layoutChild], the [child] can be set,
/// mutated, or replaced. (It should not be called outside [layoutChild].)
///
/// This must be implemented by [RenderSliverPersistentHeader] subclasses such
/// that the next layout after this call will result in [updateChild] being
/// called.
/// Any time this method would mutate the child, call [markNeedsLayout].
@protected
void markNeedsUpdate() {
markNeedsLayout();
_lastShrinkOffset = null;
void updateChild(double shrinkOffset, bool overlapsContent) { }
@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);
final double shrinkOffset = math.min(scrollOffset, maxExtent);
if (shrinkOffset != _lastShrinkOffset) {
if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
updateChild(shrinkOffset);
_minExtent = _getChildIntrinsicExtent();
updateChild(shrinkOffset, overlapsContent);
});
_lastShrinkOffset = shrinkOffset;
_lastOverlapsContent = overlapsContent;
_needsUpdateChild = false;
}
assert(_minExtent != null);
assert(minExtent != null);
assert(() {
if (_minExtent <= maxExtent)
if (minExtent <= maxExtent)
return true;
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 child was updated with shrink offset: ${shrinkOffset.toStringAsFixed(1)}\n'
'The actual measured intrinsic extent of the child was: ${_minExtent.toStringAsFixed(1)}\n'
'The specified minExtent was: ${minExtent.toStringAsFixed(1)}\n'
);
});
child?.layout(
constraints.asBoxConstraints(maxExtent: math.max(_minExtent, maxExtent - shrinkOffset)),
constraints.asBoxConstraints(maxExtent: math.max(minExtent, maxExtent - shrinkOffset)),
parentUsesSize: true,
);
}
......@@ -237,7 +236,7 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
@override
void performLayout() {
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(
scrollExtent: maxExtent,
paintExtent: math.min(constraints.overlap + childExtent, constraints.remainingPaintExtent),
......@@ -285,7 +284,7 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
} else {
_effectiveScrollOffset = constraints.scrollOffset;
}
layoutChild(_effectiveScrollOffset, maxExtent);
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(
......
......@@ -155,9 +155,7 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
assert(mounted);
NavigatorState navigator = _navigator.currentState;
assert(navigator != null);
if (!await navigator.willPop())
return true;
return mounted && navigator.pop();
return await navigator.maybePop();
}
@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> {
assert(entry._owner == null);
entry._owner = this;
_localHistory ??= <LocalHistoryEntry>[];
final bool wasEmpty = _localHistory.isEmpty;
_localHistory.add(entry);
if (wasEmpty)
changedInternalState();
}
/// Remove a local history entry from this route.
......@@ -308,6 +311,8 @@ abstract class LocalHistoryRoute<T> extends Route<T> {
_localHistory.remove(entry);
entry._owner = null;
entry._notifyRemoved();
if (_localHistory.isEmpty)
changedInternalState();
}
@override
......@@ -317,6 +322,8 @@ abstract class LocalHistoryRoute<T> extends Route<T> {
assert(entry._owner == this);
entry._owner = null;
entry._notifyRemoved();
if (_localHistory.isEmpty)
changedInternalState();
return false;
}
return super.didPop(result);
......@@ -326,26 +333,40 @@ abstract class LocalHistoryRoute<T> extends Route<T> {
bool get willHandlePopInternally {
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 {
_ModalScopeStatus({
Key key,
this.isCurrent,
this.route,
@required this.isCurrent,
@required this.canPop,
@required this.route,
@required Widget child
}) : super(key: key, child: child) {
assert(isCurrent != null);
assert(canPop != null);
assert(route != null);
assert(child != null);
}
final bool isCurrent;
final bool canPop;
final Route<dynamic> route;
@override
bool updateShouldNotify(_ModalScopeStatus old) {
return isCurrent != old.isCurrent ||
canPop != old.canPop ||
route != old.route;
}
......@@ -353,6 +374,8 @@ class _ModalScopeStatus extends InheritedWidget {
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('${isCurrent ? "active" : "inactive"}');
if (canPop)
description.add('can pop');
}
}
......@@ -396,7 +419,6 @@ class _ModalScopeState extends State<_ModalScope> {
void dispose() {
config.route.animation?.removeStatusListener(_animationStatusChanged);
config.route.forwardAnimation?.removeStatusListener(_animationStatusChanged);
willPopCallbacks = null;
super.dispose();
}
......@@ -439,13 +461,14 @@ class _ModalScopeState extends State<_ModalScope> {
child: new _ModalScopeStatus(
route: config.route,
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
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
final GlobalKey<_ModalScopeState> _scopeKey = new GlobalKey<_ModalScopeState>();
......
......@@ -22,6 +22,14 @@ class ScrollController {
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 {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
......
......@@ -5,8 +5,9 @@
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import 'framework.dart';
import 'basic.dart';
import 'framework.dart';
import 'primary_scroll_controller.dart';
import 'scroll_controller.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
......@@ -20,11 +21,16 @@ abstract class ScrollView extends StatelessWidget {
this.scrollDirection: Axis.vertical,
this.reverse: false,
this.controller,
this.primary: false,
this.physics,
this.shrinkWrap: false,
}) : super(key: key) {
assert(reverse != 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;
......@@ -33,6 +39,8 @@ abstract class ScrollView extends StatelessWidget {
final ScrollController controller;
final bool primary;
final ScrollPhysics physics;
final bool shrinkWrap;
......@@ -58,7 +66,7 @@ abstract class ScrollView extends StatelessWidget {
AxisDirection axisDirection = getDirection(context);
return new Scrollable2(
axisDirection: axisDirection,
controller: controller,
controller: controller ?? (primary ? PrimaryScrollController.of(context) : null),
physics: physics,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
if (shrinkWrap) {
......@@ -82,6 +90,14 @@ abstract class ScrollView extends StatelessWidget {
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
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)
description.add('shrink-wrapping');
}
......@@ -93,6 +109,7 @@ class CustomScrollView extends ScrollView {
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary: false,
ScrollPhysics physics,
bool shrinkWrap: false,
this.slivers: const <Widget>[],
......@@ -101,6 +118,7 @@ class CustomScrollView extends ScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
);
......@@ -117,6 +135,7 @@ abstract class BoxScrollView extends ScrollView {
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary: false,
ScrollPhysics physics,
bool shrinkWrap: false,
this.padding,
......@@ -125,6 +144,7 @@ abstract class BoxScrollView extends ScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
);
......@@ -164,6 +184,7 @@ class ListView extends BoxScrollView {
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary: false,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsets padding,
......@@ -174,6 +195,7 @@ class ListView extends BoxScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
......@@ -184,6 +206,7 @@ class ListView extends BoxScrollView {
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary: false,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsets padding,
......@@ -195,6 +218,7 @@ class ListView extends BoxScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
......@@ -205,6 +229,7 @@ class ListView extends BoxScrollView {
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary: false,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsets padding,
......@@ -215,6 +240,7 @@ class ListView extends BoxScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
......@@ -259,6 +285,7 @@ class GridView extends BoxScrollView {
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary: false,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsets padding,
......@@ -269,6 +296,7 @@ class GridView extends BoxScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
......@@ -281,6 +309,7 @@ class GridView extends BoxScrollView {
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary: false,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsets padding,
......@@ -291,6 +320,7 @@ class GridView extends BoxScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
......@@ -304,6 +334,7 @@ class GridView extends BoxScrollView {
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary: false,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsets padding,
......@@ -323,6 +354,7 @@ class GridView extends BoxScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
......@@ -333,6 +365,7 @@ class GridView extends BoxScrollView {
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary: false,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsets padding,
......@@ -352,6 +385,7 @@ class GridView extends BoxScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
......
......@@ -86,7 +86,12 @@ class Scrollable2 extends StatefulWidget {
Scrollable2State scrollable = Scrollable2.of(context);
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;
scrollable = Scrollable2.of(context);
}
......
......@@ -12,7 +12,9 @@ abstract class SliverPersistentHeaderDelegate {
/// const constructors so that they can be used in const expressions.
const SliverPersistentHeaderDelegate();
Widget build(BuildContext context, double shrinkOffset);
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);
double get minExtent;
double get maxExtent;
......@@ -101,9 +103,9 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
Element child;
void _build(double shrinkOffset) {
void _build(double shrinkOffset, bool overlapsContent) {
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
abstract class _RenderSliverPersistentHeaderForWidgetsMixin implements RenderSliverPersistentHeader {
_SliverPersistentHeaderElement _element;
@override
double get minExtent => _element.widget.delegate.minExtent;
@override
double get maxExtent => _element.widget.delegate.maxExtent;
@override
void updateChild(double shrinkOffset) {
void updateChild(double shrinkOffset, bool overlapsContent) {
assert(_element != null);
_element._build(shrinkOffset);
_element._build(shrinkOffset, overlapsContent);
}
@protected
void triggerRebuild() {
markNeedsUpdate();
markNeedsLayout();
}
}
......
......@@ -43,6 +43,7 @@ export 'src/widgets/page_view.dart';
export 'src/widgets/pages.dart';
export 'src/widgets/performance_overlay.dart';
export 'src/widgets/placeholder.dart';
export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scroll_behavior.dart';
......
......@@ -80,32 +80,39 @@ void main() {
testWidgets('Drawer scrolling', (WidgetTester tester) async {
GlobalKey<ScrollableState<Scrollable>> drawerKey =
new GlobalKey<ScrollableState<Scrollable>>(debugLabel: 'drawer');
Key appBarKey = new Key('appBar');
const double appBarHeight = 256.0;
ScrollController scrollOffset = new ScrollController();
await tester.pumpWidget(
new MaterialApp(
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(
child: new Block(
scrollableKey: drawerKey,
key: drawerKey,
child: new ListView(
controller: scrollOffset,
children: new List<Widget>.generate(10,
(int index) => new SizedBox(height: 100.0, child: new Text('D$index'))
)
)
),
body: new Block(
padding: const EdgeInsets.only(top: appBarHeight),
children: new List<Widget>.generate(10,
(int index) => new SizedBox(height: 100.0, child: new Text('B$index'))
),
body: new CustomScrollView(
slivers: <Widget>[
new SliverAppBar(
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() {
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(drawerKey.currentState.scrollOffset, equals(0));
expect(scrollOffset.offset, 0.0);
const double scrollDelta = 80.0;
await tester.scroll(find.byKey(drawerKey), const Offset(0.0, -scrollDelta));
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));
});
testWidgets('Tapping the status bar scrolls to top on iOS', (WidgetTester tester) async {
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
final Key appBarKey = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.iOS),
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'))
)
)
)
)
)
Widget _buildStatusBarTestApp(TargetPlatform platform) {
return new MaterialApp(
theme: new ThemeData(platform: platform),
home: new MediaQuery(
data: const MediaQueryData(padding: const EdgeInsets.only(top: 25.0)), // status bar
child: new Scaffold(
body: new CustomScrollView(
primary: true,
slivers: <Widget>[
new SliverAppBar(
title: new Text('Title')
),
new SliverList(
delegate: new SliverChildListDelegate(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));
testWidgets('Tapping the status bar scrolls to top on iOS', (WidgetTester tester) async {
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.pump();
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 {
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
final Key appBarKey = new UniqueKey();
await tester.pumpWidget(
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.pumpWidget(_buildStatusBarTestApp(TargetPlatform.android));
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.pump();
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 {
......
......@@ -13,12 +13,22 @@ class SamplePage extends StatefulWidget {
}
class SamplePageState extends State<SamplePage> {
ModalRoute<Null> _route;
Future<bool> _callback() async => willPopValue;
@override
void dependenciesChanged() {
super.dependenciesChanged();
final ModalRoute<Null> route = ModalRoute.of(context);
if (route.isCurrent)
route.addScopedWillPopCallback(() async => willPopValue);
_route?.removeScopedWillPopCallback(_callback);
_route = ModalRoute.of(context);
_route?.addScopedWillPopCallback(_callback);
}
@override
void dispose() {
super.dispose();
_route?.removeScopedWillPopCallback(_callback);
}
@override
......@@ -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.pump();
await tester.pump(const Duration(seconds: 1));
......
......@@ -189,8 +189,11 @@ class TestDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => 200.0;
@override
Widget build(BuildContext context, double shrinkOffset) {
return new Container(constraints: new BoxConstraints(minHeight: maxExtent / 2.0, maxHeight: maxExtent));
double get minExtent => 100.0;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(constraints: new BoxConstraints(minHeight: minExtent, maxHeight: maxExtent));
}
@override
......
......@@ -184,8 +184,11 @@ class TestDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => 200.0;
@override
Widget build(BuildContext context, double shrinkOffset) {
return new Container(constraints: new BoxConstraints(minHeight: maxExtent / 2.0, maxHeight: maxExtent));
double get minExtent => 100.0;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(constraints: new BoxConstraints(minHeight: minExtent, maxHeight: maxExtent));
}
@override
......
......@@ -74,7 +74,10 @@ class TestDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => 200.0;
@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);
}
......
......@@ -16,10 +16,13 @@ class TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate
double get maxExtent => _maxExtent;
@override
Widget build(BuildContext context, double shrinkOffset) {
double get minExtent => 16.0;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Column(
children: <Widget>[
new Container(height: 16.0),
new Container(height: minExtent),
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