Commit 1a0bdf41 authored by Ian Hickson's avatar Ian Hickson

Merge pull request #621 from Hixie/material-ink

Model ink splashes more physically
parents 366d078d ee802bfe
......@@ -58,26 +58,27 @@ class AnimationTiming {
}
}
/// An animated variable with a concrete type
/// An animated variable with a concrete type.
class AnimatedValue<T extends dynamic> extends AnimationTiming implements Animatable {
AnimatedValue(this.begin, { this.end, Curve curve, Curve reverseCurve })
: super(curve: curve, reverseCurve: reverseCurve) {
value = begin;
}
/// The current value of this variable
/// The current value of this variable.
T value;
/// The value this variable has at the beginning of the animation
/// The value this variable has at the beginning of the animation.
T begin;
/// The value this variable has at the end of the animation
/// The value this variable has at the end of the animation.
T end;
/// Returns the value this variable has at the given animation clock value
/// Returns the value this variable has at the given animation clock value.
T lerp(double t) => begin + (end - begin) * t;
/// Updates the value of this variable according to the given animation clock value and direction
/// Updates the value of this variable according to the given animation clock
/// value and direction.
void setProgress(double t, AnimationDirection direction) {
if (end != null) {
t = transform(t, direction);
......@@ -93,7 +94,7 @@ class AnimatedValue<T extends dynamic> extends AnimationTiming implements Animat
String toString() => 'AnimatedValue(begin=$begin, end=$end, value=$value)';
}
/// An animated variable containing a color
/// An animated variable containing a color.
///
/// This class specializes the interpolation of AnimatedValue<Color> to be
/// appropriate for colors.
......@@ -104,9 +105,9 @@ class AnimatedColorValue extends AnimatedValue<Color> {
Color lerp(double t) => Color.lerp(begin, end, t);
}
/// An animated variable containing a rectangle
/// An animated variable containing a size.
///
/// This class specializes the interpolation of AnimatedValue<Rect> to be
/// This class specializes the interpolation of AnimatedValue<Size> to be
/// appropriate for rectangles.
class AnimatedSizeValue extends AnimatedValue<Size> {
AnimatedSizeValue(Size begin, { Size end, Curve curve, Curve reverseCurve })
......@@ -115,7 +116,7 @@ class AnimatedSizeValue extends AnimatedValue<Size> {
Size lerp(double t) => Size.lerp(begin, end, t);
}
/// An animated variable containing a rectangle
/// An animated variable containing a rectangle.
///
/// This class specializes the interpolation of AnimatedValue<Rect> to be
/// appropriate for rectangles.
......@@ -125,3 +126,15 @@ class AnimatedRectValue extends AnimatedValue<Rect> {
Rect lerp(double t) => Rect.lerp(begin, end, t);
}
/// An animated variable containing a int.
///
/// The inherited lerp() function doesn't work with ints because it multiplies
/// the begin and end types by a double, and int * double returns a double.
/// This class overrides the lerp() function to round off the result to an int.
class AnimatedIntValue extends AnimatedValue<int> {
AnimatedIntValue(int begin, { int end, Curve curve, Curve reverseCurve })
: super(begin, end: end, curve: curve, reverseCurve: reverseCurve);
int lerp(double t) => (begin + (end - begin) * t).round();
}
......@@ -14,11 +14,13 @@ class Colors {
static const black = const Color(0xFF000000);
static const black87 = const Color(0xDD000000);
static const black54 = const Color(0x8A000000);
static const black26 = const Color(0x42000000); // disabled radio buttons and text of disabled flat buttons in light theme (26% black)
static const black45 = const Color(0x73000000); // mask color
static const black26 = const Color(0x42000000); // disabled radio buttons and text of disabled flat buttons in light theme
static const black12 = const Color(0x1F000000); // background of disabled raised buttons in light theme
static const white = const Color(0xFFFFFFFF);
static const white70 = const Color(0xB3FFFFFF);
static const white30 = const Color(0x4DFFFFFF); // disabled radio buttons and text of disabled flat buttons in dark theme (30% white)
static const white30 = const Color(0x4DFFFFFF); // disabled radio buttons and text of disabled flat buttons in dark theme
static const white12 = const Color(0x1FFFFFFF); // background of disabled raised buttons in dark theme
static const white10 = const Color(0x1AFFFFFF);
......
......@@ -10,35 +10,39 @@ import 'ink_well.dart';
import 'theme.dart';
class DrawerItem extends StatelessComponent {
const DrawerItem({ Key key, this.icon, this.child, this.onPressed, this.selected: false })
: super(key: key);
const DrawerItem({
Key key,
this.icon,
this.child,
this.onPressed,
this.selected: false
}) : super(key: key);
final String icon;
final Widget child;
final VoidCallback onPressed;
final bool selected;
ColorFilter _getIconColorFilter(ThemeData themeData) {
if (selected) {
if (themeData.brightness == ThemeBrightness.dark)
return new ColorFilter.mode(themeData.accentColor, TransferMode.srcATop);
return new ColorFilter.mode(themeData.primaryColor, TransferMode.srcATop);
}
return new ColorFilter.mode(Colors.black45, TransferMode.dstIn);
}
TextStyle _getTextStyle(ThemeData themeData) {
TextStyle result = themeData.text.body2;
if (selected)
result = result.copyWith(color: themeData.primaryColor);
if (selected) {
if (themeData.brightness == ThemeBrightness.dark)
result = result.copyWith(color: themeData.accentColor);
else
result = result.copyWith(color: themeData.primaryColor);
}
return result;
}
Color _getBackgroundColor(ThemeData themeData, { bool highlight }) {
if (highlight)
return themeData.highlightColor;
if (selected)
return themeData.selectedColor;
return Colors.transparent;
}
ColorFilter _getColorFilter(ThemeData themeData) {
if (selected)
return new ColorFilter.mode(themeData.primaryColor, TransferMode.srcATop);
return new ColorFilter.mode(const Color(0x73000000), TransferMode.dstIn);
}
Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context);
......@@ -49,7 +53,7 @@ class DrawerItem extends StatelessComponent {
padding: const EdgeDims.symmetric(horizontal: 16.0),
child: new Icon(
icon: icon,
colorFilter: _getColorFilter(themeData)
colorFilter: _getIconColorFilter(themeData)
)
)
);
......@@ -70,8 +74,6 @@ class DrawerItem extends StatelessComponent {
height: 48.0,
child: new InkWell(
onTap: onPressed,
defaultColor: _getBackgroundColor(themeData, highlight: false),
highlightColor: _getBackgroundColor(themeData, highlight: true),
child: new Row(flexChildren)
)
);
......
......@@ -4,7 +4,6 @@
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'material_button.dart';
import 'theme.dart';
......@@ -12,11 +11,27 @@ class FlatButton extends MaterialButton {
FlatButton({
Key key,
Widget child,
ButtonColor textTheme,
Color textColor,
Color disabledTextColor,
this.color,
this.colorBrightness,
this.disabledColor,
VoidCallback onPressed
}) : super(key: key,
child: child,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
onPressed: onPressed);
// These default to null, meaning transparent.
final Color color;
final Color disabledColor;
/// Controls the default text color if the text color isn't explicit set.
final ThemeBrightness colorBrightness;
_FlatButtonState createState() => new _FlatButtonState();
}
......@@ -24,19 +39,14 @@ class _FlatButtonState extends MaterialButtonState<FlatButton> {
int get elevation => 0;
Color getColor(BuildContext context, { bool highlight }) {
if (!config.enabled || !highlight)
return null;
switch (Theme.of(context).brightness) {
case ThemeBrightness.light:
return Colors.grey[400];
case ThemeBrightness.dark:
return Colors.grey[200];
}
Color getColor(BuildContext context) {
if (!config.enabled)
return config.disabledColor;
return config.color;
}
ThemeBrightness getColorBrightness(BuildContext context) {
return Theme.of(context).brightness;
return config.colorBrightness ?? Theme.of(context).brightness;
}
}
......@@ -19,12 +19,16 @@ class FloatingActionButton extends StatefulComponent {
Key key,
this.child,
this.backgroundColor,
this.elevation: 6,
this.highlightElevation: 12,
this.onPressed
}) : super(key: key);
final Widget child;
final Color backgroundColor;
final VoidCallback onPressed;
final int elevation;
final int highlightElevation;
_FloatingActionButtonState createState() => new _FloatingActionButtonState();
}
......@@ -50,19 +54,17 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
return new Material(
color: materialColor,
type: MaterialType.circle,
elevation: _highlight ? 12 : 6,
child: new ClipOval(
child: new Container(
width: _kSize,
height: _kSize,
child: new InkWell(
onTap: config.onPressed,
onHighlightChanged: _handleHighlightChanged,
child: new Center(
child: new IconTheme(
data: new IconThemeData(color: iconThemeColor),
child: config.child
)
elevation: _highlight ? config.highlightElevation : config.elevation,
child: new Container(
width: _kSize,
height: _kSize,
child: new InkWell(
onTap: config.onPressed,
onHighlightChanged: _handleHighlightChanged,
child: new Center(
child: new IconTheme(
data: new IconThemeData(color: iconThemeColor),
child: config.child
)
)
)
......
......@@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
import 'icon.dart';
import 'icon_theme_data.dart';
import 'ink_well.dart';
class IconButton extends StatelessComponent {
const IconButton({
......@@ -22,11 +23,8 @@ class IconButton extends StatelessComponent {
final VoidCallback onPressed;
Widget build(BuildContext context) {
// TODO(abarth): We should use a radial reaction here so you can hit the
// 8.0 pixel padding as well as the icon.
return new GestureDetector(
return new InkResponse(
onTap: onPressed,
behavior: HitTestBehavior.opaque,
child: new Padding(
padding: const EdgeDims.all(8.0),
child: new Icon(
......
......@@ -36,12 +36,16 @@ abstract class MaterialButton extends StatefulComponent {
MaterialButton({
Key key,
this.child,
this.textTheme,
this.textColor,
this.disabledTextColor,
this.onPressed
}) : super(key: key);
final Widget child;
final ButtonColor textColor;
final ButtonColor textTheme;
final Color textColor;
final Color disabledTextColor;
final VoidCallback onPressed;
bool get enabled => onPressed != null;
......@@ -57,12 +61,14 @@ abstract class MaterialButtonState<T extends MaterialButton> extends State<T> {
bool highlight = false;
int get elevation;
Color getColor(BuildContext context, { bool highlight });
Color getColor(BuildContext context);
ThemeBrightness getColorBrightness(BuildContext context);
Color getTextColor(BuildContext context) {
if (config.enabled) {
switch (config.textColor ?? ButtonTheme.of(context)) {
if (config.textColor != null)
return config.textColor;
switch (config.textTheme ?? ButtonTheme.of(context)) {
case ButtonColor.accent:
return Theme.of(context).accentColor;
case ButtonColor.normal:
......@@ -74,6 +80,8 @@ abstract class MaterialButtonState<T extends MaterialButton> extends State<T> {
}
}
}
if (config.disabledTextColor != null)
return config.disabledTextColor;
switch (getColorBrightness(context)) {
case ThemeBrightness.light:
return Colors.black26;
......@@ -84,34 +92,45 @@ abstract class MaterialButtonState<T extends MaterialButton> extends State<T> {
void _handleHighlightChanged(bool value) {
setState(() {
// mostly just used by the RaisedButton subclass to change the elevation
highlight = value;
});
}
Widget build(BuildContext context) {
Widget contents = new Container(
padding: new EdgeDims.symmetric(horizontal: 8.0),
child: new Center(
widthFactor: 1.0,
child: config.child
Widget contents = new InkWell(
onTap: config.onPressed,
onHighlightChanged: _handleHighlightChanged,
child: new Container(
padding: new EdgeDims.symmetric(horizontal: 8.0),
child: new Center(
widthFactor: 1.0,
child: config.child
)
)
);
TextStyle style = Theme.of(context).text.button.copyWith(color: getTextColor(context));
int elevation = this.elevation;
Color color = getColor(context);
if (elevation > 0 || color != null) {
contents = new Material(
type: MaterialType.button,
color: getColor(context),
elevation: elevation,
textStyle: style,
child: contents
);
} else {
contents = new DefaultTextStyle(
style: style,
child: contents
);
}
return new Container(
height: 36.0,
constraints: new BoxConstraints(minWidth: 88.0),
margin: new EdgeDims.all(8.0),
child: new Material(
type: MaterialType.button,
elevation: elevation,
textStyle: Theme.of(context).text.button.copyWith(color: getTextColor(context)),
child: new InkWell(
onTap: config.enabled ? config.onPressed : null,
defaultColor: getColor(context, highlight: false),
highlightColor: getColor(context, highlight: true),
onHighlightChanged: _handleHighlightChanged,
child: contents
)
)
child: contents
);
}
}
......@@ -12,36 +12,56 @@ class RaisedButton extends MaterialButton {
RaisedButton({
Key key,
Widget child,
this.color,
this.colorBrightness,
this.disabledColor,
this.elevation: 2,
this.highlightElevation: 8,
this.disabledElevation: 0,
VoidCallback onPressed
}) : super(key: key,
child: child,
onPressed: onPressed);
final Color color;
final Color disabledColor;
/// Controls the default text color if the text color isn't explicit set.
final ThemeBrightness colorBrightness;
final int elevation;
final int highlightElevation;
final int disabledElevation;
_RaisedButtonState createState() => new _RaisedButtonState();
}
class _RaisedButtonState extends MaterialButtonState<RaisedButton> {
int get elevation => config.enabled ? (highlight ? 8 : 2) : 0;
int get elevation {
if (config.enabled) {
if (highlight)
return config.highlightElevation;
return config.elevation;
} else {
return config.disabledElevation;
}
}
Color getColor(BuildContext context, { bool highlight }) {
Color getColor(BuildContext context) {
if (config.enabled) {
if (config.color != null)
return config.color;
switch (Theme.of(context).brightness) {
case ThemeBrightness.light:
if (highlight)
return Colors.grey[350];
else
return Colors.grey[300];
break;
return Colors.grey[300];
case ThemeBrightness.dark:
Map<int, Color> swatch = Theme.of(context).primarySwatch ?? Colors.blue;
if (highlight)
return swatch[700];
else
return swatch[600];
break;
return swatch[600];
}
} else {
if (config.disabledColor != null)
return config.disabledColor;
switch (Theme.of(context).brightness) {
case ThemeBrightness.light:
return Colors.black12;
......@@ -52,7 +72,7 @@ class _RaisedButtonState extends MaterialButtonState<RaisedButton> {
}
ThemeBrightness getColorBrightness(BuildContext context) {
return Theme.of(context).brightness;
return config.colorBrightness ?? Theme.of(context).brightness;
}
}
......@@ -115,7 +115,10 @@ class ScaffoldState extends State<Scaffold> {
}
ScaffoldFeatureController<SnackBar> controller;
controller = new ScaffoldFeatureController<SnackBar>._(
snackbar.withPerformance(_snackBarPerformance),
// We provide a fallback key so that if back-to-back snackbars happen to
// match in structure, material ink splashes and highlights don't survive
// from one to the next.
snackbar.withPerformance(_snackBarPerformance, fallbackKey: new UniqueKey()),
new Completer(),
() {
assert(_snackBars.first == controller);
......
......@@ -5,14 +5,18 @@
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import 'flat_button.dart';
import 'material.dart';
import 'material_button.dart';
import 'theme.dart';
import 'theme_data.dart';
import 'typography.dart';
// https://www.google.com/design/spec/components/snackbars-toasts.html#snackbars-toasts-specs
const double _kSideMargins = 24.0;
const double _kSingleLineVerticalPadding = 14.0;
const double _kMultiLineVerticalPadding = 24.0;
const double _kMultiLineVerticalTopPadding = 24.0;
const double _kMultiLineVerticalSpaceBetweenTextAndButtons = 10.0;
const Color _kSnackBackground = const Color(0xFF323232);
// TODO(ianh): We should check if the given text and actions are going to fit on
......@@ -35,11 +39,11 @@ class SnackBarAction extends StatelessComponent {
final VoidCallback onPressed;
Widget build(BuildContext context) {
return new GestureDetector(
onTap: onPressed,
child: new Container(
margin: const EdgeDims.only(left: _kSideMargins),
padding: const EdgeDims.symmetric(vertical: _kSingleLineVerticalPadding),
return new Container(
margin: const EdgeDims.only(left: _kSideMargins),
child: new FlatButton(
onPressed: onPressed,
textTheme: ButtonColor.accent,
child: new Text(label)
)
);
......@@ -77,6 +81,7 @@ class SnackBar extends StatelessComponent {
];
if (actions != null)
children.addAll(actions);
ThemeData theme = Theme.of(context);
return new ClipRect(
child: new AlignTransition(
performance: performance,
......@@ -87,12 +92,20 @@ class SnackBar extends StatelessComponent {
color: _kSnackBackground,
child: new Container(
margin: const EdgeDims.symmetric(horizontal: _kSideMargins),
child: new DefaultTextStyle(
style: new TextStyle(color: Theme.of(context).accentColor),
child: new Theme(
data: new ThemeData(
brightness: ThemeBrightness.dark,
accentColor: theme.accentColor,
accentColorBrightness: theme.accentColorBrightness,
text: Typography.white
),
child: new FadeTransition(
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: _snackBarFadeCurve),
child: new Row(children)
child: new Row(
children,
alignItems: FlexAlignItems.center
)
)
)
)
......@@ -110,9 +123,9 @@ class SnackBar extends StatelessComponent {
);
}
SnackBar withPerformance(Performance newPerformance) {
SnackBar withPerformance(Performance newPerformance, { Key fallbackKey }) {
return new SnackBar(
key: key,
key: key ?? fallbackKey,
content: content,
actions: actions,
duration: duration,
......
......@@ -10,11 +10,11 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'icon.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'ink_well.dart';
import 'material.dart';
import 'theme.dart';
typedef void TabSelectedIndexChanged(int selectedIndex);
......@@ -403,6 +403,10 @@ class TabBarSelection {
int _previousIndex = 0;
}
/// A tab strip, consisting of several TabLabels and a TabBarSelection.
/// The TabBarSelection can be used to link this to a TabBarView.
///
/// Tabs must always have an ancestor Material object.
class TabBar extends Scrollable {
TabBar({
Key key,
......@@ -551,13 +555,13 @@ class _TabBarState extends ScrollableState<TabBar> {
Widget buildContent(BuildContext context) {
assert(config.labels != null && config.labels.isNotEmpty);
assert(Material.of(context) != null);
ThemeData themeData = Theme.of(context);
Color backgroundColor = themeData.primaryColor;
Color backgroundColor = Material.of(context).color;
Color indicatorColor = themeData.accentColor;
if (indicatorColor == backgroundColor) {
if (indicatorColor == backgroundColor)
indicatorColor = Colors.white;
}
TextStyle textStyle = themeData.primaryTextTheme.body1;
IconThemeData iconTheme = themeData.primaryIconTheme;
......@@ -571,7 +575,7 @@ class _TabBarState extends ScrollableState<TabBar> {
textAndIcons = true;
}
Widget content = new IconTheme(
Widget contents = new IconTheme(
data: iconTheme,
child: new DefaultTextStyle(
style: textStyle,
......@@ -594,23 +598,17 @@ class _TabBarState extends ScrollableState<TabBar> {
);
if (config.isScrollable) {
content = new SizeObserver(
contents = new SizeObserver(
onSizeChanged: _handleViewportSizeChanged,
child: new Viewport(
scrollDirection: ScrollDirection.horizontal,
scrollOffset: new Offset(scrollOffset, 0.0),
child: content
child: contents
)
);
}
return new AnimatedContainer(
decoration: new BoxDecoration(
backgroundColor: backgroundColor
),
duration: kThemeChangeDuration,
child: content
);
return contents;
}
}
......
......@@ -27,15 +27,7 @@ class ThemeData {
// Some users want the pre-multiplied color, others just want the opacity.
hintColor = brightness == ThemeBrightness.dark ? const Color(0x42FFFFFF) : const Color(0x4C000000),
hintOpacity = brightness == ThemeBrightness.dark ? 0.26 : 0.30,
// TODO(eseidel): Where are highlight and selected colors documented?
// I flipped highlight/selected to match the News app (which is clearly not quite Material)
// Gmail has an interesting behavior of showing selected darker until
// you click on a different drawer item when the first one loses its
// selected color and becomes lighter, the ink then fills to make the new
// click dark to match the previous (resting) selected state. States
// revert when you cancel the tap.
highlightColor = const Color(0x33999999),
selectedColor = const Color(0x66999999),
highlightColor = brightness == ThemeBrightness.dark ? const Color(0x42FFFFFF) : const Color(0x1F000000),
text = brightness == ThemeBrightness.dark ? Typography.white : Typography.black {
assert(brightness != null);
......@@ -63,6 +55,13 @@ class ThemeData {
/// The brightness of the overall theme of the application. Used by widgets
/// like buttons to determine what color to pick when not using the primary or
/// accent color.
///
/// When the ThemeBrightness is dark, the canvas, card, and primary colors are
/// all dark. When the ThemeBrightness is light, the canvas and card colors
/// are bright, and the primary color's darkness varies as described by
/// primaryColorBrightness. The primaryColor does not contrast well with the
/// card and canvas colors when the brightness is dask; when the birghtness is
/// dark, use Colors.white or the accentColor for a contrasting color.
final ThemeBrightness brightness;
final Map<int, Color> primarySwatch;
......@@ -71,8 +70,9 @@ class ThemeData {
final Color dividerColor;
final Color hintColor;
final Color highlightColor;
final Color selectedColor;
final double hintOpacity;
/// Text with a color that contrasts with the card and canvas colors.
final TextTheme text;
/// The background colour for major parts of the app (toolbars, tab bars, etc)
......@@ -128,7 +128,6 @@ class ThemeData {
(otherData.dividerColor == dividerColor) &&
(otherData.hintColor == hintColor) &&
(otherData.highlightColor == highlightColor) &&
(otherData.selectedColor == selectedColor) &&
(otherData.hintOpacity == hintOpacity) &&
(otherData.text == text) &&
(otherData.primaryColorBrightness == primaryColorBrightness) &&
......@@ -143,7 +142,6 @@ class ThemeData {
value = 37 * value + dividerColor.hashCode;
value = 37 * value + hintColor.hashCode;
value = 37 * value + highlightColor.hashCode;
value = 37 * value + selectedColor.hashCode;
value = 37 * value + hintOpacity.hashCode;
value = 37 * value + text.hashCode;
value = 37 * value + primaryColorBrightness.hashCode;
......
......@@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'shadows.dart';
import 'material.dart';
import 'tabs.dart';
import 'theme.dart';
import 'typography.dart';
......@@ -36,9 +36,9 @@ class ToolBar extends StatelessComponent {
final TextTheme textTheme;
final EdgeDims padding;
ToolBar withPadding(EdgeDims newPadding) {
ToolBar withPadding(EdgeDims newPadding, { Key fallbackKey }) {
return new ToolBar(
key: key,
key: key ?? fallbackKey,
left: left,
center: center,
right: right,
......@@ -67,52 +67,63 @@ class ToolBar extends StatelessComponent {
sideStyle ??= primaryTextTheme.body2;
}
List<Widget> children = new List<Widget>();
final List<Widget> firstRow = <Widget>[];
if (left != null)
children.add(left);
children.add(
firstRow.add(left);
firstRow.add(
new Flexible(
child: new Padding(
child: center != null ? new DefaultTextStyle(child: center, style: centerStyle) : null,
padding: new EdgeDims.only(left: 24.0)
padding: new EdgeDims.only(left: 24.0),
child: center != null ? new DefaultTextStyle(style: centerStyle, child: center) : null
)
)
);
if (right != null)
children.addAll(right);
final List<Widget> columnChildren = <Widget>[
new Container(height: kToolBarHeight, child: new Row(children))
firstRow.addAll(right);
final List<Widget> rows = <Widget>[
new Container(
height: kToolBarHeight,
child: new DefaultTextStyle(
style: sideStyle,
child: new Row(firstRow)
)
)
];
if (bottom != null)
columnChildren.add(new DefaultTextStyle(
style: centerStyle,
child: new Container(height: kExtendedToolBarHeight - kToolBarHeight, child: bottom)
));
if (bottom != null) {
rows.add(
new DefaultTextStyle(
style: centerStyle,
child: new Container(
height: kExtendedToolBarHeight - kToolBarHeight,
child: bottom
)
)
);
}
if (tabBar != null)
columnChildren.add(tabBar);
rows.add(tabBar);
EdgeDims combinedPadding = new EdgeDims.symmetric(horizontal: 8.0);
if (padding != null)
combinedPadding += padding;
Widget content = new AnimatedContainer(
duration: kThemeChangeDuration,
padding: new EdgeDims.symmetric(horizontal: 8.0),
decoration: new BoxDecoration(
backgroundColor: color,
boxShadow: elevationToShadow[elevation]
),
child: new DefaultTextStyle(
style: sideStyle,
child: new Container(padding: padding, child: new Column(columnChildren, justifyContent: FlexJustifyContent.collapse))
Widget contents = new Material(
color: color,
elevation: elevation,
child: new Container(
padding: combinedPadding,
child: new Column(
rows,
justifyContent: FlexJustifyContent.collapse
)
)
);
if (iconThemeData != null)
content = new IconTheme(data: iconThemeData, child: content);
return content;
contents = new IconTheme(data: iconThemeData, child: contents);
return contents;
}
}
......@@ -386,8 +386,17 @@ abstract class State<T extends StatefulComponent> {
_element.markNeedsBuild();
}
/// Called when this object is removed from the tree. Override this to clean
/// up any resources allocated by this object.
/// Called when this object is removed from the tree.
/// The object might momentarily be reattached to the tree elsewhere.
///
/// Use this to clean up any links between this state and other
/// elements in the tree (e.g. if you have provided an ancestor with
/// a pointer to a descendant's renderObject).
void deactivate() { }
/// Called when this object is removed from the tree permanently.
/// Override this to clean up any resources allocated by this
/// object.
///
/// If you override this, make sure to end your method with a call to
/// super.dispose().
......@@ -405,6 +414,11 @@ abstract class State<T extends StatefulComponent> {
/// provides the set of inherited widgets for this location in the tree.
Widget build(BuildContext context);
/// Called when an Inherited widget in the ancestor chain has changed. Usually
/// there is nothing to do here; whenever this is called, build() is also
/// called.
void dependenciesChanged(Type affectedWidgetType) { }
String toString() {
final List<String> data = <String>[];
debugFillDescription(data);
......@@ -540,6 +554,7 @@ abstract class BuildContext {
InheritedWidget inheritFromWidgetOfType(Type targetType);
Widget ancestorWidgetOfType(Type targetType);
State ancestorStateOfType(Type targetType);
RenderObject ancestorRenderObjectOfType(Type targetType);
void visitAncestorElements(bool visitor(Element element));
void visitChildElements(void visitor(Element element));
}
......@@ -777,7 +792,7 @@ abstract class Element<T extends Widget> implements BuildContext {
assert(child._parent == this);
child._parent = null;
child.detachRenderObject();
_inactiveElements.add(child);
_inactiveElements.add(child); // this eventually calls child.deactivate()
}
void deactivate() {
......@@ -839,13 +854,24 @@ abstract class Element<T extends Widget> implements BuildContext {
return statefulAncestor?.state;
}
RenderObject ancestorRenderObjectOfType(Type targetType) {
Element ancestor = _parent;
while (ancestor != null) {
if (ancestor is RenderObjectElement && ancestor.renderObject.runtimeType == targetType)
break;
ancestor = ancestor._parent;
}
RenderObjectElement renderObjectAncestor = ancestor;
return renderObjectAncestor?.renderObject;
}
void visitAncestorElements(bool visitor(Element element)) {
Element ancestor = _parent;
while (ancestor != null && visitor(ancestor))
ancestor = ancestor._parent;
}
void dependenciesChanged() {
void dependenciesChanged(Type affectedWidgetType) {
assert(false);
}
......@@ -1024,7 +1050,7 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
/// Called by rebuild() after the appropriate checks have been made.
void performRebuild();
void dependenciesChanged() {
void dependenciesChanged(Type affectedWidgetType) {
markNeedsBuild();
}
......@@ -1169,6 +1195,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>>
rebuild();
}
void deactivate() {
_state.deactivate();
super.deactivate();
}
void unmount() {
super.unmount();
_state.dispose();
......@@ -1183,6 +1214,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>>
_state = null;
}
void dependenciesChanged(Type affectedWidgetType) {
super.dependenciesChanged(affectedWidgetType);
_state.dependenciesChanged(affectedWidgetType);
}
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (state != null)
......@@ -1252,7 +1288,7 @@ class InheritedElement extends ProxyElement<InheritedWidget> {
void notifyChildren(Element child) {
if (child._dependencies != null &&
child._dependencies.contains(ourRuntimeType)) {
child.dependenciesChanged();
child.dependenciesChanged(ourRuntimeType);
}
if (child.runtimeType != ourRuntimeType)
child.visitChildren(notifyChildren);
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'framework.dart';
/// Return true to cancel the notification bubbling.
typedef bool NotificationListenerCallback<T extends Notification>(T notification);
abstract class Notification {
void dispatch(BuildContext target) {
target.visitAncestorElements((Element element) {
if (element is StatelessComponentElement &&
element.widget is NotificationListener) {
final NotificationListener widget = element.widget;
if (widget._dispatch(this))
return false;
}
return true;
});
}
}
class NotificationListener<T extends Notification> extends StatelessComponent {
NotificationListener({
Key key,
this.child,
this.onNotification
}) : super(key: key);
final Widget child;
final NotificationListenerCallback<T> onNotification;
bool _dispatch(Notification notification) {
if (onNotification != null && notification is T)
return onNotification(notification);
return false;
}
Widget build(BuildContext context) => child;
}
/// Indicates that the layout of one of the descendants of the object receiving
/// this notification has changed in some way, and that therefore any
/// assumptions about that layout are no longer valid.
///
/// Useful if, for instance, you're trying to align multiple descendants.
///
/// Be aware that in the widgets library, only the [Scrollable] classes dispatch
/// this notification. (Transitions, in particular, do not.) Changing one's
/// layout in one's build function does not cause this notification to be
/// dispatched automatically. If an ancestor expects to be notified for any
/// layout change, make sure you only use widgets that either never change
/// layout, or that do notify their ancestors when appropriate.
class LayoutChangedNotification extends Notification { }
......@@ -16,6 +16,7 @@ import 'framework.dart';
import 'gesture_detector.dart';
import 'homogeneous_viewport.dart';
import 'mixed_viewport.dart';
import 'notification_listener.dart';
import 'page_storage.dart';
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
......@@ -50,6 +51,8 @@ abstract class Scrollable extends StatefulComponent {
final ScrollListener onScrollEnd;
final SnapOffsetCallback snapOffsetCallback;
final double snapAlignmentOffset;
ScrollableState createState();
}
abstract class ScrollableState<T extends Scrollable> extends State<T> {
......@@ -180,6 +183,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
_scrollOffset = newScrollOffset;
});
PageStorage.of(context)?.writeState(context, _scrollOffset);
new ScrollNotification(this, _scrollOffset).dispatch(context);
dispatchOnScroll();
}
......@@ -271,6 +275,12 @@ ScrollableState findScrollableAncestor(BuildContext context) {
return result;
}
class ScrollNotification extends Notification {
ScrollNotification(this.scrollable, this.position);
final ScrollableState scrollable;
final double position;
}
Future ensureWidgetIsVisible(BuildContext context, { Duration duration, Curve curve }) {
assert(context.findRenderObject() is RenderBox);
// TODO(abarth): This function doesn't handle nested scrollable widgets.
......
......@@ -25,6 +25,7 @@ export 'src/widgets/mimic.dart';
export 'src/widgets/mixed_viewport.dart';
export 'src/widgets/modal_barrier.dart';
export 'src/widgets/navigator.dart';
export 'src/widgets/notification_listener.dart';
export 'src/widgets/overlay.dart';
export 'src/widgets/page_storage.dart';
export 'src/widgets/placeholder.dart';
......
......@@ -2,6 +2,10 @@ import 'package:flutter/animation.dart';
import 'package:test/test.dart';
void main() {
test("Check for a time dilation being in effect", () {
expect(timeDilation, equals(1.0));
});
test("Can cancel queued callback", () {
int secondId;
......
......@@ -11,16 +11,18 @@ void main() {
testWidgets((WidgetTester tester) {
DateTime currentValue;
Widget widget = new Block(<Widget>[
new DatePicker(
selectedDate: new DateTime.utc(2015, 6, 9, 7, 12),
firstDate: new DateTime.utc(2013),
lastDate: new DateTime.utc(2018),
onChanged: (DateTime dateTime) {
currentValue = dateTime;
}
)
]);
Widget widget = new Material(
child: new Block(<Widget>[
new DatePicker(
selectedDate: new DateTime.utc(2015, 6, 9, 7, 12),
firstDate: new DateTime.utc(2013),
lastDate: new DateTime.utc(2018),
onChanged: (DateTime dateTime) {
currentValue = dateTime;
}
)
])
);
tester.pumpWidget(widget);
......
......@@ -11,18 +11,22 @@ import 'test_matchers.dart';
Key firstKey = new Key('first');
Key secondKey = new Key('second');
final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
'/': (RouteArguments args) => new Block([
new Container(height: 100.0, width: 100.0),
new Card(child: new Hero(tag: 'a', child: new Container(height: 100.0, width: 100.0, key: firstKey))),
new Container(height: 100.0, width: 100.0),
new FlatButton(child: new Text('button'), onPressed: () => Navigator.pushNamed(args.context, '/two')),
]),
'/two': (RouteArguments args) => new Block([
new Container(height: 150.0, width: 150.0),
new Card(child: new Hero(tag: 'a', child: new Container(height: 150.0, width: 150.0, key: secondKey))),
new Container(height: 150.0, width: 150.0),
new FlatButton(child: new Text('button'), onPressed: () => Navigator.pop(args.context)),
]),
'/': (RouteArguments args) => new Material(
child: new Block([
new Container(height: 100.0, width: 100.0),
new Card(child: new Hero(tag: 'a', child: new Container(height: 100.0, width: 100.0, key: firstKey))),
new Container(height: 100.0, width: 100.0),
new FlatButton(child: new Text('button'), onPressed: () => Navigator.pushNamed(args.context, '/two')),
])
),
'/two': (RouteArguments args) => new Material(
child: new Block([
new Container(height: 150.0, width: 150.0),
new Card(child: new Hero(tag: 'a', child: new Container(height: 150.0, width: 150.0, key: secondKey))),
new Container(height: 150.0, width: 150.0),
new FlatButton(child: new Text('button'), onPressed: () => Navigator.pop(args.context)),
])
),
};
void main() {
......
......@@ -10,10 +10,12 @@ import 'package:test/test.dart';
TabBarSelection selection;
Widget buildFrame({ List<String> tabs, bool isScrollable: false }) {
return new TabBar(
labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(),
selection: selection,
isScrollable: isScrollable
return new Material(
child: new TabBar(
labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(),
selection: selection,
isScrollable: isScrollable
)
);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment