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 {
appBar: new AppBar(
elevation: 0,
title: new Text('Colors'),
tabBar: new TabBar<ColorSwatch>(
bottom: new TabBar<ColorSwatch>(
isScrollable: true,
labels: new Map<ColorSwatch, TabLabel>.fromIterable(colorSwatches, value: (ColorSwatch swatch) {
return new TabLabel(text: swatch.name);
......
......@@ -71,7 +71,7 @@ class ScrollableTabsDemoState extends State<ScrollableTabsDemo> {
]
)
],
tabBar: new TabBar<IconData>(
bottom: new TabBar<IconData>(
isScrollable: true,
labels: new Map<IconData, TabLabel>.fromIterable(
icons,
......
......@@ -50,7 +50,7 @@ class TabsDemoState extends State<TabsDemo> {
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);
})
......
......@@ -100,7 +100,7 @@ class _TabsFabDemoState extends State<TabsFabDemo> {
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)
)
),
......
......@@ -16,15 +16,16 @@ class TwoLevelListDemo extends StatelessWidget {
children: <Widget>[
new TwoLevelListItem(title: new Text('Top')),
new TwoLevelSublist(
title: new Text('Sublist'),
children: <Widget>[
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: <Widget>[
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'))
]
)
);
......
......@@ -73,7 +73,7 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
child: new Scaffold(
appBar: new AppBar(
title: new Text(title),
tabBar: new TabBar<ComponentDemoTabData>(
bottom: new TabBar<ComponentDemoTabData>(
isScrollable: true,
labels: ComponentDemoTabData.buildTabLabels(demos)
)
......
......@@ -241,7 +241,7 @@ class StockHomeState extends State<StockHome> {
]
)
],
tabBar: new TabBar<StockHomeTab>(
bottom: new TabBar<StockHomeTab>(
labels: <StockHomeTab, TabLabel>{
StockHomeTab.market: new TabLabel(text: StockStrings.of(context).market()),
StockHomeTab.portfolio: new TabLabel(text: StockStrings.of(context).portfolio())
......
......@@ -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<dynamic> 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<Widget> 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: <Widget>[
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)
)
]
);
......
......@@ -20,24 +20,27 @@ const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be de
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 200);
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
/// 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<Scaffold> {
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<Scaffold> {
} 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;
......
......@@ -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<T> extends State<TabBarSelection<T>> {
/// * [TabBarView]
/// * [AppBar.tabBar]
/// * <https://www.google.com/design/spec/components/tabs.html>
class TabBar<T> extends Scrollable {
class TabBar<T> extends Scrollable implements AppBarBottomWidget {
TabBar({
Key key,
this.labels,
......@@ -671,7 +672,9 @@ class TabBar<T> 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;
......
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