From 23d7a23e482ec0e125a2c8857beb29012b8bf29b Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Thu, 10 Mar 2016 16:44:08 -0800 Subject: [PATCH] Added AppBarBehavior.under, etc --- .../lib/demo/flexible_space_demo.dart | 31 +++++-- .../material_gallery/lib/gallery/section.dart | 2 +- .../flutter/lib/src/material/app_bar.dart | 2 +- .../flutter/lib/src/material/constants.dart | 2 +- .../lib/src/material/flexible_space_bar.dart | 4 +- .../flutter/lib/src/material/scaffold.dart | 90 +++++++++++++------ .../lib/src/widgets/scrollable_list.dart | 2 +- 7 files changed, 93 insertions(+), 40 deletions(-) diff --git a/examples/material_gallery/lib/demo/flexible_space_demo.dart b/examples/material_gallery/lib/demo/flexible_space_demo.dart index 0282dbef31..ec7eab3793 100644 --- a/examples/material_gallery/lib/demo/flexible_space_demo.dart +++ b/examples/material_gallery/lib/demo/flexible_space_demo.dart @@ -72,8 +72,10 @@ class FlexibleSpaceDemo extends StatefulWidget { } class FlexibleSpaceDemoState extends State { + final GlobalKey scaffoldKey = new GlobalKey(); final double appBarHeight = 256.0; final Key scrollableKey = new UniqueKey(); + AppBarBehavior _appBarBehavior = AppBarBehavior.scroll; Widget build(BuildContext context) { return new Theme( @@ -82,18 +84,37 @@ class FlexibleSpaceDemoState extends State { primarySwatch: Colors.indigo ), child: new Scaffold( + key: scaffoldKey, appBarHeight: appBarHeight, scrollableKey: scrollableKey, - appBarBehavior: AppBarBehavior.scroll, + appBarBehavior: _appBarBehavior, appBar: new AppBar( actions: [ new IconButton( icon: Icons.create, - tooltip: 'Search' + tooltip: 'Search', + onPressed: () { + scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text('Not supported.') + )); + } ), - new IconButton( - icon: Icons.more_vert, - tooltip: 'Show menu' + new PopupMenuButton( + onSelected: (AppBarBehavior value) { + setState(() { + _appBarBehavior = value; + }); + }, + items: >[ + new PopupMenuItem( + value: AppBarBehavior.scroll, + child: new Text('AppBar scrolls away') + ), + new PopupMenuItem( + value: AppBarBehavior.under, + child: new Text('AppBar stays put') + ) + ] ) ], flexibleSpace: (BuildContext context) { diff --git a/examples/material_gallery/lib/gallery/section.dart b/examples/material_gallery/lib/gallery/section.dart index c1060f11d7..2670c2d271 100644 --- a/examples/material_gallery/lib/gallery/section.dart +++ b/examples/material_gallery/lib/gallery/section.dart @@ -37,7 +37,7 @@ class GallerySection extends StatelessWidget { data: theme, child: new Scaffold( appBarHeight: appBarHeight, - appBarBehavior: AppBarBehavior.scroll, + appBarBehavior: AppBarBehavior.under, scrollableKey: scrollableKey, appBar: new AppBar( flexibleSpace: (BuildContext context) => new FlexibleSpaceBar(title: new Text(title)) diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index d478d6e576..596bcd83f7 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -117,7 +117,7 @@ class AppBar extends StatelessWidget { // If the toolBar's height shrinks below toolBarHeight, it will be clipped and bottom // justified. This is so that the toolbar appears to move upwards as its height is reduced. - final double toolBarHeight = kAppBarHeight + combinedPadding.top + combinedPadding.bottom; + final double toolBarHeight = kToolBarHeight + combinedPadding.top + combinedPadding.bottom; final Widget toolBar = new ConstrainedBox( constraints: new BoxConstraints(maxHeight: toolBarHeight), child: new Padding( diff --git a/packages/flutter/lib/src/material/constants.dart b/packages/flutter/lib/src/material/constants.dart index 93f37f0e76..f56f5068d8 100644 --- a/packages/flutter/lib/src/material/constants.dart +++ b/packages/flutter/lib/src/material/constants.dart @@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart'; // Mobile Landscape: 48dp // Mobile Portrait: 56dp // Tablet/Desktop: 64dp -const double kAppBarHeight = 56.0; +const double kToolBarHeight = 56.0; const double kExtendedAppBarHeight = 128.0; const double kTextTabBarHeight = 48.0; diff --git a/packages/flutter/lib/src/material/flexible_space_bar.dart b/packages/flutter/lib/src/material/flexible_space_bar.dart index b9d44081d1..f61b63e810 100644 --- a/packages/flutter/lib/src/material/flexible_space_bar.dart +++ b/packages/flutter/lib/src/material/flexible_space_bar.dart @@ -27,7 +27,7 @@ class _FlexibleSpaceBarState extends State { final double appBarHeight = Scaffold.of(context).appBarHeight; final Animation animation = Scaffold.of(context).appBarAnimation; final EdgeInsets toolBarPadding = MediaQuery.of(context)?.padding ?? EdgeInsets.zero; - final double toolBarHeight = kAppBarHeight + toolBarPadding.top; + final double toolBarHeight = kToolBarHeight + toolBarPadding.top; final List children = []; // background image @@ -63,7 +63,7 @@ class _FlexibleSpaceBarState extends State { color: titleStyle.color.withAlpha(new Tween(begin: 255.0, end: 0.0).evaluate(opacityCurve).toInt()) ); final double yAlignStart = 1.0; - final double yAlignEnd = (toolBarPadding.top + kAppBarHeight / 2.0) / toolBarHeight; + final double yAlignEnd = (toolBarPadding.top + kToolBarHeight / 2.0) / toolBarHeight; final double scaleAndAlignEnd = (appBarHeight - toolBarHeight) / appBarHeight; final CurvedAnimation scaleAndAlignCurve = new CurvedAnimation( parent: animation, diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 8cb699da4d..954d3decae 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -20,9 +20,25 @@ import 'snack_bar.dart'; const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 400); +/// The Scaffold's appbar is the toolbar, tabbar, and the "flexible space" that's +/// stacked behind them. The Scaffold's appBarBehavior defines how the appbar +/// responds to scrolling the application. enum AppBarBehavior { + /// The tool bar's layout does not respond to scrolling. anchor, + + /// The tool bar's appearance and layout depend on the scrollOffset of the + /// Scrollable identified by the Scaffold's scrollableKey. With the scrollOffset + /// at 0.0, scrolling downwards causes the toolbar's flexible space to shrink, + /// and then the entire toolbar fade outs and scrolls off the top of the screen. + /// Scrolling upwards always causes the toolbar to reappear. scroll, + + /// The tool bar's appearance and layout depend on the scrollOffset of the + /// Scrollable identified by the Scaffold's scrollableKey. With the scrollOffset + /// at 0.0, Scrolling downwards causes the toolbar's flexible space to shrink. + /// Other than that, the toolbar remains anchored at the top. + under, } enum _ScaffoldSlot { @@ -192,7 +208,7 @@ class Scaffold extends StatefulWidget { this.appBarHeight }) : super(key: key) { assert((appBarBehavior == AppBarBehavior.scroll) ? scrollableKey != null : true); - assert((appBarBehavior == AppBarBehavior.scroll) ? appBarHeight != null && appBarHeight > kAppBarHeight : true); + assert((appBarBehavior == AppBarBehavior.scroll) ? appBarHeight != null && appBarHeight > kToolBarHeight : true); } final AppBar appBar; @@ -430,36 +446,53 @@ class ScaffoldState extends State { )); } + Widget _buildAnchoredAppBar(double toolBarHeight, EdgeInsets toolBarPadding) { + // Drive _appBarController to the point where the flexible space has disappeared. + _appBarController.value = (appBarHeight - toolBarHeight) / appBarHeight; + return new SizedBox( + height: toolBarHeight, + child: _getModifiedAppBar(padding: toolBarPadding) + ); + } + Widget _buildScrollableAppBar(BuildContext context) { - final EdgeInsets appBarPadding = MediaQuery.of(context)?.padding ?? EdgeInsets.zero; - final double appBarHeight = kAppBarHeight + appBarPadding.top; + final EdgeInsets toolBarPadding = MediaQuery.of(context)?.padding ?? EdgeInsets.zero; + final double toolBarHeight = kToolBarHeight + toolBarPadding.top; Widget appBar; - if (_scrollOffset <= appBarHeight && _scrollOffset >= appBarHeight - appBarHeight) { - // scrolled to the top, only the app bar is (partially) visible - final double height = math.max(_floatingAppBarHeight, appBarHeight - _scrollOffset); - final double opacity = _appBarOpacity(1.0 - ((appBarHeight - height) / appBarHeight)); - _appBarController.value = (appBarHeight - height) / appBarHeight; - appBar = new SizedBox( - height: height, - child: _getModifiedAppBar(padding: appBarPadding, foregroundOpacity: opacity) - ); + if (_scrollOffset <= appBarHeight && _scrollOffset >= appBarHeight - toolBarHeight) { + // scrolled to the top, only the toolbar is (partially) visible. + if (config.appBarBehavior == AppBarBehavior.under) { + appBar = _buildAnchoredAppBar(toolBarHeight, toolBarPadding); + } else { + final double height = math.max(_floatingAppBarHeight, appBarHeight - _scrollOffset); + final double opacity = _appBarOpacity(1.0 - ((toolBarHeight - height) / toolBarHeight)); + _appBarController.value = (appBarHeight - height) / appBarHeight; + appBar = new SizedBox( + height: height, + child: _getModifiedAppBar(padding: toolBarPadding, foregroundOpacity: opacity) + ); + } } else if (_scrollOffset > appBarHeight) { - // scrolled down, show the "floating" app bar - _floatingAppBarHeight = (_floatingAppBarHeight + _scrollOffsetDelta).clamp(0.0, appBarHeight); - final double appBarOpacity = _appBarOpacity(_floatingAppBarHeight / appBarHeight); - _appBarController.value = (appBarHeight - _floatingAppBarHeight) / appBarHeight; - appBar = new SizedBox( - height: _floatingAppBarHeight, - child: _getModifiedAppBar(padding: appBarPadding, foregroundOpacity: appBarOpacity) - ); + // scrolled past the entire app bar, maybe show the "floating" toolbar. + if (config.appBarBehavior == AppBarBehavior.under) { + appBar = _buildAnchoredAppBar(toolBarHeight, toolBarPadding); + } else { + _floatingAppBarHeight = (_floatingAppBarHeight + _scrollOffsetDelta).clamp(0.0, toolBarHeight); + final double toolBarOpacity = _appBarOpacity(_floatingAppBarHeight / toolBarHeight); + _appBarController.value = (appBarHeight - _floatingAppBarHeight) / appBarHeight; + appBar = new SizedBox( + height: _floatingAppBarHeight, + child: _getModifiedAppBar(padding: toolBarPadding, foregroundOpacity: toolBarOpacity) + ); + } } else { // _scrollOffset < appBarHeight - appBarHeight, scrolled to the top, flexible space is visible final double height = appBarHeight - _scrollOffset.clamp(0.0, appBarHeight); _appBarController.value = (appBarHeight - height) / appBarHeight; appBar = new SizedBox( height: height, - child: _getModifiedAppBar(padding: appBarPadding, elevation: 0) + child: _getModifiedAppBar(padding: toolBarPadding, elevation: 0) ); _floatingAppBarHeight = 0.0; } @@ -468,10 +501,10 @@ class ScaffoldState extends State { } Widget build(BuildContext context) { - EdgeInsets padding = MediaQuery.of(context)?.padding ?? EdgeInsets.zero; + final EdgeInsets padding = MediaQuery.of(context)?.padding ?? EdgeInsets.zero; if (_snackBars.length > 0) { - ModalRoute route = ModalRoute.of(context); + final ModalRoute route = ModalRoute.of(context); if (route == null || route.isCurrent) { if (_snackBarController.isCompleted && _snackBarTimer == null) _snackBarTimer = new Timer(_snackBars.first._widget.duration, _hideSnackBar); @@ -484,7 +517,7 @@ class ScaffoldState extends State { final List children = new List(); _addIfNonNull(children, config.body, _ScaffoldSlot.body); if (config.appBarBehavior == AppBarBehavior.anchor) { - Widget appBar = new ConstrainedBox( + final Widget appBar = new ConstrainedBox( child: _getModifiedAppBar(padding: padding), constraints: new BoxConstraints(maxHeight: config.appBarHeight ?? kExtendedAppBarHeight + padding.top) ); @@ -494,7 +527,7 @@ class ScaffoldState extends State { if (_currentBottomSheet != null || (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)) { - List bottomSheets = []; + final List bottomSheets = []; if (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty) bottomSheets.addAll(_dismissedBottomSheets); if (_currentBottomSheet != null) @@ -510,7 +543,7 @@ class ScaffoldState extends State { _addIfNonNull(children, _snackBars.first._widget, _ScaffoldSlot.snackBar); if (config.floatingActionButton != null) { - Widget fab = new _FloatingActionButtonTransition( + final Widget fab = new _FloatingActionButtonTransition( key: new ValueKey(config.floatingActionButton.key), child: config.floatingActionButton ); @@ -529,8 +562,7 @@ class ScaffoldState extends State { Widget application; - if (config.appBarBehavior == AppBarBehavior.scroll) { - double overScroll = _scrollOffset.clamp(double.NEGATIVE_INFINITY, 0.0); + if (config.appBarBehavior != AppBarBehavior.anchor) { application = new NotificationListener( onNotification: _handleScrollNotification, child: new Stack( @@ -542,7 +574,7 @@ class ScaffoldState extends State { ) ), new Positioned( - top: -overScroll, + top: 0.0, left: 0.0, right: 0.0, child: _buildScrollableAppBar(context) diff --git a/packages/flutter/lib/src/widgets/scrollable_list.dart b/packages/flutter/lib/src/widgets/scrollable_list.dart index 5641b1eb42..b8bfbdce38 100644 --- a/packages/flutter/lib/src/widgets/scrollable_list.dart +++ b/packages/flutter/lib/src/widgets/scrollable_list.dart @@ -45,7 +45,7 @@ class ScrollableList extends Scrollable { } class _ScrollableListState extends ScrollableState { - ScrollBehavior createScrollBehavior() => new OverscrollBehavior(); + ScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(); ExtentScrollBehavior get scrollBehavior => super.scrollBehavior; void _handleExtentsChanged(double contentExtent, double containerExtent) {