diff --git a/examples/flutter_gallery/lib/demo/colors_demo.dart b/examples/flutter_gallery/lib/demo/colors_demo.dart index 471b34913dd..4bd52691f03 100644 --- a/examples/flutter_gallery/lib/demo/colors_demo.dart +++ b/examples/flutter_gallery/lib/demo/colors_demo.dart @@ -119,7 +119,7 @@ class ColorsDemo extends StatelessWidget { appBar: new AppBar( elevation: 0, title: new Text('Colors'), - tabBar: new TabBar( + bottom: new TabBar( isScrollable: true, labels: new Map.fromIterable(colorSwatches, value: (ColorSwatch swatch) { return new TabLabel(text: swatch.name); diff --git a/examples/flutter_gallery/lib/demo/scrollable_tabs_demo.dart b/examples/flutter_gallery/lib/demo/scrollable_tabs_demo.dart index 2f00f0e3e63..567cab98e7d 100644 --- a/examples/flutter_gallery/lib/demo/scrollable_tabs_demo.dart +++ b/examples/flutter_gallery/lib/demo/scrollable_tabs_demo.dart @@ -71,7 +71,7 @@ class ScrollableTabsDemoState extends State { ] ) ], - tabBar: new TabBar( + bottom: new TabBar( isScrollable: true, labels: new Map.fromIterable( icons, diff --git a/examples/flutter_gallery/lib/demo/tabs_demo.dart b/examples/flutter_gallery/lib/demo/tabs_demo.dart index ddfdda08aef..fa05f031fcb 100644 --- a/examples/flutter_gallery/lib/demo/tabs_demo.dart +++ b/examples/flutter_gallery/lib/demo/tabs_demo.dart @@ -50,7 +50,7 @@ class TabsDemoState extends State { appBarBehavior: AppBarBehavior.under, appBar: new AppBar( title: new Text('Tabs and scrolling'), - tabBar: new TabBar<_Page>( + bottom: new TabBar<_Page>( labels: new Map<_Page, TabLabel>.fromIterable(_pages, value: (_Page page) { return new TabLabel(text: page.label); }) diff --git a/examples/flutter_gallery/lib/demo/tabs_fab_demo.dart b/examples/flutter_gallery/lib/demo/tabs_fab_demo.dart index 6dbaec1bef1..8e92f795f7f 100644 --- a/examples/flutter_gallery/lib/demo/tabs_fab_demo.dart +++ b/examples/flutter_gallery/lib/demo/tabs_fab_demo.dart @@ -100,7 +100,7 @@ class _TabsFabDemoState extends State { key: scaffoldKey, appBar: new AppBar( title: new Text('FAB per tab'), - tabBar: new TabBar<_Page>( + bottom: new TabBar<_Page>( labels: new Map<_Page, TabLabel>.fromIterable(pages, value: (_Page page) => page.tabLabel) ) ), diff --git a/examples/flutter_gallery/lib/demo/two_level_list_demo.dart b/examples/flutter_gallery/lib/demo/two_level_list_demo.dart index 215fb06f42a..d9514a7b8a2 100644 --- a/examples/flutter_gallery/lib/demo/two_level_list_demo.dart +++ b/examples/flutter_gallery/lib/demo/two_level_list_demo.dart @@ -16,15 +16,16 @@ class TwoLevelListDemo extends StatelessWidget { children: [ new TwoLevelListItem(title: new Text('Top')), new TwoLevelSublist( - title: new Text('Sublist'), - children: [ - new TwoLevelListItem(title: new Text('One')), - new TwoLevelListItem(title: new Text('Two')), - new TwoLevelListItem(title: new Text('Free')), - new TwoLevelListItem(title: new Text('Four')) - ] - ), - new TwoLevelListItem(title: new Text('Bottom')) + title: new Text('Sublist'), + children: [ + new TwoLevelListItem(title: new Text('One')), + new TwoLevelListItem(title: new Text('Two')), + // https://en.wikipedia.org/wiki/Free_Four + new TwoLevelListItem(title: new Text('Free')), + new TwoLevelListItem(title: new Text('Four')) + ] + ), + new TwoLevelListItem(title: new Text('Bottom')) ] ) ); diff --git a/examples/flutter_gallery/lib/gallery/demo.dart b/examples/flutter_gallery/lib/gallery/demo.dart index 5ce8e0fad55..38bf2e6d06b 100644 --- a/examples/flutter_gallery/lib/gallery/demo.dart +++ b/examples/flutter_gallery/lib/gallery/demo.dart @@ -73,7 +73,7 @@ class TabbedComponentDemoScaffold extends StatelessWidget { child: new Scaffold( appBar: new AppBar( title: new Text(title), - tabBar: new TabBar( + bottom: new TabBar( isScrollable: true, labels: ComponentDemoTabData.buildTabLabels(demos) ) diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 416a742f78b..ba50cd85ac0 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -241,7 +241,7 @@ class StockHomeState extends State { ] ) ], - tabBar: new TabBar( + bottom: new TabBar( labels: { StockHomeTab.market: new TabLabel(text: StockStrings.of(context).market()), StockHomeTab.portfolio: new TabLabel(text: StockStrings.of(context).portfolio()) diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index d2a04c3fc7f..7c6ddcf6f85 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -14,6 +14,14 @@ import 'tabs.dart'; import 'theme.dart'; import 'typography.dart'; +/// A widget that can appear at the bottom of an [AppBar]. The [Scaffold] uses +/// the bottom widget's [bottomHeight] to handle layout for +/// [AppBarBehavior.scroll] and [AppBarBehavior.under]. +abstract class AppBarBottomWidget extends Widget { + /// Defines the height of the app bar's optional bottom widget. + double get bottomHeight; +} + // TODO(eseidel) Toolbar needs to change size based on orientation: // http://www.google.com/design/spec/layout/structure.html#structure-app-bar // Mobile Landscape: 48dp @@ -30,10 +38,13 @@ import 'typography.dart'; /// App bars are most commonly used in the [Scaffold.appBar] property, which /// places the app bar at the top of the app. /// -/// The AppBar displays the toolbar widgets, [leading], [title], -/// and [actions], above the [tabBar] (if any). If a [flexibleSpace] widget is -/// specified then it is stacked behind the toolbar and tabbar. The [Scaffold] -/// typically creates the appbar with an initial height equal to [expandedHeight]. +/// The AppBar displays the toolbar widgets, [leading], [title], and +/// [actions], above the [bottom] (if any). If a [flexibleSpace] widget is +/// specified then it is stacked behind the toolbar and the bottom widget. +/// The [Scaffold] typically creates the appbar with an initial height equal to +/// [expandedHeight]. If the [Scaffold.appBarBehavior] is set then the +/// AppBar's [collapsedHeight] and [bottomHeight] define how small the app bar +/// will become when the application is scrolled. /// /// See also: /// @@ -53,18 +64,16 @@ class AppBar extends StatelessWidget { this.title, this.actions, this.flexibleSpace, - this.tabBar, + this.bottom, this.elevation: 4, this.backgroundColor, this.brightness, this.textTheme, this.padding: EdgeInsets.zero, double expandedHeight, - double collapsedHeight, - double minimumHeight + double collapsedHeight }) : _expandedHeight = expandedHeight, _collapsedHeight = collapsedHeight, - _minimumHeight = minimumHeight, super(key: key); /// A widget to display before the [title]. @@ -77,7 +86,7 @@ class AppBar extends StatelessWidget { /// field with an [IconButton] that calls [Navigator.pop]. final Widget leading; - /// The primary widget displayed in the app bar. + /// The primary widget displayed in the appbar. /// /// Typically a [Text] widget containing a description of the current contents /// of the app. @@ -97,8 +106,10 @@ class AppBar extends StatelessWidget { /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details. final Widget flexibleSpace; - /// A horizontal bar of tabs to display at the bottom of the app bar. - final TabBar tabBar; + /// This widget appears across the bottom of the appbar. + /// + /// Typically a [TabBar]. + final AppBarBottomWidget bottom; /// The z-coordinate at which to place this app bar. /// @@ -129,7 +140,6 @@ class AppBar extends StatelessWidget { final double _expandedHeight; final double _collapsedHeight; - final double _minimumHeight; /// Creates a copy of this app bar but with the given fields replaced with the new values. AppBar copyWith({ @@ -138,6 +148,7 @@ class AppBar extends StatelessWidget { Widget title, List actions, Widget flexibleSpace, + AppBarBottomWidget bottom, int elevation, Color backgroundColor, Brightness brightness, @@ -152,7 +163,7 @@ class AppBar extends StatelessWidget { title: title ?? this.title, actions: actions ?? this.actions, flexibleSpace: flexibleSpace ?? this.flexibleSpace, - tabBar: tabBar ?? this.tabBar, + bottom: bottom ?? this.bottom, elevation: elevation ?? this.elevation, backgroundColor: backgroundColor ?? this.backgroundColor, brightness: brightness ?? this.brightness, @@ -163,31 +174,31 @@ class AppBar extends StatelessWidget { ); } - double get _tabBarHeight => tabBar == null ? null : tabBar.minimumHeight; - double get _toolBarHeight => kToolBarHeight; - /// By default, the height of the toolbar and the tabbar (if any). - /// The [Scaffold] gives its appbar this height initially. If a + /// The height of the bottom widget. The [Scaffold] uses this value to control + /// the size of the app bar when its appBarBehavior is [AppBarBehavior.scroll] + /// or [AppBarBehavior.under]. + double get bottomHeight => bottom?.bottomHeight ?? 0.0; + + /// By default, the total height of the toolbar and the bottom widget (if any). + /// The [Scaffold] gives its app bar this height initially. If a /// [flexibleSpace] widget is specified this height should be big /// enough to accommodate whatever that widget contains. - double get expandedHeight => _expandedHeight ?? (_toolBarHeight + (_tabBarHeight ?? 0.0)); + double get expandedHeight => _expandedHeight ?? (_toolBarHeight + bottomHeight); - /// By default, the height of the toolbar and the tabbar (if any). + /// By default, the height of the toolbar and the bottom widget (if any). /// If the height of the app bar is constrained to be less than this value - /// the toolbar and tabbar are scrolled upwards, out of view. - double get collapsedHeight => _collapsedHeight ?? (_toolBarHeight + (_tabBarHeight ?? 0.0)); - - double get minimumHeight => _minimumHeight ?? _tabBarHeight ?? _toolBarHeight; + /// then the toolbar and bottom widget are scrolled upwards, out of view. + double get collapsedHeight => _collapsedHeight ?? (_toolBarHeight + bottomHeight); // Defines the opacity of the toolbar's text and icons. double _toolBarOpacity(double appBarHeight, double statusBarHeight) { - return ((appBarHeight - (_tabBarHeight ?? 0.0) - statusBarHeight) / _toolBarHeight).clamp(0.0, 1.0); + return ((appBarHeight - bottomHeight - statusBarHeight) / _toolBarHeight).clamp(0.0, 1.0); } - double _tabBarOpacity(double appBarHeight, double statusBarHeight) { - final double tabBarHeight = _tabBarHeight ?? 0.0; - return ((appBarHeight - statusBarHeight) / tabBarHeight).clamp(0.0, 1.0); + double _bottomOpacity(double appBarHeight, double statusBarHeight) { + return ((appBarHeight - statusBarHeight) / bottomHeight).clamp(0.0, 1.0); } Widget _buildForSize(BuildContext context, BoxConstraints constraints) { @@ -254,15 +265,15 @@ class AppBar extends StatelessWidget { ) ); - final double tabBarOpacity = _tabBarOpacity(size.height, statusBarHeight); - if (tabBar != null) { + final double bottomOpacity = _bottomOpacity(size.height, statusBarHeight); + if (bottom != null) { appBar = new Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ appBar, - tabBarOpacity == 1.0 ? tabBar : new Opacity( - child: tabBar, - opacity: const Interval(0.25, 1.0, curve: Curves.ease).transform(tabBarOpacity) + bottomOpacity == 1.0 ? bottom : new Opacity( + child: bottom, + opacity: const Interval(0.25, 1.0, curve: Curves.ease).transform(bottomOpacity) ) ] ); diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index d69fe1d1b70..ada2eef7acf 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -20,24 +20,27 @@ const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be de const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 200); final Tween _kFloatingActionButtonTurnTween = new Tween(begin: -0.125, end: 0.0); -/// 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. +/// The Scaffold's appbar is the toolbar, bottom, and the "flexible space" +/// that's stacked behind them. The Scaffold's appBarBehavior defines how +/// its layout responds to scrolling the application's body. enum AppBarBehavior { - /// The tool bar's layout does not respond to scrolling. + /// The app bar's layout does not respond to scrolling. anchor, - /// The tool bar's appearance and layout depend on the scrollOffset of the + /// The app 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. + /// and then the app bar fades out and scrolls off the top of the screen. + /// Scrolling upwards always causes the app bar's bottom widget to reappear + /// if the bottom widget isn't null, otherwise the app bar's toolbar reappears. scroll, - /// The tool bar's appearance and layout depend on the scrollOffset of the + /// The app 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. + /// If the bottom widget isn't null the app bar shrinks to the bottom widget's + /// [AppBarBottomWidget.bottomHeight], otherwise the app bar shrinks to its + /// [AppBar.collapsedHeight]. under, } @@ -612,13 +615,14 @@ class ScaffoldState extends State { Widget _buildScrollableAppBar(BuildContext context, EdgeInsets padding) { final double expandedHeight = (config.appBar?.expandedHeight ?? 0.0) + padding.top; final double collapsedHeight = (config.appBar?.collapsedHeight ?? 0.0) + padding.top; - final double minimumHeight = (config.appBar?.minimumHeight ?? 0.0) + padding.top; + final double bottomHeight = config.appBar?.bottomHeight + padding.top; + final double underHeight = config.appBar.bottom != null ? bottomHeight : collapsedHeight; Widget appBar; - if (_scrollOffset <= expandedHeight && _scrollOffset >= expandedHeight - minimumHeight) { + if (_scrollOffset <= expandedHeight && _scrollOffset >= expandedHeight - underHeight) { // scrolled to the top, flexible space collapsed, only the toolbar and tabbar are (partially) visible. if (config.appBarBehavior == AppBarBehavior.under) { - appBar = _buildAnchoredAppBar(expandedHeight, minimumHeight, padding); + appBar = _buildAnchoredAppBar(expandedHeight, underHeight, padding); } else { final double height = math.max(_floatingAppBarHeight, expandedHeight - _scrollOffset); _appBarController.value = (expandedHeight - height) / expandedHeight; @@ -630,7 +634,7 @@ class ScaffoldState extends State { } else if (_scrollOffset > expandedHeight) { // scrolled past the entire app bar, maybe show the "floating" toolbar. if (config.appBarBehavior == AppBarBehavior.under) { - appBar = _buildAnchoredAppBar(expandedHeight, minimumHeight, padding); + appBar = _buildAnchoredAppBar(expandedHeight, underHeight, padding); } else { _floatingAppBarHeight = (_floatingAppBarHeight + _scrollOffsetDelta).clamp(0.0, collapsedHeight); _appBarController.value = (expandedHeight - _floatingAppBarHeight) / expandedHeight; diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 56a5bf6423a..bb78cfd9821 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; +import 'app_bar.dart'; import 'colors.dart'; import 'debug.dart'; import 'icon.dart'; @@ -643,7 +644,7 @@ class TabBarSelectionState extends State> { /// * [TabBarView] /// * [AppBar.tabBar] /// * -class TabBar extends Scrollable { +class TabBar extends Scrollable implements AppBarBottomWidget { TabBar({ Key key, this.labels, @@ -671,7 +672,9 @@ class TabBar extends Scrollable { /// the color of the theme's body2 text color is used. final Color labelColor; - double get minimumHeight { + /// The height of the tab labels and indicator. + @override + double get bottomHeight { for (TabLabel label in labels.values) { if (label.text != null && (label.icon != null || label.iconBuilder != null)) return _kTextAndIconTabHeight + _kTabIndicatorHeight;