Add support for the appbar behavior described in the "Flexible space with image" section of https://www.google.com/design/spec/patterns/scrolling-techniques.html#scrolling-techniques-scrolling.

This commit is contained in:
Hans Muller 2016-02-02 12:17:55 -08:00
parent 687ff57e24
commit 6bc65e0373
15 changed files with 413 additions and 86 deletions

View File

@ -46,14 +46,23 @@ class GallerySection extends StatelessComponent {
brightness: ThemeBrightness.light,
primarySwatch: colors
);
final appBarHeight = 200.0;
final scrollableKey = new ValueKey<String>(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 ?? <GalleryDemo>[]).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),

View File

@ -272,18 +272,21 @@ class CardCollectionState extends State<CardCollection> {
);
}
Widget _buildToolBar() {
Widget _buildToolBar(BuildContext context) {
return new ToolBar(
right: <Widget>[
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<CardCollection> {
primarySwatch: _primaryColor
),
child: new Scaffold(
toolBar: _buildToolBar(),
toolBar: _buildToolBar(context),
drawer: _buildDrawer(),
body: body
)

View File

@ -1,6 +1,7 @@
name: widgets
assets:
- assets/starcircle.png
- assets/ali_connors.png
material-design-icons:
- name: action/account_circle
- name: action/alarm

View File

@ -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';

View File

@ -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;

View File

@ -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;
}

View File

@ -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<FlexibleSpaceBar> {
Widget build(BuildContext context) {
assert(debugCheckHasScaffold(context));
final double appBarHeight = Scaffold.of(context).appBarHeight;
final Animation<double> animation = Scaffold.of(context).appBarAnimation;
final EdgeDims toolBarPadding = MediaQuery.of(context)?.padding ?? EdgeDims.zero;
final double toolBarHeight = kToolBarHeight + toolBarPadding.top;
final List<Widget> children = <Widget>[];
// 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<double>(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<double>(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<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 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<FractionalOffset>(
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<double>(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));
}
}

View File

@ -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
);
}

View File

@ -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';
}

View File

@ -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<Widget> children;
final EdgeDims scrollablePadding;
final Key scrollableKey;
_MaterialListState createState() => new _MaterialListState();
}
@ -43,11 +47,12 @@ class _MaterialListState extends State<MaterialList> {
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
);

View File

@ -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<ScaffoldState>());
@ -193,6 +208,14 @@ class Scaffold extends StatefulComponent {
class ScaffoldState extends State<Scaffold> {
// APPBAR API
AnimationController _appBarController;
Animation<double> get appBarAnimation => _appBarController.view;
double get appBarHeight => config.appBarHeight;
// DRAWER API
final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>();
@ -320,7 +343,13 @@ class ScaffoldState extends State<Scaffold> {
// 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<Scaffold> {
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<Scaffold> {
}
}
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<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 _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<Scaffold> {
final List<LayoutId> children = new List<LayoutId>();
_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<Scaffold> {
));
}
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<ScrollNotification>(
onNotification: _handleScrollNotification,
child: new Stack(
children: <Widget> [
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);
}
}

View File

@ -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<Widget> 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<Widget> 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<Widget> firstRow = <Widget>[];
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<Widget> toolBarRow = <Widget>[];
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<Widget> rows = <Widget>[
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: <Widget>[toolBar, tabBar]
);
} else if (flexibleSpace != null) {
appBar = new Stack(
children: <Widget>[
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)

View File

@ -151,7 +151,7 @@ class RenderList extends RenderVirtualViewport<ListParentData> 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;

View File

@ -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;

View File

@ -158,11 +158,12 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> {
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) {