Commit c707b53f authored by Hans Muller's avatar Hans Muller Committed by GitHub

AppBar bottom widget, not necessarily a TabBar (#4631)

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