diff --git a/examples/material_gallery/lib/main.dart b/examples/material_gallery/lib/main.dart index 01d1e5f70c..86bc52dadf 100644 --- a/examples/material_gallery/lib/main.dart +++ b/examples/material_gallery/lib/main.dart @@ -46,14 +46,23 @@ class GallerySection extends StatelessComponent { brightness: ThemeBrightness.light, primarySwatch: colors ); + final appBarHeight = 200.0; + final scrollableKey = new ValueKey(title); // assume section titles differ Navigator.push(context, new MaterialPageRoute( builder: (BuildContext context) { return new Theme( data: theme, child: new Scaffold( - toolBar: new ToolBar(center: new Text(title)), + appBarHeight: appBarHeight, + appBarBehavior: AppBarBehavior.scroll, + scrollableKey: scrollableKey, + toolBar: new ToolBar( + flexibleSpace: (BuildContext context) => new FlexibleSpaceBar(title: new Text(title)) + ), body: new Material( child: new MaterialList( + scrollableKey: scrollableKey, + scrollablePadding: new EdgeDims.only(top: appBarHeight), type: MaterialListType.oneLine, children: (demos ?? []).map((GalleryDemo demo) { return new ListItem( @@ -116,14 +125,18 @@ class GalleryHome extends StatelessComponent { Widget build(BuildContext context) { return new Scaffold( + appBarHeight: 128.0, toolBar: new ToolBar( - bottom: new Container( - padding: const EdgeDims.only(left: 16.0, bottom: 24.0), - child: new Align( - alignment: const FractionalOffset(0.0, 1.0), - child: new Text('Flutter Gallery', style: Typography.white.headline) - ) - ) + flexibleSpace: (BuildContext context) { + return new Container( + padding: const EdgeDims.only(left: 16.0, bottom: 24.0), + height: 128.0, + child: new Align( + alignment: const FractionalOffset(0.0, 1.0), + child: new Text('Flutter Gallery', style: Typography.white.headline) + ) + ); + } ), body: new Padding( padding: const EdgeDims.all(4.0), diff --git a/examples/widgets/card_collection.dart b/examples/widgets/card_collection.dart index 34de081e83..3193303d5a 100644 --- a/examples/widgets/card_collection.dart +++ b/examples/widgets/card_collection.dart @@ -272,18 +272,21 @@ class CardCollectionState extends State { ); } - Widget _buildToolBar() { + Widget _buildToolBar(BuildContext context) { return new ToolBar( right: [ new Text(_dismissDirectionText(_dismissDirection)) ], - bottom: new Padding( - padding: const EdgeDims.only(left: 72.0), - child: new Align( - alignment: const FractionalOffset(0.0, 0.5), - child: new Text('Swipe Away: ${_cardModels.length}') - ) - ) + flexibleSpace: (_) { + return new Container( + padding: const EdgeDims.only(left: 72.0), + height: 128.0, + child: new Align( + alignment: const FractionalOffset(0.0, 0.75), + child: new Text('Swipe Away: ${_cardModels.length}', style: Theme.of(context).primaryTextTheme.title) + ) + ); + } ); } @@ -456,7 +459,7 @@ class CardCollectionState extends State { primarySwatch: _primaryColor ), child: new Scaffold( - toolBar: _buildToolBar(), + toolBar: _buildToolBar(context), drawer: _buildDrawer(), body: body ) diff --git a/examples/widgets/flutter.yaml b/examples/widgets/flutter.yaml index 773cebdad7..9f2d5cc9d2 100644 --- a/examples/widgets/flutter.yaml +++ b/examples/widgets/flutter.yaml @@ -1,6 +1,7 @@ name: widgets assets: - assets/starcircle.png + - assets/ali_connors.png material-design-icons: - name: action/account_circle - name: action/alarm diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 74c1b588e4..2183130008 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -23,6 +23,7 @@ export 'src/material/drawer_header.dart'; export 'src/material/drawer_item.dart'; export 'src/material/dropdown.dart'; export 'src/material/flat_button.dart'; +export 'src/material/flexible_space_bar.dart'; export 'src/material/floating_action_button.dart'; export 'src/material/icon.dart'; export 'src/material/icon_button.dart'; diff --git a/packages/flutter/lib/src/material/constants.dart b/packages/flutter/lib/src/material/constants.dart index 16c5a454aa..3ad1a04d6d 100644 --- a/packages/flutter/lib/src/material/constants.dart +++ b/packages/flutter/lib/src/material/constants.dart @@ -15,6 +15,10 @@ const double kStatusBarHeight = 50.0; const double kToolBarHeight = 56.0; const double kExtendedToolBarHeight = 128.0; +const double kTextTabBarHeight = 48.0; +const double kIconTabBarHeight = 48.0; +const double kTextandIconTabBarHeight = 72.0; + // https://www.google.com/design/spec/layout/metrics-keylines.html#metrics-keylines-keylines-spacing const double kListTitleHeight = 72.0; const double kListSubtitleHeight = 48.0; diff --git a/packages/flutter/lib/src/material/debug.dart b/packages/flutter/lib/src/material/debug.dart index 3c6d8ec541..45e05c2320 100644 --- a/packages/flutter/lib/src/material/debug.dart +++ b/packages/flutter/lib/src/material/debug.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'material.dart'; +import 'scaffold.dart'; bool debugCheckHasMaterial(BuildContext context) { assert(() { @@ -19,3 +20,18 @@ bool debugCheckHasMaterial(BuildContext context) { }); return true; } + + +bool debugCheckHasScaffold(BuildContext context) { + assert(() { + if (Scaffold.of(context) == null) { + Element element = context; + throw new WidgetError( + 'Missing Scaffold widget.', + '${context.widget} needs to be placed inside the body of a Scaffold widget. Ownership chain:\n${element.debugGetOwnershipChain(10)}' + ); + } + return true; + }); + return true; +} diff --git a/packages/flutter/lib/src/material/flexible_space_bar.dart b/packages/flutter/lib/src/material/flexible_space_bar.dart new file mode 100644 index 0000000000..85cdeccc95 --- /dev/null +++ b/packages/flutter/lib/src/material/flexible_space_bar.dart @@ -0,0 +1,94 @@ +// Copyright 2016 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 'dart:math' as math; + +import 'package:flutter/animation.dart'; +import 'package:flutter/widgets.dart'; + +import 'debug.dart'; +import 'constants.dart'; +import 'scaffold.dart'; +import 'theme.dart'; + +class FlexibleSpaceBar extends StatefulComponent { + FlexibleSpaceBar({ Key key, this.title, this.image }) : super(key: key); + + final Widget title; + final Widget image; + + _FlexibleSpaceBarState createState() => new _FlexibleSpaceBarState(); +} + +class _FlexibleSpaceBarState extends State { + + Widget build(BuildContext context) { + assert(debugCheckHasScaffold(context)); + final double appBarHeight = Scaffold.of(context).appBarHeight; + final Animation animation = Scaffold.of(context).appBarAnimation; + final EdgeDims toolBarPadding = MediaQuery.of(context)?.padding ?? EdgeDims.zero; + final double toolBarHeight = kToolBarHeight + toolBarPadding.top; + final List children = []; + + // background image + if (config.image != null) { + final double fadeStart = (appBarHeight - toolBarHeight * 2.0) / appBarHeight; + final double fadeEnd = (appBarHeight - toolBarHeight) / appBarHeight; + final CurvedAnimation opacityCurve = new CurvedAnimation( + parent: animation, + curve: new Interval(math.max(0.0, fadeStart), math.min(fadeEnd, 1.0)) + ); + final double parallax = new Tween(begin: 0.0, end: appBarHeight / 4.0).evaluate(animation); + children.add(new Positioned( + top: -parallax, + left: 0.0, + right: 0.0, + child: new Opacity( + opacity: new Tween(begin: 1.0, end: 0.0).evaluate(opacityCurve), + child: config.image + ) + )); + } + + // title + if (config.title != null) { + final double fadeStart = (appBarHeight - toolBarHeight) / appBarHeight; + final double fadeEnd = (appBarHeight - toolBarHeight / 2.0) / appBarHeight; + final CurvedAnimation opacityCurve = new CurvedAnimation( + parent: animation, + curve: new Interval(fadeStart, fadeEnd) + ); + TextStyle titleStyle = Theme.of(context).primaryTextTheme.title; + titleStyle = titleStyle.copyWith( + color: titleStyle.color.withAlpha(new Tween(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 scaleAndAlignEnd = (appBarHeight - toolBarHeight) / appBarHeight; + final CurvedAnimation scaleAndAlignCurve = new CurvedAnimation( + parent: animation, + curve: new Interval(0.0, scaleAndAlignEnd) + ); + children.add(new Padding( + padding: const EdgeDims.only(left: 72.0, bottom: 14.0), + child: new Align( + alignment: new Tween( + begin: new FractionalOffset(0.0, yAlignStart), + end: new FractionalOffset(0.0, yAlignEnd) + ).evaluate(scaleAndAlignCurve), + child: new ScaleTransition( + alignment: const FractionalOffset(0.0, 1.0), + scale: new Tween(begin: 1.5, end: 1.0).animate(scaleAndAlignCurve), + child: new Align( + alignment: new FractionalOffset(0.0, 1.0), + child: new DefaultTextStyle(style: titleStyle, child: config.title) + ) + ) + ) + )); + } + + return new ClipRect(child: new Stack(children: children)); + } +} diff --git a/packages/flutter/lib/src/material/icon.dart b/packages/flutter/lib/src/material/icon.dart index f9e8fd0664..9cbcd3e657 100644 --- a/packages/flutter/lib/src/material/icon.dart +++ b/packages/flutter/lib/src/material/icon.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; -import 'theme.dart'; +import 'colors.dart'; import 'icon_theme.dart'; import 'icon_theme_data.dart'; +import 'theme.dart'; enum IconSize { s18, @@ -39,7 +40,7 @@ class Icon extends StatelessComponent { final IconThemeColor colorTheme; final Color color; - String _getColorSuffix(BuildContext context) { + IconThemeColor _getIconThemeColor(BuildContext context) { IconThemeColor iconThemeColor = colorTheme; if (iconThemeColor == null) { IconThemeData iconThemeData = IconTheme.of(context); @@ -49,12 +50,7 @@ class Icon extends StatelessComponent { ThemeBrightness themeBrightness = Theme.of(context).brightness; iconThemeColor = themeBrightness == ThemeBrightness.dark ? IconThemeColor.white : IconThemeColor.black; } - switch(iconThemeColor) { - case IconThemeColor.white: - return "white"; - case IconThemeColor.black: - return "black"; - } + return iconThemeColor; } Widget build(BuildContext context) { @@ -65,13 +61,41 @@ class Icon extends StatelessComponent { category = parts[0]; subtype = parts[1]; } - String colorSuffix = _getColorSuffix(context); - int iconSize = _kIconSize[size]; + final IconThemeColor iconThemeColor = _getIconThemeColor(context); + final int iconSize = _kIconSize[size]; + + String colorSuffix; + switch(iconThemeColor) { + case IconThemeColor.black: + colorSuffix = "black"; + break; + case IconThemeColor.white: + colorSuffix = "white"; + break; + } + + Color iconColor = color; + final int iconAlpha = (255.0 * (IconTheme.of(context)?.clampedOpacity ?? 1.0)).round(); + if (iconAlpha != 255) { + if (color != null) + iconColor = color.withAlpha(iconAlpha); + else { + switch(iconThemeColor) { + case IconThemeColor.black: + iconColor = Colors.black.withAlpha(iconAlpha); + break; + case IconThemeColor.white: + iconColor = Colors.white.withAlpha(iconAlpha); + break; + } + } + } + return new AssetImage( name: '$category/ic_${subtype}_${colorSuffix}_${iconSize}dp.png', width: iconSize.toDouble(), height: iconSize.toDouble(), - color: color + color: iconColor ); } diff --git a/packages/flutter/lib/src/material/icon_theme_data.dart b/packages/flutter/lib/src/material/icon_theme_data.dart index 6fea8105ca..46dffa1519 100644 --- a/packages/flutter/lib/src/material/icon_theme_data.dart +++ b/packages/flutter/lib/src/material/icon_theme_data.dart @@ -2,15 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' as ui; + enum IconThemeColor { white, black } class IconThemeData { - const IconThemeData({ this.color }); + const IconThemeData({ this.color, this.opacity }); + final IconThemeColor color; + final double opacity; + + double get clampedOpacity => (opacity ?? 1.0).clamp(0.0, 1.0); static IconThemeData lerp(IconThemeData begin, IconThemeData end, double t) { return new IconThemeData( - color: t < 0.5 ? begin.color : end.color + color: t < 0.5 ? begin.color : end.color, + opacity: ui.lerpDouble(begin.clampedOpacity, end.clampedOpacity, t) ); } @@ -18,10 +25,10 @@ class IconThemeData { if (other is! IconThemeData) return false; final IconThemeData typedOther = other; - return color == typedOther.color; + return color == typedOther.color && opacity == typedOther.opacity; } - int get hashCode => color.hashCode; + int get hashCode => ui.hashValues(color, opacity); String toString() => '$color'; } diff --git a/packages/flutter/lib/src/material/material_list.dart b/packages/flutter/lib/src/material/material_list.dart index aa5434cf5a..3eaedbed21 100644 --- a/packages/flutter/lib/src/material/material_list.dart +++ b/packages/flutter/lib/src/material/material_list.dart @@ -27,13 +27,17 @@ class MaterialList extends StatefulComponent { this.initialScrollOffset, this.onScroll, this.type: MaterialListType.twoLine, - this.children + this.children, + this.scrollablePadding: EdgeDims.zero, + this.scrollableKey }) : super(key: key); final double initialScrollOffset; final ScrollListener onScroll; final MaterialListType type; final Iterable children; + final EdgeDims scrollablePadding; + final Key scrollableKey; _MaterialListState createState() => new _MaterialListState(); } @@ -43,11 +47,12 @@ class _MaterialListState extends State { Widget build(BuildContext context) { return new ScrollableList( + key: config.scrollableKey, initialScrollOffset: config.initialScrollOffset, scrollDirection: Axis.vertical, onScroll: config.onScroll, itemExtent: kListItemExtent[config.type], - padding: const EdgeDims.symmetric(vertical: 8.0), + padding: const EdgeDims.symmetric(vertical: 8.0) + config.scrollablePadding, scrollableListPainter: _scrollbarPainter, children: config.children ); diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 8a0356ca15..40bb8e3a05 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'bottom_sheet.dart'; +import 'constants.dart'; import 'drawer.dart'; import 'icon_button.dart'; import 'material.dart'; @@ -20,6 +21,11 @@ import 'tool_bar.dart'; const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 400); +enum AppBarBehavior { + anchor, + scroll, +} + enum _ScaffoldSlot { body, toolBar, @@ -177,13 +183,22 @@ class Scaffold extends StatefulComponent { this.toolBar, this.body, this.floatingActionButton, - this.drawer - }) : super(key: key); + this.drawer, + this.scrollableKey, + this.appBarBehavior: AppBarBehavior.anchor, + this.appBarHeight + }) : super(key: key) { + assert((appBarBehavior == AppBarBehavior.scroll) ? scrollableKey != null : true); + assert((appBarBehavior == AppBarBehavior.scroll) ? appBarHeight != null && appBarHeight > kToolBarHeight : true); + } final ToolBar toolBar; final Widget body; final Widget floatingActionButton; 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()); @@ -193,6 +208,14 @@ class Scaffold extends StatefulComponent { class ScaffoldState extends State { + // APPBAR API + + AnimationController _appBarController; + + Animation get appBarAnimation => _appBarController.view; + + double get appBarHeight => config.appBarHeight; + // DRAWER API final GlobalKey _drawerKey = new GlobalKey(); @@ -320,7 +343,13 @@ class ScaffoldState extends State { // INTERNALS + void initState() { + super.initState(); + _appBarController = new AnimationController(); + } + void dispose() { + _appBarController.stop(); _snackBarController?.stop(); _snackBarController = null; _snackBarTimer?.cancel(); @@ -335,7 +364,7 @@ class ScaffoldState extends State { bool _shouldShowBackArrow; - Widget _getModifiedToolBar(EdgeDims padding) { + Widget _getModifiedToolBar({ EdgeDims padding, double foregroundOpacity: 1.0, int elevation: 4 }) { ToolBar toolBar = config.toolBar; if (toolBar == null) return null; @@ -360,11 +389,73 @@ class ScaffoldState extends State { } } return toolBar.copyWith( + elevation: elevation, padding: toolBarPadding, + foregroundOpacity: foregroundOpacity, left: left ); } + double _scrollOffset = 0.0; + double _scrollOffsetDelta = 0.0; + double _floatingAppBarHeight = 0.0; + + bool _handleScrollNotification(ScrollNotification notification) { + final double newScrollOffset = notification.scrollable.scrollOffset; + if (config.scrollableKey != null && config.scrollableKey == notification.scrollable.config.key) + setState(() { + _scrollOffsetDelta = _scrollOffset - newScrollOffset; + _scrollOffset = newScrollOffset; + }); + return false; + } + + double _toolBarOpacity(double progress) { + // The value of progress is 1.0 if the entire (padded) toolbar is visible, 0.0 + // if the toolbar's height is zero. + return new Tween(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 _buildScrollableAppBar(BuildContext context) { + final EdgeDims toolBarPadding = MediaQuery.of(context)?.padding ?? EdgeDims.zero; + final double toolBarHeight = kToolBarHeight + toolBarPadding.top; + Widget appBar; + + if (_scrollOffset <= appBarHeight && _scrollOffset >= appBarHeight - toolBarHeight) { + // scrolled to the top, only the toolbar is (partially) visible + final double height = math.max(_floatingAppBarHeight, appBarHeight - _scrollOffset); + final double opacity = _toolBarOpacity(1.0 - ((toolBarHeight - height) / toolBarHeight)); + _appBarController.value = (appBarHeight - height) / appBarHeight; + appBar = new SizedBox( + height: height, + child: _getModifiedToolBar(padding: toolBarPadding, foregroundOpacity: opacity) + ); + } else if (_scrollOffset > appBarHeight) { + // scrolled down, show the "floating" toolbar + _floatingAppBarHeight = (_floatingAppBarHeight + _scrollOffsetDelta).clamp(0.0, toolBarHeight); + final toolBarOpacity = _toolBarOpacity(_floatingAppBarHeight / toolBarHeight); + _appBarController.value = (appBarHeight - _floatingAppBarHeight) / appBarHeight; + appBar = new SizedBox( + height: _floatingAppBarHeight, + child: _getModifiedToolBar(padding: toolBarPadding, foregroundOpacity: toolBarOpacity) + ); + } else { + // _scrollOffset < appBarHeight - toolBarHeight, scrolled to the top, flexible space is visible + final double height = appBarHeight - _scrollOffset.clamp(0.0, appBarHeight); + _appBarController.value = (appBarHeight - height) / appBarHeight; + appBar = new SizedBox( + height: height, + child: _getModifiedToolBar(padding: toolBarPadding, elevation: 0) + ); + _floatingAppBarHeight = 0.0; + } + + return appBar; + } + Widget build(BuildContext context) { EdgeDims padding = MediaQuery.of(context)?.padding ?? EdgeDims.zero; @@ -381,7 +472,14 @@ class ScaffoldState extends State { final List children = new List(); _addIfNonNull(children, config.body, _ScaffoldSlot.body); - _addIfNonNull(children, _getModifiedToolBar(padding), _ScaffoldSlot.toolBar); + if (config.appBarBehavior == AppBarBehavior.anchor) { + Widget toolBar = new ConstrainedBox( + child: _getModifiedToolBar(padding: padding), + constraints: new BoxConstraints(maxHeight: config.appBarHeight ?? kExtendedToolBarHeight + padding.top) + ); + _addIfNonNull(children, toolBar, _ScaffoldSlot.toolBar); + } + // Otherwise the ToolBar will be part of a [toolbar, body] Stack. See AppBarBehavior.scroll below. if (_currentBottomSheet != null || (_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)) { @@ -418,14 +516,39 @@ class ScaffoldState extends State { )); } - return new Material( - child: new CustomMultiChildLayout( + Widget application; + + if (config.appBarBehavior == AppBarBehavior.scroll) { + double overScroll = _scrollOffset.clamp(double.NEGATIVE_INFINITY, 0.0); + application = new NotificationListener( + onNotification: _handleScrollNotification, + child: new Stack( + children: [ + new CustomMultiChildLayout( + children: children, + delegate: new _ScaffoldLayout( + padding: EdgeDims.zero + ) + ), + new Positioned( + top: -overScroll, + left: 0.0, + right: 0.0, + child: _buildScrollableAppBar(context) + ) + ] + ) + ); + } else { + application = new CustomMultiChildLayout( children: children, delegate: new _ScaffoldLayout( padding: padding ) - ) - ); + ); + } + + return new Material(child: application); } } diff --git a/packages/flutter/lib/src/material/tool_bar.dart b/packages/flutter/lib/src/material/tool_bar.dart index 7b2cbe47fe..4d8920047f 100644 --- a/packages/flutter/lib/src/material/tool_bar.dart +++ b/packages/flutter/lib/src/material/tool_bar.dart @@ -17,18 +17,23 @@ class ToolBar extends StatelessComponent { this.left, this.center, this.right, - this.bottom, + this.flexibleSpace, + this.foregroundOpacity: 1.0, this.tabBar, this.elevation: 4, this.backgroundColor, this.textTheme, this.padding: EdgeDims.zero - }) : super(key: key); + }) : super(key: key) { + assert((flexibleSpace != null) ? tabBar == null : true); + assert((tabBar != null) ? flexibleSpace == null : true); + } final Widget left; final Widget center; final List right; - final Widget bottom; + final WidgetBuilder flexibleSpace; + final double foregroundOpacity; final Widget tabBar; final int elevation; final Color backgroundColor; @@ -40,7 +45,8 @@ class ToolBar extends StatelessComponent { Widget left, Widget center, List right, - Widget bottom, + WidgetBuilder flexibleSpace, + double foregroundOpacity, int elevation, Color backgroundColor, TextTheme textTheme, @@ -51,7 +57,8 @@ class ToolBar extends StatelessComponent { left: left ?? this.left, center: center ?? this.center, right: right ?? this.right, - bottom: bottom ?? this.bottom, + flexibleSpace: flexibleSpace ?? this.flexibleSpace, + foregroundOpacity: foregroundOpacity ?? this.foregroundOpacity, tabBar: tabBar ?? this.tabBar, elevation: elevation ?? this.elevation, backgroundColor: backgroundColor ?? this.backgroundColor, @@ -76,10 +83,24 @@ class ToolBar extends StatelessComponent { sideStyle ??= primaryTextTheme.body2; } - final List firstRow = []; + if (foregroundOpacity != 1.0) { + final int alpha = (foregroundOpacity.clamp(0.0, 1.0) * 255.0).round(); + if (centerStyle?.color != null) + centerStyle = centerStyle.copyWith(color: centerStyle.color.withAlpha(alpha)); + 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 + ); + } + } + + final List toolBarRow = []; if (left != null) - firstRow.add(left); - firstRow.add( + toolBarRow.add(left); + toolBarRow.add( new Flexible( child: new Padding( padding: new EdgeDims.only(left: 24.0), @@ -88,45 +109,55 @@ class ToolBar extends StatelessComponent { ) ); if (right != null) - firstRow.addAll(right); - - final List rows = [ - new Container( - height: kToolBarHeight, - child: new DefaultTextStyle( - style: sideStyle, - child: new Row(children: firstRow) - ) - ) - ]; - if (bottom != null) { - rows.add( - new DefaultTextStyle( - style: centerStyle, - child: new Container( - height: kExtendedToolBarHeight - kToolBarHeight, - child: bottom - ) - ) - ); - } - if (tabBar != null) - rows.add(tabBar); + toolBarRow.addAll(right); EdgeDims combinedPadding = new EdgeDims.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), + child: new Padding( + padding: new EdgeDims.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 EdgeDims.only(top: combinedPadding.top, bottom: combinedPadding.bottom), + child: new Row(children: toolBarRow) + ) + ) + ) + ) + ) + ); + + Widget appBar = toolBar; + if (tabBar != null) { + appBar = new Column( + justifyContent: FlexJustifyContent.collapse, + children: [toolBar, tabBar] + ); + } else if (flexibleSpace != null) { + appBar = new Stack( + children: [ + flexibleSpace(context), + new Align(child: toolBar, alignment: const FractionalOffset(0.0, 0.0)) + ] + ); + } + Widget contents = new Material( color: color, elevation: elevation, - child: new Container( - padding: combinedPadding, - child: new Column( - children: rows, - justifyContent: FlexJustifyContent.collapse - ) - ) + child: appBar ); if (iconThemeData != null) diff --git a/packages/flutter/lib/src/rendering/list.dart b/packages/flutter/lib/src/rendering/list.dart index e5c20bd0f8..af9522b1b5 100644 --- a/packages/flutter/lib/src/rendering/list.dart +++ b/packages/flutter/lib/src/rendering/list.dart @@ -151,7 +151,7 @@ class RenderList extends RenderVirtualViewport implements HasScr break; case Axis.horizontal: itemWidth = itemExtent ?? size.width; - itemHeight = math.max(0, size.height - (padding == null ? 0.0 : padding.vertical)); + itemHeight = math.max(0.0, size.height - (padding == null ? 0.0 : padding.vertical)); x = padding != null ? padding.left : 0.0; dx = itemWidth; break; diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index f30c50a996..7c949aceff 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -377,12 +377,16 @@ class ScrollableViewport extends Scrollable { this.child, double initialScrollOffset, Axis scrollDirection: Axis.vertical, - ScrollListener onScroll + ScrollListener onScrollStart, + ScrollListener onScroll, + ScrollListener onScrollEnd }) : super( key: key, scrollDirection: scrollDirection, initialScrollOffset: initialScrollOffset, - onScroll: onScroll + onScrollStart: onScrollStart, + onScroll: onScroll, + onScrollEnd: onScrollEnd ); final Widget child; diff --git a/packages/flutter/lib/src/widgets/scrollable_list.dart b/packages/flutter/lib/src/widgets/scrollable_list.dart index 9fc21d5f1a..60fbddc3f6 100644 --- a/packages/flutter/lib/src/widgets/scrollable_list.dart +++ b/packages/flutter/lib/src/widgets/scrollable_list.dart @@ -158,11 +158,12 @@ class _ListViewportElement extends VirtualViewportElement { void layout(BoxConstraints constraints) { final int length = renderObject.virtualChildCount; final double itemExtent = widget.itemExtent; + final EdgeDims padding = widget.padding ?? EdgeDims.zero; - double contentExtent = widget.itemExtent * length; + double contentExtent = widget.itemExtent * length + padding.top + padding.bottom; double containerExtent = _getContainerExtentFromRenderObject(); - _materializedChildBase = math.max(0, widget.startOffset ~/ itemExtent); + _materializedChildBase = math.max(0, (widget.startOffset - padding.top) ~/ itemExtent); int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / itemExtent).ceil()); if (!widget.itemsWrap) {