Flexible AppBar with a TabBar

This commit is contained in:
Hans Muller 2016-03-17 13:27:23 -07:00
parent 5d3c55a162
commit 070fdf7754
11 changed files with 337 additions and 157 deletions

View file

@ -82,6 +82,7 @@ class FlexibleSpaceDemoState extends State<FlexibleSpaceDemo> {
@override
Widget build(BuildContext context) {
final double statusBarHeight = (MediaQuery.of(context)?.padding ?? EdgeInsets.zero).top;
return new Theme(
data: new ThemeData(
brightness: ThemeBrightness.light,
@ -89,10 +90,10 @@ class FlexibleSpaceDemoState extends State<FlexibleSpaceDemo> {
),
child: new Scaffold(
key: scaffoldKey,
appBarHeight: appBarHeight,
scrollableKey: scrollableKey,
appBarBehavior: _appBarBehavior,
appBar: new AppBar(
expandedHeight: appBarHeight,
actions: <Widget>[
new IconButton(
icon: Icons.create,
@ -134,7 +135,7 @@ class FlexibleSpaceDemoState extends State<FlexibleSpaceDemo> {
),
body: new Block(
scrollableKey: scrollableKey,
padding: new EdgeInsets.only(top: appBarHeight),
padding: new EdgeInsets.only(top: appBarHeight + statusBarHeight),
children: <Widget>[
new _ContactCategory(
icon: Icons.call,

View file

@ -0,0 +1,109 @@
// 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/material.dart';
enum TabsDemoStyle {
iconsAndText,
iconsOnly,
textOnly
}
class ScrollableTabsDemo extends StatefulWidget {
@override
ScrollableTabsDemoState createState() => new ScrollableTabsDemoState();
}
class ScrollableTabsDemoState extends State<ScrollableTabsDemo> {
final List<IconData> icons = <IconData>[
Icons.event,
Icons.home,
Icons.android,
Icons.alarm,
Icons.face,
Icons.language,
];
final Map<IconData, String> labels = <IconData, String>{
Icons.event: 'EVENT',
Icons.home: 'HOME',
Icons.android: 'ANDROID',
Icons.alarm: 'ALARM',
Icons.face: 'FACE',
Icons.language: 'LANGUAGE',
};
TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText;
void changeDemoStyle(TabsDemoStyle style) {
setState(() {
_demoStyle = style;
});
}
@override
Widget build(BuildContext context) {
final Color iconColor = Theme.of(context).accentColor;
return new TabBarSelection<IconData>(
values: icons,
child: new Scaffold(
appBar: new AppBar(
title: new Text('Scrollable Tabs'),
actions: <Widget>[
new PopupMenuButton<TabsDemoStyle>(
onSelected: changeDemoStyle,
items: <PopupMenuItem<TabsDemoStyle>>[
new PopupMenuItem<TabsDemoStyle>(
value: TabsDemoStyle.iconsAndText,
child: new Text('Icons and Text')
),
new PopupMenuItem<TabsDemoStyle>(
value: TabsDemoStyle.iconsOnly,
child: new Text('Icons Only')
),
new PopupMenuItem<TabsDemoStyle>(
value: TabsDemoStyle.textOnly,
child: new Text('Text Only')
),
]
)
],
tabBar: new TabBar<IconData>(
isScrollable: true,
labels: new Map<IconData, TabLabel>.fromIterable(
icons,
value: (IconData icon) {
switch(_demoStyle) {
case TabsDemoStyle.iconsAndText:
return new TabLabel(text: labels[icon], icon: icon);
case TabsDemoStyle.iconsOnly:
return new TabLabel(icon: icon);
case TabsDemoStyle.textOnly:
return new TabLabel(text: labels[icon]);
}
}
)
)
),
body: new TabBarView<IconData>(
children: icons.map((IconData icon) {
return new Container(
key: new ObjectKey(icon),
padding: const EdgeInsets.all(12.0),
child:new Card(
child: new Center(
child: new Icon(
icon: icon,
color: iconColor,
size: 128.0
)
)
)
);
}).toList()
)
)
);
}
}

View file

@ -4,55 +4,74 @@
import 'package:flutter/material.dart';
class TabsDemo extends StatelessWidget {
final List<IconData> icons = <IconData>[
Icons.event,
Icons.home,
Icons.android,
Icons.alarm,
Icons.face,
Icons.language,
];
class _Page {
_Page({ this.label });
final Map<IconData, String> labels = <IconData, String>{
Icons.event: 'EVENT',
Icons.home: 'HOME',
Icons.android: 'ANDROID',
Icons.alarm: 'ALARM',
Icons.face: 'FACE',
Icons.language: 'LANGUAGE',
};
final GlobalKey<ScrollableState<Scrollable>> key = new GlobalKey<ScrollableState<Scrollable>>();
final String label;
}
final List<_Page> _pages = <_Page>[
new _Page(label: 'ONE'),
new _Page(label: 'TWO'),
new _Page(label: 'FREE'),
new _Page(label: 'FOUR')
];
class TabsDemo extends StatefulWidget {
@override
TabsDemoState createState() => new TabsDemoState();
}
class TabsDemoState extends State<TabsDemo> {
_Page _selectedPage;
double _scrollOffset = 0.0;
@override
void initState() {
super.initState();
_selectedPage = _pages[0];
}
@override
Widget build(BuildContext context) {
final Color iconColor = Theme.of(context).accentColor;
return new TabBarSelection<IconData>(
values: icons,
final double statusBarHeight = (MediaQuery.of(context)?.padding ?? EdgeInsets.zero).top;
return new TabBarSelection<_Page>(
values: _pages,
onChanged: (_Page value) {
setState(() {
_selectedPage = value;
_selectedPage.key.currentState.scrollTo(_scrollOffset);
});
},
child: new Scaffold(
scrollableKey: _selectedPage.key,
appBarBehavior: AppBarBehavior.under,
appBar: new AppBar(
title: new Text("Scrollable Tabs"),
tabBar: new TabBar<IconData>(
isScrollable: true,
labels: new Map<IconData, TabLabel>.fromIterable(
icons,
value: (IconData icon) => new TabLabel(text: labels[icon], icon: icon)
)
title: new Text('Tabs and Scrolling'),
tabBar: new TabBar<_Page>(
labels: new Map<_Page, TabLabel>.fromIterable(_pages, value: (_Page page) {
return new TabLabel(text: page.label);
})
)
),
body: new TabBarView<IconData>(
children: icons.map((IconData icon) {
return new Container(
key: new ObjectKey(icon),
padding: const EdgeInsets.all(12.0),
child: new Card(
child: new Center(
child: new Icon(
icon: icon,
color: iconColor,
size: 128.0
body: new TabBarView<_Page>(
children: _pages.map((_Page page) {
return new Block(
padding: new EdgeInsets.only(top: kTextTabBarHeight + kToolBarHeight + statusBarHeight),
scrollableKey: page.key,
onScroll: (double value) { _scrollOffset = value; },
children: new List<Widget>.generate(6, (int i) {
return new Container(
padding: const EdgeInsets.all(8.0),
height: 192.0,
child: new Card(
child: new Center(
child: new Text('Tab $page.label, item $i')
)
)
)
)
);
})
);
}).toList()
)

View file

@ -34,7 +34,6 @@ class _TabsFabDemoState extends State<TabsFabDemo> {
final GlobalKey scaffoldKey = new GlobalKey();
final List<_Page> pages = <_Page>[
new _Page(label: 'Blue', colors: Colors.indigo, icon: Icons.add),
new _Page(label: 'Too', colors: Colors.indigo, icon: Icons.add),
new _Page(label: 'Eco', colors: Colors.green, icon: Icons.create),
new _Page(label: 'No'),
new _Page(label: 'Teal', colors: Colors.teal, icon: Icons.add),

View file

@ -30,6 +30,7 @@ import '../demo/toggle_controls_demo.dart';
import '../demo/scrolling_techniques_demo.dart';
import '../demo/slider_demo.dart';
import '../demo/snack_bar_demo.dart';
import '../demo/scrollable_tabs_demo.dart';
import '../demo/tabs_demo.dart';
import '../demo/tabs_fab_demo.dart';
import '../demo/text_field_demo.dart';
@ -49,14 +50,15 @@ class GalleryHome extends StatefulWidget {
class GalleryHomeState extends State<GalleryHome> {
@override
Widget build(BuildContext context) {
final double appBarHeight = 128.0;
return new Scaffold(
appBarHeight: 128.0,
drawer: new GalleryDrawer(),
appBar: new AppBar(
expandedHeight: appBarHeight,
flexibleSpace: (BuildContext context) {
return new Container(
padding: const EdgeInsets.only(left: 16.0, bottom: 24.0),
height: 128.0,
padding: const EdgeInsets.only(left: 64.0),
height: appBarHeight,
child: new Align(
alignment: const FractionalOffset(0.0, 1.0),
child: new Text('Flutter Gallery', style: Typography.white.headline)
@ -118,6 +120,7 @@ class GalleryHomeState extends State<GalleryHome> {
new GalleryDemo(title: 'Page Selector', builder: () => new PageSelectorDemo()),
new GalleryDemo(title: 'Persistent Bottom Sheet', builder: () => new PersistentBottomSheetDemo()),
new GalleryDemo(title: 'Progress Indicators', builder: () => new ProgressIndicatorDemo()),
new GalleryDemo(title: 'Scrollable Tabs', builder: () => new ScrollableTabsDemo()),
new GalleryDemo(title: 'Selection Controls', builder: () => new ToggleControlsDemo()),
new GalleryDemo(title: 'Sliders', builder: () => new SliderDemo()),
new GalleryDemo(title: 'SnackBar', builder: () => new SnackBarDemo()),

View file

@ -25,6 +25,7 @@ class GallerySection extends StatelessWidget {
}
void showDemos(BuildContext context) {
final double statusBarHeight = (MediaQuery.of(context)?.padding ?? EdgeInsets.zero).top;
final ThemeData theme = new ThemeData(
brightness: Theme.of(context).brightness,
primarySwatch: colors
@ -36,16 +37,16 @@ class GallerySection extends StatelessWidget {
return new Theme(
data: theme,
child: new Scaffold(
appBarHeight: appBarHeight,
appBarBehavior: AppBarBehavior.under,
scrollableKey: scrollableKey,
appBar: new AppBar(
expandedHeight: appBarHeight,
flexibleSpace: (BuildContext context) => new FlexibleSpaceBar(title: new Text(title))
),
body: new Material(
child: new MaterialList(
scrollableKey: scrollableKey,
scrollablePadding: new EdgeInsets.only(top: appBarHeight),
scrollablePadding: new EdgeInsets.only(top: appBarHeight + statusBarHeight),
type: MaterialListType.oneLine,
children: (demos ?? const <GalleryDemo>[]).map((GalleryDemo demo) {
return new ListItem(

View file

@ -8,6 +8,7 @@ import 'constants.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'material.dart';
import 'tabs.dart';
import 'theme.dart';
import 'typography.dart';
@ -23,8 +24,16 @@ class AppBar extends StatelessWidget {
this.elevation: 4,
this.backgroundColor,
this.textTheme,
this.padding: EdgeInsets.zero
}) : super(key: key) {
this.padding: EdgeInsets.zero,
double expandedHeight,
double collapsedHeight,
double minimumHeight,
double actualHeight
}) : _expandedHeight = expandedHeight,
_collapsedHeight = collapsedHeight,
_minimumHeight = minimumHeight,
_actualHeight = actualHeight,
super(key: key) {
assert((flexibleSpace != null) ? tabBar == null : true);
assert((tabBar != null) ? flexibleSpace == null : true);
}
@ -34,11 +43,15 @@ class AppBar extends StatelessWidget {
final List<Widget> actions;
final WidgetBuilder flexibleSpace;
final double foregroundOpacity;
final Widget tabBar;
final TabBar<dynamic> tabBar;
final int elevation;
final Color backgroundColor;
final TextTheme textTheme;
final EdgeInsets padding;
final double _expandedHeight;
final double _collapsedHeight;
final double _minimumHeight;
final double _actualHeight;
AppBar copyWith({
Key key,
@ -50,7 +63,10 @@ class AppBar extends StatelessWidget {
int elevation,
Color backgroundColor,
TextTheme textTheme,
EdgeInsets padding
EdgeInsets padding,
double expandedHeight,
double collapsedHeight,
double actualHeight
}) {
return new AppBar(
key: key ?? this.key,
@ -63,37 +79,56 @@ class AppBar extends StatelessWidget {
elevation: elevation ?? this.elevation,
backgroundColor: backgroundColor ?? this.backgroundColor,
textTheme: textTheme ?? this.textTheme,
padding: padding ?? this.padding
padding: padding ?? this.padding,
expandedHeight: expandedHeight ?? this._expandedHeight,
collapsedHeight: collapsedHeight ?? this._collapsedHeight,
actualHeight: actualHeight ?? this._actualHeight
);
}
double get _tabBarHeight => tabBar == null ? null : tabBar.minimumHeight;
double get _toolBarHeight => kToolBarHeight;
double get expandedHeight => _expandedHeight ?? (_toolBarHeight + (_tabBarHeight ?? 0.0));
double get collapsedHeight => _collapsedHeight ?? (_toolBarHeight + (_tabBarHeight ?? 0.0));
double get minimumHeight => _minimumHeight ?? _tabBarHeight ?? _toolBarHeight;
double get actualHeight => _actualHeight ?? expandedHeight;
// Defines the opacity of the toolbar's text and icons.
double _toolBarOpacity(double statusBarHeight) {
return ((actualHeight - (_tabBarHeight ?? 0.0) - statusBarHeight) / _toolBarHeight).clamp(0.0, 1.0);
}
double _tabBarOpacity(double statusBarHeight) {
final double tabBarHeight = _tabBarHeight ?? 0.0;
return ((actualHeight - statusBarHeight) / tabBarHeight).clamp(0.0, 1.0);
}
@override
Widget build(BuildContext context) {
Color color = backgroundColor;
IconThemeData iconThemeData;
TextStyle centerStyle = textTheme?.title;
TextStyle sideStyle = textTheme?.body1;
final double statusBarHeight = (MediaQuery.of(context)?.padding ?? EdgeInsets.zero).top;
final ThemeData theme = Theme.of(context);
if (color == null || iconThemeData == null || textTheme == null) {
ThemeData themeData = Theme.of(context);
color ??= themeData.primaryColor;
iconThemeData ??= themeData.primaryIconTheme;
IconThemeData iconTheme = theme.primaryIconTheme;
TextStyle centerStyle = textTheme?.title ?? theme.primaryTextTheme.title;
TextStyle sideStyle = textTheme?.body1 ?? theme.primaryTextTheme.body1;
TextTheme primaryTextTheme = themeData.primaryTextTheme;
centerStyle ??= primaryTextTheme.title;
sideStyle ??= primaryTextTheme.body2;
}
if (foregroundOpacity != 1.0) {
final int alpha = (foregroundOpacity.clamp(0.0, 1.0) * 255.0).round();
final double toolBarOpacity = _toolBarOpacity(statusBarHeight);
if (toolBarOpacity != 1.0) {
final double opacity = const Interval(0.25, 1.0, curve: Curves.ease).transform(toolBarOpacity);
if (centerStyle?.color != null)
centerStyle = centerStyle.copyWith(color: centerStyle.color.withAlpha(alpha));
centerStyle = centerStyle.copyWith(color: centerStyle.color.withOpacity(opacity));
if (sideStyle?.color != null)
sideStyle = sideStyle.copyWith(color: sideStyle.color.withAlpha(alpha));
if (iconThemeData != null) {
iconThemeData = new IconThemeData(
opacity: foregroundOpacity * iconThemeData.clampedOpacity,
color: iconThemeData.color
sideStyle = sideStyle.copyWith(color: sideStyle.color.withOpacity(opacity));
if (iconTheme != null) {
iconTheme = new IconThemeData(
opacity: opacity * iconTheme.clampedOpacity,
color: iconTheme.color
);
}
}
@ -112,59 +147,72 @@ class AppBar extends StatelessWidget {
if (actions != null)
toolBarRow.addAll(actions);
Widget appBar = new SizedBox(
height: kToolBarHeight,
child: new IconTheme(
data: iconTheme,
child: new DefaultTextStyle(
style: sideStyle,
child: new Row(children: toolBarRow)
)
)
);
final double tabBarOpacity = _tabBarOpacity(statusBarHeight);
if (tabBar != null) {
appBar = new Column(
children: <Widget>[
appBar,
tabBarOpacity == 1.0 ? tabBar : new Opacity(
child: tabBar,
opacity: const Interval(0.25, 1.0, curve: Curves.ease).transform(tabBarOpacity)
)
]
);
}
EdgeInsets combinedPadding = new EdgeInsets.symmetric(horizontal: 8.0);
if (padding != null)
combinedPadding += padding;
// 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 = kToolBarHeight + combinedPadding.top + combinedPadding.bottom;
final Widget toolBar = new ConstrainedBox(
constraints: new BoxConstraints(maxHeight: toolBarHeight),
// If the appBar's height shrinks below collapsedHeight, it will be clipped and bottom
// justified. This is so that the toolBar/tabBar appear to move upwards as the appBar's
// height is reduced.
final double paddedCollapsedHeight = collapsedHeight + combinedPadding.top + combinedPadding.bottom;
appBar = new ConstrainedBox(
constraints: new BoxConstraints(maxHeight: paddedCollapsedHeight),
child: new Padding(
padding: new EdgeInsets.only(left: combinedPadding.left, right: combinedPadding.right),
child: new ClipRect(
child: new OverflowBox(
alignment: const FractionalOffset(0.0, 1.0), // bottom justify
minHeight: toolBarHeight,
maxHeight: toolBarHeight,
child: new DefaultTextStyle(
style: sideStyle,
child: new Padding(
padding: new EdgeInsets.only(top: combinedPadding.top, bottom: combinedPadding.bottom),
child: new Row(children: toolBarRow)
)
minHeight: paddedCollapsedHeight,
maxHeight: paddedCollapsedHeight,
child: new Padding(
padding: new EdgeInsets.only(top: combinedPadding.top, bottom: combinedPadding.bottom),
child: appBar
)
)
)
)
);
Widget appBar = toolBar;
if (tabBar != null) {
appBar = new Column(
mainAxisAlignment: MainAxisAlignment.collapse,
children: <Widget>[toolBar, tabBar]
);
} else if (flexibleSpace != null) {
if (flexibleSpace != null) {
appBar = new Stack(
children: <Widget>[
flexibleSpace(context),
new Align(child: toolBar, alignment: const FractionalOffset(0.0, 0.0))
new Align(child: appBar, alignment: const FractionalOffset(0.0, 0.0))
]
);
}
Widget contents = new Material(
color: color,
appBar = new Material(
color: backgroundColor ?? theme.primaryColor,
elevation: elevation,
child: appBar
);
if (iconThemeData != null)
contents = new IconTheme(data: iconThemeData, child: contents);
return contents;
return appBar;
}
}

View file

@ -13,8 +13,8 @@ const double kToolBarHeight = 56.0;
const double kExtendedAppBarHeight = 128.0;
const double kTextTabBarHeight = 48.0;
const double kIconTabBarHeight = 48.0;
const double kTextandIconTabBarHeight = 72.0;
const double kIconTabBarHeight = 26.0;
const double kTextAndIconTabBarHeight = 74.0;
// https://www.google.com/design/spec/layout/metrics-keylines.html#metrics-keylines-keylines-spacing
const double kListTitleHeight = 72.0;

View file

@ -25,10 +25,10 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasScaffold(context));
final double appBarHeight = Scaffold.of(context).appBarHeight;
final double statusBarHeight = (MediaQuery.of(context)?.padding ?? EdgeInsets.zero).top;
final Animation<double> animation = Scaffold.of(context).appBarAnimation;
final EdgeInsets toolBarPadding = MediaQuery.of(context)?.padding ?? EdgeInsets.zero;
final double toolBarHeight = kToolBarHeight + toolBarPadding.top;
final double appBarHeight = Scaffold.of(context).appBarHeight + statusBarHeight;
final double toolBarHeight = kToolBarHeight + statusBarHeight;
final List<Widget> children = <Widget>[];
// background image
@ -46,7 +46,10 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
right: 0.0,
child: new Opacity(
opacity: new Tween<double>(begin: 1.0, end: 0.0).evaluate(opacityCurve),
child: config.image
child: new SizedBox(
height: appBarHeight + statusBarHeight,
child: config.image
)
)
));
}
@ -64,7 +67,7 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
color: titleStyle.color.withAlpha(new Tween<double>(begin: 255.0, end: 0.0).evaluate(opacityCurve).toInt())
);
final double yAlignStart = 1.0;
final double yAlignEnd = (toolBarPadding.top + kToolBarHeight / 2.0) / toolBarHeight;
final double yAlignEnd = (statusBarHeight + kToolBarHeight / 2.0) / toolBarHeight;
final double scaleAndAlignEnd = (appBarHeight - toolBarHeight) / appBarHeight;
final CurvedAnimation scaleAndAlignCurve = new CurvedAnimation(
parent: animation,

View file

@ -10,7 +10,6 @@ import 'package:flutter/widgets.dart';
import 'app_bar.dart';
import 'bottom_sheet.dart';
import 'constants.dart';
import 'drawer.dart';
import 'icons.dart';
import 'icon_button.dart';
@ -211,11 +210,9 @@ class Scaffold extends StatefulWidget {
this.floatingActionButton,
this.drawer,
this.scrollableKey,
this.appBarBehavior: AppBarBehavior.anchor,
this.appBarHeight
this.appBarBehavior: AppBarBehavior.anchor
}) : super(key: key) {
assert((appBarBehavior == AppBarBehavior.scroll) ? scrollableKey != null : true);
assert((appBarBehavior == AppBarBehavior.scroll) ? appBarHeight != null && appBarHeight > kToolBarHeight : true);
}
final AppBar appBar;
@ -224,7 +221,6 @@ class Scaffold extends StatefulWidget {
final Widget drawer;
final Key scrollableKey;
final AppBarBehavior appBarBehavior;
final double appBarHeight;
/// The state from the closest instance of this class that encloses the given context.
static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
@ -241,7 +237,7 @@ class ScaffoldState extends State<Scaffold> {
Animation<double> get appBarAnimation => _appBarController.view;
double get appBarHeight => config.appBarHeight;
double get appBarHeight => config.appBar?.expandedHeight ?? 0.0;
// DRAWER API
@ -401,11 +397,10 @@ class ScaffoldState extends State<Scaffold> {
bool _shouldShowBackArrow;
Widget _getModifiedAppBar({ EdgeInsets padding, double foregroundOpacity: 1.0, int elevation }) {
Widget _getModifiedAppBar({ EdgeInsets padding, int elevation, double actualHeight}) {
AppBar appBar = config.appBar;
if (appBar == null)
return null;
EdgeInsets appBarPadding = new EdgeInsets.only(top: padding.top);
Widget leading = appBar.leading;
if (leading == null) {
if (config.drawer != null) {
@ -427,9 +422,9 @@ class ScaffoldState extends State<Scaffold> {
}
return appBar.copyWith(
elevation: elevation ?? appBar.elevation ?? 4,
padding: appBarPadding,
foregroundOpacity: foregroundOpacity,
leading: leading
padding: new EdgeInsets.only(top: padding.top),
leading: leading,
actualHeight: actualHeight
);
}
@ -438,73 +433,66 @@ class ScaffoldState extends State<Scaffold> {
double _floatingAppBarHeight = 0.0;
bool _handleScrollNotification(ScrollNotification notification) {
final double newScrollOffset = notification.scrollable.scrollOffset;
if (config.scrollableKey != null && config.scrollableKey == notification.scrollable.config.key)
if (config.scrollableKey != null && config.scrollableKey == notification.scrollable.config.key) {
final double newScrollOffset = notification.scrollable.scrollOffset;
setState(() {
_scrollOffsetDelta = _scrollOffset - newScrollOffset;
_scrollOffset = newScrollOffset;
});
}
return false;
}
double _appBarOpacity(double progress) {
// The value of progress is 1.0 if the entire (padded) app bar is visible, 0.0
// if the app bar's height is zero.
return new Tween<double>(begin: 0.0, end: 1.0).evaluate(new CurvedAnimation(
parent: new AnimationController()..value = progress.clamp(0.0, 1.0),
curve: new Interval(0.50, 1.0)
));
}
Widget _buildAnchoredAppBar(double toolBarHeight, EdgeInsets toolBarPadding) {
Widget _buildAnchoredAppBar(double expandedHeight, double height, EdgeInsets padding) {
// Drive _appBarController to the point where the flexible space has disappeared.
_appBarController.value = (appBarHeight - toolBarHeight) / appBarHeight;
_appBarController.value = (expandedHeight - height) / expandedHeight;
return new SizedBox(
height: toolBarHeight,
child: _getModifiedAppBar(padding: toolBarPadding)
height: height,
child: _getModifiedAppBar(padding: padding, actualHeight: height)
);
}
Widget _buildScrollableAppBar(BuildContext context) {
final EdgeInsets toolBarPadding = MediaQuery.of(context)?.padding ?? EdgeInsets.zero;
final double toolBarHeight = kToolBarHeight + toolBarPadding.top;
final EdgeInsets padding = MediaQuery.of(context)?.padding ?? EdgeInsets.zero;
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;
Widget appBar;
if (_scrollOffset <= appBarHeight && _scrollOffset >= appBarHeight - toolBarHeight) {
// scrolled to the top, only the toolbar is (partially) visible.
if (_scrollOffset <= expandedHeight && _scrollOffset >= expandedHeight - minimumHeight) {
// scrolled to the top, flexible space collapsed, only the toolbar and tabbar are (partially) visible.
if (config.appBarBehavior == AppBarBehavior.under) {
appBar = _buildAnchoredAppBar(toolBarHeight, toolBarPadding);
appBar = _buildAnchoredAppBar(expandedHeight, minimumHeight, padding);
} else {
final double height = math.max(_floatingAppBarHeight, appBarHeight - _scrollOffset);
final double opacity = _appBarOpacity(1.0 - ((toolBarHeight - height) / toolBarHeight));
_appBarController.value = (appBarHeight - height) / appBarHeight;
final double height = math.max(_floatingAppBarHeight, expandedHeight - _scrollOffset);
_appBarController.value = (expandedHeight - height) / expandedHeight;
appBar = new SizedBox(
height: height,
child: _getModifiedAppBar(padding: toolBarPadding, foregroundOpacity: opacity)
child: _getModifiedAppBar(padding: padding, actualHeight: height)
);
}
} else if (_scrollOffset > appBarHeight) {
} else if (_scrollOffset > expandedHeight) {
// scrolled past the entire app bar, maybe show the "floating" toolbar.
if (config.appBarBehavior == AppBarBehavior.under) {
appBar = _buildAnchoredAppBar(toolBarHeight, toolBarPadding);
appBar = _buildAnchoredAppBar(expandedHeight, minimumHeight, padding);
} else {
_floatingAppBarHeight = (_floatingAppBarHeight + _scrollOffsetDelta).clamp(0.0, toolBarHeight);
final double toolBarOpacity = _appBarOpacity(_floatingAppBarHeight / toolBarHeight);
_appBarController.value = (appBarHeight - _floatingAppBarHeight) / appBarHeight;
_floatingAppBarHeight = (_floatingAppBarHeight + _scrollOffsetDelta).clamp(0.0, collapsedHeight);
_appBarController.value = (expandedHeight - _floatingAppBarHeight) / expandedHeight;
appBar = new SizedBox(
height: _floatingAppBarHeight,
child: _getModifiedAppBar(padding: toolBarPadding, foregroundOpacity: toolBarOpacity)
child: _getModifiedAppBar(padding: padding, actualHeight: _floatingAppBarHeight)
);
}
} 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;
// _scrollOffset < expandedHeight - collapsedHeight, scrolled to the top, flexible space is visible]
final double height = expandedHeight - _scrollOffset.clamp(0.0, expandedHeight);
_appBarController.value = (expandedHeight - height) / expandedHeight;
appBar = new SizedBox(
height: height,
child: _getModifiedAppBar(padding: toolBarPadding, elevation: 0)
child: _getModifiedAppBar(padding: padding, elevation: 0, actualHeight: height)
);
_floatingAppBarHeight = 0.0;
}
return appBar;
@ -528,9 +516,10 @@ class ScaffoldState extends State<Scaffold> {
final List<LayoutId> children = new List<LayoutId>();
_addIfNonNull(children, config.body, _ScaffoldSlot.body);
if (config.appBarBehavior == AppBarBehavior.anchor) {
final double expandedHeight = (config.appBar?.expandedHeight ?? 0.0) + padding.top;
final Widget appBar = new ConstrainedBox(
child: _getModifiedAppBar(padding: padding),
constraints: new BoxConstraints(maxHeight: config.appBarHeight ?? kExtendedAppBarHeight + padding.top)
child: _getModifiedAppBar(padding: padding, actualHeight: expandedHeight),
constraints: new BoxConstraints(maxHeight: expandedHeight)
);
_addIfNonNull(children, appBar, _ScaffoldSlot.appBar);
}

View file

@ -592,6 +592,14 @@ class TabBar<T> extends Scrollable {
final Map<T, TabLabel> labels;
final bool isScrollable;
double get minimumHeight {
for (TabLabel label in labels.values) {
if (label.text != null && (label.icon != null || label.iconBuilder != null))
return _kTextAndIconTabHeight + _kTabIndicatorHeight;
}
return _kTabHeight + _kTabIndicatorHeight;
}
@override
_TabBarState<T> createState() => new _TabBarState<T>();
}
@ -810,9 +818,9 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
indicatorColor = Colors.white;
}
TextStyle textStyle = themeData.primaryTextTheme.body1;
TextStyle textStyle = themeData.primaryTextTheme.body2;
IconThemeData iconTheme = themeData.primaryIconTheme;
Color textColor = themeData.primaryTextTheme.body1.color.withAlpha(0xB2); // 70% alpha
Color textColor = themeData.primaryTextTheme.body2.color.withAlpha(0xB2); // 70% alpha
List<Widget> tabs = <Widget>[];
bool textAndIcons = false;