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,
|
brightness: ThemeBrightness.light,
|
||||||
primarySwatch: colors
|
primarySwatch: colors
|
||||||
);
|
);
|
||||||
|
final appBarHeight = 200.0;
|
||||||
|
final scrollableKey = new ValueKey<String>(title); // assume section titles differ
|
||||||
Navigator.push(context, new MaterialPageRoute(
|
Navigator.push(context, new MaterialPageRoute(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return new Theme(
|
return new Theme(
|
||||||
data: theme,
|
data: theme,
|
||||||
child: new Scaffold(
|
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(
|
body: new Material(
|
||||||
child: new MaterialList(
|
child: new MaterialList(
|
||||||
|
scrollableKey: scrollableKey,
|
||||||
|
scrollablePadding: new EdgeDims.only(top: appBarHeight),
|
||||||
type: MaterialListType.oneLine,
|
type: MaterialListType.oneLine,
|
||||||
children: (demos ?? <GalleryDemo>[]).map((GalleryDemo demo) {
|
children: (demos ?? <GalleryDemo>[]).map((GalleryDemo demo) {
|
||||||
return new ListItem(
|
return new ListItem(
|
||||||
@ -116,14 +125,18 @@ class GalleryHome extends StatelessComponent {
|
|||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
|
appBarHeight: 128.0,
|
||||||
toolBar: new ToolBar(
|
toolBar: new ToolBar(
|
||||||
bottom: new Container(
|
flexibleSpace: (BuildContext context) {
|
||||||
|
return new Container(
|
||||||
padding: const EdgeDims.only(left: 16.0, bottom: 24.0),
|
padding: const EdgeDims.only(left: 16.0, bottom: 24.0),
|
||||||
|
height: 128.0,
|
||||||
child: new Align(
|
child: new Align(
|
||||||
alignment: const FractionalOffset(0.0, 1.0),
|
alignment: const FractionalOffset(0.0, 1.0),
|
||||||
child: new Text('Flutter Gallery', style: Typography.white.headline)
|
child: new Text('Flutter Gallery', style: Typography.white.headline)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
}
|
||||||
),
|
),
|
||||||
body: new Padding(
|
body: new Padding(
|
||||||
padding: const EdgeDims.all(4.0),
|
padding: const EdgeDims.all(4.0),
|
||||||
|
@ -272,20 +272,23 @@ class CardCollectionState extends State<CardCollection> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildToolBar() {
|
Widget _buildToolBar(BuildContext context) {
|
||||||
return new ToolBar(
|
return new ToolBar(
|
||||||
right: <Widget>[
|
right: <Widget>[
|
||||||
new Text(_dismissDirectionText(_dismissDirection))
|
new Text(_dismissDirectionText(_dismissDirection))
|
||||||
],
|
],
|
||||||
bottom: new Padding(
|
flexibleSpace: (_) {
|
||||||
|
return new Container(
|
||||||
padding: const EdgeDims.only(left: 72.0),
|
padding: const EdgeDims.only(left: 72.0),
|
||||||
|
height: 128.0,
|
||||||
child: new Align(
|
child: new Align(
|
||||||
alignment: const FractionalOffset(0.0, 0.5),
|
alignment: const FractionalOffset(0.0, 0.75),
|
||||||
child: new Text('Swipe Away: ${_cardModels.length}')
|
child: new Text('Swipe Away: ${_cardModels.length}', style: Theme.of(context).primaryTextTheme.title)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildCard(BuildContext context, int index) {
|
Widget _buildCard(BuildContext context, int index) {
|
||||||
if (index >= _cardModels.length)
|
if (index >= _cardModels.length)
|
||||||
@ -456,7 +459,7 @@ class CardCollectionState extends State<CardCollection> {
|
|||||||
primarySwatch: _primaryColor
|
primarySwatch: _primaryColor
|
||||||
),
|
),
|
||||||
child: new Scaffold(
|
child: new Scaffold(
|
||||||
toolBar: _buildToolBar(),
|
toolBar: _buildToolBar(context),
|
||||||
drawer: _buildDrawer(),
|
drawer: _buildDrawer(),
|
||||||
body: body
|
body: body
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
name: widgets
|
name: widgets
|
||||||
assets:
|
assets:
|
||||||
- assets/starcircle.png
|
- assets/starcircle.png
|
||||||
|
- assets/ali_connors.png
|
||||||
material-design-icons:
|
material-design-icons:
|
||||||
- name: action/account_circle
|
- name: action/account_circle
|
||||||
- name: action/alarm
|
- name: action/alarm
|
||||||
|
@ -23,6 +23,7 @@ export 'src/material/drawer_header.dart';
|
|||||||
export 'src/material/drawer_item.dart';
|
export 'src/material/drawer_item.dart';
|
||||||
export 'src/material/dropdown.dart';
|
export 'src/material/dropdown.dart';
|
||||||
export 'src/material/flat_button.dart';
|
export 'src/material/flat_button.dart';
|
||||||
|
export 'src/material/flexible_space_bar.dart';
|
||||||
export 'src/material/floating_action_button.dart';
|
export 'src/material/floating_action_button.dart';
|
||||||
export 'src/material/icon.dart';
|
export 'src/material/icon.dart';
|
||||||
export 'src/material/icon_button.dart';
|
export 'src/material/icon_button.dart';
|
||||||
|
@ -15,6 +15,10 @@ const double kStatusBarHeight = 50.0;
|
|||||||
const double kToolBarHeight = 56.0;
|
const double kToolBarHeight = 56.0;
|
||||||
const double kExtendedToolBarHeight = 128.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
|
// https://www.google.com/design/spec/layout/metrics-keylines.html#metrics-keylines-keylines-spacing
|
||||||
const double kListTitleHeight = 72.0;
|
const double kListTitleHeight = 72.0;
|
||||||
const double kListSubtitleHeight = 48.0;
|
const double kListSubtitleHeight = 48.0;
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
|
import 'scaffold.dart';
|
||||||
|
|
||||||
bool debugCheckHasMaterial(BuildContext context) {
|
bool debugCheckHasMaterial(BuildContext context) {
|
||||||
assert(() {
|
assert(() {
|
||||||
@ -19,3 +20,18 @@ bool debugCheckHasMaterial(BuildContext context) {
|
|||||||
});
|
});
|
||||||
return true;
|
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 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'theme.dart';
|
import 'colors.dart';
|
||||||
import 'icon_theme.dart';
|
import 'icon_theme.dart';
|
||||||
import 'icon_theme_data.dart';
|
import 'icon_theme_data.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
enum IconSize {
|
enum IconSize {
|
||||||
s18,
|
s18,
|
||||||
@ -39,7 +40,7 @@ class Icon extends StatelessComponent {
|
|||||||
final IconThemeColor colorTheme;
|
final IconThemeColor colorTheme;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
String _getColorSuffix(BuildContext context) {
|
IconThemeColor _getIconThemeColor(BuildContext context) {
|
||||||
IconThemeColor iconThemeColor = colorTheme;
|
IconThemeColor iconThemeColor = colorTheme;
|
||||||
if (iconThemeColor == null) {
|
if (iconThemeColor == null) {
|
||||||
IconThemeData iconThemeData = IconTheme.of(context);
|
IconThemeData iconThemeData = IconTheme.of(context);
|
||||||
@ -49,12 +50,7 @@ class Icon extends StatelessComponent {
|
|||||||
ThemeBrightness themeBrightness = Theme.of(context).brightness;
|
ThemeBrightness themeBrightness = Theme.of(context).brightness;
|
||||||
iconThemeColor = themeBrightness == ThemeBrightness.dark ? IconThemeColor.white : IconThemeColor.black;
|
iconThemeColor = themeBrightness == ThemeBrightness.dark ? IconThemeColor.white : IconThemeColor.black;
|
||||||
}
|
}
|
||||||
switch(iconThemeColor) {
|
return iconThemeColor;
|
||||||
case IconThemeColor.white:
|
|
||||||
return "white";
|
|
||||||
case IconThemeColor.black:
|
|
||||||
return "black";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -65,13 +61,41 @@ class Icon extends StatelessComponent {
|
|||||||
category = parts[0];
|
category = parts[0];
|
||||||
subtype = parts[1];
|
subtype = parts[1];
|
||||||
}
|
}
|
||||||
String colorSuffix = _getColorSuffix(context);
|
final IconThemeColor iconThemeColor = _getIconThemeColor(context);
|
||||||
int iconSize = _kIconSize[size];
|
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(
|
return new AssetImage(
|
||||||
name: '$category/ic_${subtype}_${colorSuffix}_${iconSize}dp.png',
|
name: '$category/ic_${subtype}_${colorSuffix}_${iconSize}dp.png',
|
||||||
width: iconSize.toDouble(),
|
width: iconSize.toDouble(),
|
||||||
height: 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
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
enum IconThemeColor { white, black }
|
enum IconThemeColor { white, black }
|
||||||
|
|
||||||
class IconThemeData {
|
class IconThemeData {
|
||||||
const IconThemeData({ this.color });
|
const IconThemeData({ this.color, this.opacity });
|
||||||
|
|
||||||
final IconThemeColor color;
|
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) {
|
static IconThemeData lerp(IconThemeData begin, IconThemeData end, double t) {
|
||||||
return new IconThemeData(
|
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)
|
if (other is! IconThemeData)
|
||||||
return false;
|
return false;
|
||||||
final IconThemeData typedOther = other;
|
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';
|
String toString() => '$color';
|
||||||
}
|
}
|
||||||
|
@ -27,13 +27,17 @@ class MaterialList extends StatefulComponent {
|
|||||||
this.initialScrollOffset,
|
this.initialScrollOffset,
|
||||||
this.onScroll,
|
this.onScroll,
|
||||||
this.type: MaterialListType.twoLine,
|
this.type: MaterialListType.twoLine,
|
||||||
this.children
|
this.children,
|
||||||
|
this.scrollablePadding: EdgeDims.zero,
|
||||||
|
this.scrollableKey
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final double initialScrollOffset;
|
final double initialScrollOffset;
|
||||||
final ScrollListener onScroll;
|
final ScrollListener onScroll;
|
||||||
final MaterialListType type;
|
final MaterialListType type;
|
||||||
final Iterable<Widget> children;
|
final Iterable<Widget> children;
|
||||||
|
final EdgeDims scrollablePadding;
|
||||||
|
final Key scrollableKey;
|
||||||
|
|
||||||
_MaterialListState createState() => new _MaterialListState();
|
_MaterialListState createState() => new _MaterialListState();
|
||||||
}
|
}
|
||||||
@ -43,11 +47,12 @@ class _MaterialListState extends State<MaterialList> {
|
|||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new ScrollableList(
|
return new ScrollableList(
|
||||||
|
key: config.scrollableKey,
|
||||||
initialScrollOffset: config.initialScrollOffset,
|
initialScrollOffset: config.initialScrollOffset,
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
onScroll: config.onScroll,
|
onScroll: config.onScroll,
|
||||||
itemExtent: kListItemExtent[config.type],
|
itemExtent: kListItemExtent[config.type],
|
||||||
padding: const EdgeDims.symmetric(vertical: 8.0),
|
padding: const EdgeDims.symmetric(vertical: 8.0) + config.scrollablePadding,
|
||||||
scrollableListPainter: _scrollbarPainter,
|
scrollableListPainter: _scrollbarPainter,
|
||||||
children: config.children
|
children: config.children
|
||||||
);
|
);
|
||||||
|
@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'bottom_sheet.dart';
|
import 'bottom_sheet.dart';
|
||||||
|
import 'constants.dart';
|
||||||
import 'drawer.dart';
|
import 'drawer.dart';
|
||||||
import 'icon_button.dart';
|
import 'icon_button.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
@ -20,6 +21,11 @@ import 'tool_bar.dart';
|
|||||||
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
|
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
|
||||||
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 400);
|
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 400);
|
||||||
|
|
||||||
|
enum AppBarBehavior {
|
||||||
|
anchor,
|
||||||
|
scroll,
|
||||||
|
}
|
||||||
|
|
||||||
enum _ScaffoldSlot {
|
enum _ScaffoldSlot {
|
||||||
body,
|
body,
|
||||||
toolBar,
|
toolBar,
|
||||||
@ -177,13 +183,22 @@ class Scaffold extends StatefulComponent {
|
|||||||
this.toolBar,
|
this.toolBar,
|
||||||
this.body,
|
this.body,
|
||||||
this.floatingActionButton,
|
this.floatingActionButton,
|
||||||
this.drawer
|
this.drawer,
|
||||||
}) : super(key: key);
|
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 ToolBar toolBar;
|
||||||
final Widget body;
|
final Widget body;
|
||||||
final Widget floatingActionButton;
|
final Widget floatingActionButton;
|
||||||
final Widget drawer;
|
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.
|
/// The state from the closest instance of this class that encloses the given context.
|
||||||
static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
|
static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
|
||||||
@ -193,6 +208,14 @@ class Scaffold extends StatefulComponent {
|
|||||||
|
|
||||||
class ScaffoldState extends State<Scaffold> {
|
class ScaffoldState extends State<Scaffold> {
|
||||||
|
|
||||||
|
// APPBAR API
|
||||||
|
|
||||||
|
AnimationController _appBarController;
|
||||||
|
|
||||||
|
Animation<double> get appBarAnimation => _appBarController.view;
|
||||||
|
|
||||||
|
double get appBarHeight => config.appBarHeight;
|
||||||
|
|
||||||
// DRAWER API
|
// DRAWER API
|
||||||
|
|
||||||
final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>();
|
final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>();
|
||||||
@ -320,7 +343,13 @@ class ScaffoldState extends State<Scaffold> {
|
|||||||
|
|
||||||
// INTERNALS
|
// INTERNALS
|
||||||
|
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_appBarController = new AnimationController();
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_appBarController.stop();
|
||||||
_snackBarController?.stop();
|
_snackBarController?.stop();
|
||||||
_snackBarController = null;
|
_snackBarController = null;
|
||||||
_snackBarTimer?.cancel();
|
_snackBarTimer?.cancel();
|
||||||
@ -335,7 +364,7 @@ class ScaffoldState extends State<Scaffold> {
|
|||||||
|
|
||||||
bool _shouldShowBackArrow;
|
bool _shouldShowBackArrow;
|
||||||
|
|
||||||
Widget _getModifiedToolBar(EdgeDims padding) {
|
Widget _getModifiedToolBar({ EdgeDims padding, double foregroundOpacity: 1.0, int elevation: 4 }) {
|
||||||
ToolBar toolBar = config.toolBar;
|
ToolBar toolBar = config.toolBar;
|
||||||
if (toolBar == null)
|
if (toolBar == null)
|
||||||
return null;
|
return null;
|
||||||
@ -360,11 +389,73 @@ class ScaffoldState extends State<Scaffold> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return toolBar.copyWith(
|
return toolBar.copyWith(
|
||||||
|
elevation: elevation,
|
||||||
padding: toolBarPadding,
|
padding: toolBarPadding,
|
||||||
|
foregroundOpacity: foregroundOpacity,
|
||||||
left: left
|
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) {
|
Widget build(BuildContext context) {
|
||||||
EdgeDims padding = MediaQuery.of(context)?.padding ?? EdgeDims.zero;
|
EdgeDims padding = MediaQuery.of(context)?.padding ?? EdgeDims.zero;
|
||||||
|
|
||||||
@ -381,7 +472,14 @@ class ScaffoldState extends State<Scaffold> {
|
|||||||
|
|
||||||
final List<LayoutId> children = new List<LayoutId>();
|
final List<LayoutId> children = new List<LayoutId>();
|
||||||
_addIfNonNull(children, config.body, _ScaffoldSlot.body);
|
_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 ||
|
if (_currentBottomSheet != null ||
|
||||||
(_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)) {
|
(_dismissedBottomSheets != null && _dismissedBottomSheets.isNotEmpty)) {
|
||||||
@ -418,15 +516,40 @@ class ScaffoldState extends State<Scaffold> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Material(
|
Widget application;
|
||||||
child: new CustomMultiChildLayout(
|
|
||||||
|
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,
|
children: children,
|
||||||
delegate: new _ScaffoldLayout(
|
delegate: new _ScaffoldLayout(
|
||||||
padding: padding
|
padding: padding
|
||||||
)
|
)
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new Material(child: application);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScaffoldFeatureController<T extends Widget> {
|
class ScaffoldFeatureController<T extends Widget> {
|
||||||
|
@ -17,18 +17,23 @@ class ToolBar extends StatelessComponent {
|
|||||||
this.left,
|
this.left,
|
||||||
this.center,
|
this.center,
|
||||||
this.right,
|
this.right,
|
||||||
this.bottom,
|
this.flexibleSpace,
|
||||||
|
this.foregroundOpacity: 1.0,
|
||||||
this.tabBar,
|
this.tabBar,
|
||||||
this.elevation: 4,
|
this.elevation: 4,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.textTheme,
|
this.textTheme,
|
||||||
this.padding: EdgeDims.zero
|
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 left;
|
||||||
final Widget center;
|
final Widget center;
|
||||||
final List<Widget> right;
|
final List<Widget> right;
|
||||||
final Widget bottom;
|
final WidgetBuilder flexibleSpace;
|
||||||
|
final double foregroundOpacity;
|
||||||
final Widget tabBar;
|
final Widget tabBar;
|
||||||
final int elevation;
|
final int elevation;
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
@ -40,7 +45,8 @@ class ToolBar extends StatelessComponent {
|
|||||||
Widget left,
|
Widget left,
|
||||||
Widget center,
|
Widget center,
|
||||||
List<Widget> right,
|
List<Widget> right,
|
||||||
Widget bottom,
|
WidgetBuilder flexibleSpace,
|
||||||
|
double foregroundOpacity,
|
||||||
int elevation,
|
int elevation,
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
TextTheme textTheme,
|
TextTheme textTheme,
|
||||||
@ -51,7 +57,8 @@ class ToolBar extends StatelessComponent {
|
|||||||
left: left ?? this.left,
|
left: left ?? this.left,
|
||||||
center: center ?? this.center,
|
center: center ?? this.center,
|
||||||
right: right ?? this.right,
|
right: right ?? this.right,
|
||||||
bottom: bottom ?? this.bottom,
|
flexibleSpace: flexibleSpace ?? this.flexibleSpace,
|
||||||
|
foregroundOpacity: foregroundOpacity ?? this.foregroundOpacity,
|
||||||
tabBar: tabBar ?? this.tabBar,
|
tabBar: tabBar ?? this.tabBar,
|
||||||
elevation: elevation ?? this.elevation,
|
elevation: elevation ?? this.elevation,
|
||||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||||
@ -76,10 +83,24 @@ class ToolBar extends StatelessComponent {
|
|||||||
sideStyle ??= primaryTextTheme.body2;
|
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)
|
if (left != null)
|
||||||
firstRow.add(left);
|
toolBarRow.add(left);
|
||||||
firstRow.add(
|
toolBarRow.add(
|
||||||
new Flexible(
|
new Flexible(
|
||||||
child: new Padding(
|
child: new Padding(
|
||||||
padding: new EdgeDims.only(left: 24.0),
|
padding: new EdgeDims.only(left: 24.0),
|
||||||
@ -88,45 +109,55 @@ class ToolBar extends StatelessComponent {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (right != null)
|
if (right != null)
|
||||||
firstRow.addAll(right);
|
toolBarRow.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);
|
|
||||||
|
|
||||||
EdgeDims combinedPadding = new EdgeDims.symmetric(horizontal: 8.0);
|
EdgeDims combinedPadding = new EdgeDims.symmetric(horizontal: 8.0);
|
||||||
if (padding != null)
|
if (padding != null)
|
||||||
combinedPadding += padding;
|
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(
|
Widget contents = new Material(
|
||||||
color: color,
|
color: color,
|
||||||
elevation: elevation,
|
elevation: elevation,
|
||||||
child: new Container(
|
child: appBar
|
||||||
padding: combinedPadding,
|
|
||||||
child: new Column(
|
|
||||||
children: rows,
|
|
||||||
justifyContent: FlexJustifyContent.collapse
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (iconThemeData != null)
|
if (iconThemeData != null)
|
||||||
|
@ -151,7 +151,7 @@ class RenderList extends RenderVirtualViewport<ListParentData> implements HasScr
|
|||||||
break;
|
break;
|
||||||
case Axis.horizontal:
|
case Axis.horizontal:
|
||||||
itemWidth = itemExtent ?? size.width;
|
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;
|
x = padding != null ? padding.left : 0.0;
|
||||||
dx = itemWidth;
|
dx = itemWidth;
|
||||||
break;
|
break;
|
||||||
|
@ -377,12 +377,16 @@ class ScrollableViewport extends Scrollable {
|
|||||||
this.child,
|
this.child,
|
||||||
double initialScrollOffset,
|
double initialScrollOffset,
|
||||||
Axis scrollDirection: Axis.vertical,
|
Axis scrollDirection: Axis.vertical,
|
||||||
ScrollListener onScroll
|
ScrollListener onScrollStart,
|
||||||
|
ScrollListener onScroll,
|
||||||
|
ScrollListener onScrollEnd
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
scrollDirection: scrollDirection,
|
scrollDirection: scrollDirection,
|
||||||
initialScrollOffset: initialScrollOffset,
|
initialScrollOffset: initialScrollOffset,
|
||||||
onScroll: onScroll
|
onScrollStart: onScrollStart,
|
||||||
|
onScroll: onScroll,
|
||||||
|
onScrollEnd: onScrollEnd
|
||||||
);
|
);
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
@ -158,11 +158,12 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> {
|
|||||||
void layout(BoxConstraints constraints) {
|
void layout(BoxConstraints constraints) {
|
||||||
final int length = renderObject.virtualChildCount;
|
final int length = renderObject.virtualChildCount;
|
||||||
final double itemExtent = widget.itemExtent;
|
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();
|
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());
|
int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / itemExtent).ceil());
|
||||||
|
|
||||||
if (!widget.itemsWrap) {
|
if (!widget.itemsWrap) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user