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:
parent
687ff57e24
commit
6bc65e0373
@ -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),
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
name: widgets
|
||||
assets:
|
||||
- assets/starcircle.png
|
||||
- assets/ali_connors.png
|
||||
material-design-icons:
|
||||
- name: action/account_circle
|
||||
- name: action/alarm
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
94
packages/flutter/lib/src/material/flexible_space_bar.dart
Normal file
94
packages/flutter/lib/src/material/flexible_space_bar.dart
Normal 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));
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user