mirror of
https://github.com/flutter/flutter
synced 2024-10-16 21:22:57 +00:00
Flexible AppBar with a TabBar
This commit is contained in:
parent
5d3c55a162
commit
070fdf7754
|
@ -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,
|
||||
|
|
109
examples/material_gallery/lib/demo/scrollable_tabs_demo.dart
Normal file
109
examples/material_gallery/lib/demo/scrollable_tabs_demo.dart
Normal 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()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue