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 { ...@@ -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 { class AnimatedValue<T extends dynamic> extends AnimationTiming implements Animatable {
AnimatedValue(this.begin, { this.end, Curve curve, Curve reverseCurve }) AnimatedValue(this.begin, { this.end, Curve curve, Curve reverseCurve })
: super(curve: curve, reverseCurve: reverseCurve) { : super(curve: curve, reverseCurve: reverseCurve) {
value = begin; value = begin;
} }
/// The current value of this variable /// The current value of this variable.
T value; 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; 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; 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; 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) { void setProgress(double t, AnimationDirection direction) {
if (end != null) { if (end != null) {
t = transform(t, direction); t = transform(t, direction);
...@@ -93,7 +94,7 @@ class AnimatedValue<T extends dynamic> extends AnimationTiming implements Animat ...@@ -93,7 +94,7 @@ class AnimatedValue<T extends dynamic> extends AnimationTiming implements Animat
String toString() => 'AnimatedValue(begin=$begin, end=$end, value=$value)'; 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 /// This class specializes the interpolation of AnimatedValue<Color> to be
/// appropriate for colors. /// appropriate for colors.
...@@ -104,9 +105,9 @@ class AnimatedColorValue extends AnimatedValue<Color> { ...@@ -104,9 +105,9 @@ class AnimatedColorValue extends AnimatedValue<Color> {
Color lerp(double t) => Color.lerp(begin, end, t); 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. /// appropriate for rectangles.
class AnimatedSizeValue extends AnimatedValue<Size> { class AnimatedSizeValue extends AnimatedValue<Size> {
AnimatedSizeValue(Size begin, { Size end, Curve curve, Curve reverseCurve }) AnimatedSizeValue(Size begin, { Size end, Curve curve, Curve reverseCurve })
...@@ -115,7 +116,7 @@ class AnimatedSizeValue extends AnimatedValue<Size> { ...@@ -115,7 +116,7 @@ class AnimatedSizeValue extends AnimatedValue<Size> {
Size lerp(double t) => Size.lerp(begin, end, t); 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 /// This class specializes the interpolation of AnimatedValue<Rect> to be
/// appropriate for rectangles. /// appropriate for rectangles.
...@@ -125,3 +126,15 @@ class AnimatedRectValue extends AnimatedValue<Rect> { ...@@ -125,3 +126,15 @@ class AnimatedRectValue extends AnimatedValue<Rect> {
Rect lerp(double t) => Rect.lerp(begin, end, t); 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 { ...@@ -14,11 +14,13 @@ class Colors {
static const black = const Color(0xFF000000); static const black = const Color(0xFF000000);
static const black87 = const Color(0xDD000000); static const black87 = const Color(0xDD000000);
static const black54 = const Color(0x8A000000); 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 black12 = const Color(0x1F000000); // background of disabled raised buttons in light theme
static const white = const Color(0xFFFFFFFF); static const white = const Color(0xFFFFFFFF);
static const white70 = const Color(0xB3FFFFFF); 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 white12 = const Color(0x1FFFFFFF); // background of disabled raised buttons in dark theme
static const white10 = const Color(0x1AFFFFFF); static const white10 = const Color(0x1AFFFFFF);
......
...@@ -10,33 +10,37 @@ import 'ink_well.dart'; ...@@ -10,33 +10,37 @@ import 'ink_well.dart';
import 'theme.dart'; import 'theme.dart';
class DrawerItem extends StatelessComponent { class DrawerItem extends StatelessComponent {
const DrawerItem({ Key key, this.icon, this.child, this.onPressed, this.selected: false }) const DrawerItem({
: super(key: key); Key key,
this.icon,
this.child,
this.onPressed,
this.selected: false
}) : super(key: key);
final String icon; final String icon;
final Widget child; final Widget child;
final VoidCallback onPressed; final VoidCallback onPressed;
final bool selected; 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 _getTextStyle(ThemeData themeData) {
TextStyle result = themeData.text.body2; TextStyle result = themeData.text.body2;
if (selected) if (selected) {
if (themeData.brightness == ThemeBrightness.dark)
result = result.copyWith(color: themeData.accentColor);
else
result = result.copyWith(color: themeData.primaryColor); result = result.copyWith(color: themeData.primaryColor);
return result;
} }
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) { Widget build(BuildContext context) {
...@@ -49,7 +53,7 @@ class DrawerItem extends StatelessComponent { ...@@ -49,7 +53,7 @@ class DrawerItem extends StatelessComponent {
padding: const EdgeDims.symmetric(horizontal: 16.0), padding: const EdgeDims.symmetric(horizontal: 16.0),
child: new Icon( child: new Icon(
icon: icon, icon: icon,
colorFilter: _getColorFilter(themeData) colorFilter: _getIconColorFilter(themeData)
) )
) )
); );
...@@ -70,8 +74,6 @@ class DrawerItem extends StatelessComponent { ...@@ -70,8 +74,6 @@ class DrawerItem extends StatelessComponent {
height: 48.0, height: 48.0,
child: new InkWell( child: new InkWell(
onTap: onPressed, onTap: onPressed,
defaultColor: _getBackgroundColor(themeData, highlight: false),
highlightColor: _getBackgroundColor(themeData, highlight: true),
child: new Row(flexChildren) child: new Row(flexChildren)
) )
); );
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'material_button.dart'; import 'material_button.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -12,11 +11,27 @@ class FlatButton extends MaterialButton { ...@@ -12,11 +11,27 @@ class FlatButton extends MaterialButton {
FlatButton({ FlatButton({
Key key, Key key,
Widget child, Widget child,
ButtonColor textTheme,
Color textColor,
Color disabledTextColor,
this.color,
this.colorBrightness,
this.disabledColor,
VoidCallback onPressed VoidCallback onPressed
}) : super(key: key, }) : super(key: key,
child: child, child: child,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
onPressed: onPressed); 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(); _FlatButtonState createState() => new _FlatButtonState();
} }
...@@ -24,19 +39,14 @@ class _FlatButtonState extends MaterialButtonState<FlatButton> { ...@@ -24,19 +39,14 @@ class _FlatButtonState extends MaterialButtonState<FlatButton> {
int get elevation => 0; int get elevation => 0;
Color getColor(BuildContext context, { bool highlight }) { Color getColor(BuildContext context) {
if (!config.enabled || !highlight) if (!config.enabled)
return null; return config.disabledColor;
switch (Theme.of(context).brightness) { return config.color;
case ThemeBrightness.light:
return Colors.grey[400];
case ThemeBrightness.dark:
return Colors.grey[200];
}
} }
ThemeBrightness getColorBrightness(BuildContext context) { ThemeBrightness getColorBrightness(BuildContext context) {
return Theme.of(context).brightness; return config.colorBrightness ?? Theme.of(context).brightness;
} }
} }
...@@ -19,12 +19,16 @@ class FloatingActionButton extends StatefulComponent { ...@@ -19,12 +19,16 @@ class FloatingActionButton extends StatefulComponent {
Key key, Key key,
this.child, this.child,
this.backgroundColor, this.backgroundColor,
this.elevation: 6,
this.highlightElevation: 12,
this.onPressed this.onPressed
}) : super(key: key); }) : super(key: key);
final Widget child; final Widget child;
final Color backgroundColor; final Color backgroundColor;
final VoidCallback onPressed; final VoidCallback onPressed;
final int elevation;
final int highlightElevation;
_FloatingActionButtonState createState() => new _FloatingActionButtonState(); _FloatingActionButtonState createState() => new _FloatingActionButtonState();
} }
...@@ -50,8 +54,7 @@ class _FloatingActionButtonState extends State<FloatingActionButton> { ...@@ -50,8 +54,7 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
return new Material( return new Material(
color: materialColor, color: materialColor,
type: MaterialType.circle, type: MaterialType.circle,
elevation: _highlight ? 12 : 6, elevation: _highlight ? config.highlightElevation : config.elevation,
child: new ClipOval(
child: new Container( child: new Container(
width: _kSize, width: _kSize,
height: _kSize, height: _kSize,
...@@ -66,7 +69,6 @@ class _FloatingActionButtonState extends State<FloatingActionButton> { ...@@ -66,7 +69,6 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
) )
) )
) )
)
); );
} }
} }
...@@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; ...@@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
import 'icon.dart'; import 'icon.dart';
import 'icon_theme_data.dart'; import 'icon_theme_data.dart';
import 'ink_well.dart';
class IconButton extends StatelessComponent { class IconButton extends StatelessComponent {
const IconButton({ const IconButton({
...@@ -22,11 +23,8 @@ class IconButton extends StatelessComponent { ...@@ -22,11 +23,8 @@ class IconButton extends StatelessComponent {
final VoidCallback onPressed; final VoidCallback onPressed;
Widget build(BuildContext context) { Widget build(BuildContext context) {
// TODO(abarth): We should use a radial reaction here so you can hit the return new InkResponse(
// 8.0 pixel padding as well as the icon.
return new GestureDetector(
onTap: onPressed, onTap: onPressed,
behavior: HitTestBehavior.opaque,
child: new Padding( child: new Padding(
padding: const EdgeDims.all(8.0), padding: const EdgeDims.all(8.0),
child: new Icon( child: new Icon(
......
...@@ -2,331 +2,193 @@ ...@@ -2,331 +2,193 @@
// 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:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
// This file has the following classes: import 'material.dart';
// InkWell - the widget for material-design-style inkly-reacting material, showing splashes and a highlight import 'theme.dart';
// _InkWellState - InkWell's State class
// _InkSplash - tracks a single splash
// _RenderInkSplashes - a RenderBox that renders multiple _InkSplash objects and handles gesture recognition
// _InkSplashes - the RenderObjectWidget for _RenderInkSplashes used by InkWell to handle the splashes
const int _kSplashInitialOpacity = 0x30; // 0..255
const double _kSplashCanceledVelocity = 0.7; // logical pixels per millisecond
const double _kSplashConfirmedVelocity = 0.7; // logical pixels per millisecond
const double _kSplashInitialSize = 0.0; // logical pixels
const double _kSplashUnconfirmedVelocity = 0.2; // logical pixels per millisecond
const Duration _kInkWellHighlightFadeDuration = const Duration(milliseconds: 100);
class InkWell extends StatefulComponent { class InkResponse extends StatefulComponent {
InkWell({ InkResponse({
Key key, Key key,
this.child, this.child,
this.onTap, this.onTap,
this.onDoubleTap, this.onDoubleTap,
this.onLongPress, this.onLongPress,
this.onHighlightChanged, this.onHighlightChanged
this.defaultColor,
this.highlightColor
}) : super(key: key); }) : super(key: key);
final Widget child; final Widget child;
final GestureTapCallback onTap; final GestureTapCallback onTap;
final GestureTapCallback onDoubleTap; final GestureTapCallback onDoubleTap;
final GestureLongPressCallback onLongPress; final GestureLongPressCallback onLongPress;
final _HighlightChangedCallback onHighlightChanged; final ValueChanged<bool> onHighlightChanged;
final Color defaultColor;
final Color highlightColor;
_InkWellState createState() => new _InkWellState(); _InkResponseState createState() => new _InkResponseState<InkResponse>();
} }
class _InkWellState extends State<InkWell> { class _InkResponseState<T extends InkResponse> extends State<T> {
bool _highlight = false;
Widget build(BuildContext context) {
return new AnimatedContainer(
decoration: new BoxDecoration(
backgroundColor: _highlight ? config.highlightColor : config.defaultColor
),
duration: _kInkWellHighlightFadeDuration,
child: new _InkSplashes(
onTap: config.onTap,
onDoubleTap: config.onDoubleTap,
onLongPress: config.onLongPress,
onHighlightChanged: (bool value) {
setState(() {
_highlight = value;
});
if (config.onHighlightChanged != null)
config.onHighlightChanged(value);
},
child: config.child
)
);
}
}
bool get containedInWell => false;
double _getSplashTargetSize(Size bounds, Point position) { Set<InkSplash> _splashes;
double d1 = (position - bounds.topLeft(Point.origin)).distance; InkSplash _currentSplash;
double d2 = (position - bounds.topRight(Point.origin)).distance;
double d3 = (position - bounds.bottomLeft(Point.origin)).distance;
double d4 = (position - bounds.bottomRight(Point.origin)).distance;
return math.max(math.max(d1, d2), math.max(d3, d4)).ceil().toDouble();
}
class _InkSplash { void _handleTapDown(Point position) {
_InkSplash(this.position, this.renderer) { RenderBox referenceBox = context.findRenderObject();
_targetRadius = _getSplashTargetSize(renderer.size, position); assert(Material.of(context) != null);
_radius = new ValuePerformance<double>( InkSplash splash;
variable: new AnimatedValue<double>( splash = Material.of(context).splashAt(
_kSplashInitialSize, referenceBox: referenceBox,
end: _targetRadius, position: referenceBox.globalToLocal(position),
curve: Curves.easeOut containedInWell: containedInWell,
), onRemoved: () {
duration: new Duration(milliseconds: (_targetRadius / _kSplashUnconfirmedVelocity).floor()) if (_splashes != null) {
)..addListener(_handleRadiusChange) assert(_splashes.contains(splash));
..play(); _splashes.remove(splash);
if (_currentSplash == splash)
_currentSplash = null;
} // else we're probably in deactivate()
}
);
_splashes ??= new Set<InkSplash>();
_splashes.add(splash);
_currentSplash = splash;
} }
final Point position; void _handleTap() {
final _RenderInkSplashes renderer; _currentSplash?.confirm();
_currentSplash = null;
double _targetRadius; if (config.onTap != null)
double _pinnedRadius; config.onTap();
ValuePerformance<double> _radius; }
void _updateVelocity(double velocity) { void _handleTapCancel() {
int duration = (_targetRadius / velocity).floor(); _currentSplash?.cancel();
_radius.duration = new Duration(milliseconds: duration); _currentSplash = null;
_radius.play();
} }
void confirm() { void _handleDoubleTap() {
_updateVelocity(_kSplashConfirmedVelocity); _currentSplash?.confirm();
_pinnedRadius = null; _currentSplash = null;
if (config.onDoubleTap != null)
config.onDoubleTap();
} }
void cancel() { void _handleLongPress() {
_updateVelocity(_kSplashCanceledVelocity); _currentSplash?.confirm();
_pinnedRadius = _radius.value; _currentSplash = null;
if (config.onLongPress != null)
config.onLongPress();
} }
void _handleRadiusChange() { void deactivate() {
if (_radius.value == _targetRadius) if (_splashes != null) {
renderer._removeSplash(this); Set<InkSplash> splashes = _splashes;
renderer.markNeedsPaint(); _splashes = null;
for (InkSplash splash in splashes)
splash.dispose();
_currentSplash = null;
}
assert(_currentSplash == null);
super.deactivate();
} }
void paint(PaintingCanvas canvas) { Widget build(BuildContext context) {
int opacity = (_kSplashInitialOpacity * (1.1 - (_radius.value / _targetRadius))).floor(); final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null;
Paint paint = new Paint()..color = new Color(opacity << 24); return new GestureDetector(
double radius = _pinnedRadius == null ? _radius.value : _pinnedRadius; onTapDown: enabled ? _handleTapDown : null,
canvas.drawCircle(position, radius, paint); onTap: enabled ? _handleTap : null,
onTapCancel: enabled ? _handleTapCancel : null,
onDoubleTap: config.onDoubleTap != null ? _handleDoubleTap : null,
onLongPress: config.onLongPress != null ? _handleLongPress : null,
behavior: HitTestBehavior.opaque,
child: config.child
);
} }
}
typedef _HighlightChangedCallback(bool value); }
class _RenderInkSplashes extends RenderProxyBox { /// An area of a Material that responds to touch.
_RenderInkSplashes({ ///
RenderBox child, /// Must have an ancestor Material widget in which to cause ink reactions.
class InkWell extends InkResponse {
InkWell({
Key key,
Widget child,
GestureTapCallback onTap, GestureTapCallback onTap,
GestureTapCallback onDoubleTap, GestureTapCallback onDoubleTap,
GestureLongPressCallback onLongPress, GestureLongPressCallback onLongPress,
this.onHighlightChanged this.onHighlightChanged
}) : super(child) { }) : super(
this.onTap = onTap; key: key,
this.onDoubleTap = onDoubleTap; child: child,
this.onLongPress = onLongPress; onTap: onTap,
} onDoubleTap: onDoubleTap,
onLongPress: onLongPress
GestureTapCallback get onTap => _onTap; );
GestureTapCallback _onTap;
void set onTap (GestureTapCallback value) {
_onTap = value;
_syncTapRecognizer();
}
GestureTapCallback get onDoubleTap => _onDoubleTap;
GestureTapCallback _onDoubleTap;
void set onDoubleTap (GestureTapCallback value) {
_onDoubleTap = value;
_syncDoubleTapRecognizer();
}
GestureTapCallback get onLongPress => _onLongPress;
GestureTapCallback _onLongPress;
void set onLongPress (GestureTapCallback value) {
_onLongPress = value;
_syncLongPressRecognizer();
}
_HighlightChangedCallback onHighlightChanged;
final List<_InkSplash> _splashes = new List<_InkSplash>();
_InkSplash _lastSplash;
TapGestureRecognizer _tap; final ValueChanged<bool> onHighlightChanged;
DoubleTapGestureRecognizer _doubleTap;
LongPressGestureRecognizer _longPress;
void _removeSplash(_InkSplash splash) { _InkWellState createState() => new _InkWellState();
_splashes.remove(splash); }
if (_lastSplash == splash)
_lastSplash = null;
}
void handleEvent(InputEvent event, BoxHitTestEntry entry) { class _InkWellState extends _InkResponseState<InkWell> {
if (event.type == 'pointerdown' && (onTap != null || onDoubleTap != null || onLongPress != null)) {
_tap?.addPointer(event);
_doubleTap?.addPointer(event);
_longPress?.addPointer(event);
}
}
void attach() { bool get containedInWell => true;
super.attach();
_syncTapRecognizer();
_syncDoubleTapRecognizer();
_syncLongPressRecognizer();
}
void detach() { InkHighlight _lastHighlight;
_disposeTapRecognizer();
_disposeDoubleTapRecognizer();
_disposeLongPressRecognizer();
super.detach();
}
void _syncTapRecognizer() {
if (onTap == null && onDoubleTap == null && onLongPress == null) {
_disposeTapRecognizer();
} else {
_tap ??= new TapGestureRecognizer(router: FlutterBinding.instance.pointerRouter)
..onTapDown = _handleTapDown
..onTap = _handleTap
..onTapCancel = _handleTapCancel;
}
}
void _disposeTapRecognizer() { void updateHighlight(bool value) {
_tap?.dispose(); if (value == (_lastHighlight != null && _lastHighlight.active))
_tap = null; return;
if (value) {
if (_lastHighlight == null) {
RenderBox referenceBox = context.findRenderObject();
assert(Material.of(context) != null);
_lastHighlight = Material.of(context).highlightRectAt(
referenceBox: referenceBox,
color: Theme.of(context).highlightColor,
onRemoved: () {
assert(_lastHighlight != null);
_lastHighlight = null;
} }
);
void _syncDoubleTapRecognizer() {
if (onDoubleTap == null) {
_disposeDoubleTapRecognizer();
} else { } else {
_doubleTap ??= new DoubleTapGestureRecognizer(router: FlutterBinding.instance.pointerRouter) _lastHighlight.activate();
..onDoubleTap = _handleDoubleTap;
}
} }
void _disposeDoubleTapRecognizer() {
_doubleTap?.dispose();
_doubleTap = null;
}
void _syncLongPressRecognizer() {
if (onLongPress == null) {
_disposeLongPressRecognizer();
} else { } else {
_longPress ??= new LongPressGestureRecognizer(router: FlutterBinding.instance.pointerRouter) _lastHighlight.deactivate();
..onLongPress = _handleLongPress;
} }
} if (config.onHighlightChanged != null)
config.onHighlightChanged(value != null);
void _disposeLongPressRecognizer() {
_longPress?.dispose();
_longPress = null;
} }
void _handleTapDown(Point position) { void _handleTapDown(Point position) {
_lastSplash = new _InkSplash(globalToLocal(position), this); super._handleTapDown(position);
_splashes.add(_lastSplash); updateHighlight(true);
if (onHighlightChanged != null)
onHighlightChanged(true);
} }
void _handleTap() { void _handleTap() {
_lastSplash?.confirm(); super._handleTap();
_lastSplash = null; updateHighlight(false);
if (onHighlightChanged != null)
onHighlightChanged(false);
if (onTap != null)
onTap();
} }
void _handleTapCancel() { void _handleTapCancel() {
_lastSplash?.cancel(); super._handleTapCancel();
_lastSplash = null; updateHighlight(false);
if (onHighlightChanged != null)
onHighlightChanged(false);
} }
void _handleDoubleTap() { void deactivate() {
_lastSplash?.confirm(); _lastHighlight?.dispose();
_lastSplash = null; _lastHighlight = null;
if (onDoubleTap != null) super.deactivate();
onDoubleTap();
} }
void _handleLongPress() { void dependenciesChanged(Type affectedWidgetType) {
_lastSplash?.confirm(); if (affectedWidgetType == Theme && _lastHighlight != null)
_lastSplash = null; _lastHighlight.color = Theme.of(context).highlightColor;
if (onLongPress != null)
onLongPress();
} }
bool hitTestSelf(Point position) => true;
void paint(PaintingContext context, Offset offset) {
if (!_splashes.isEmpty) {
final PaintingCanvas canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
canvas.clipRect(Point.origin & size);
for (_InkSplash splash in _splashes)
splash.paint(canvas);
canvas.restore();
}
super.paint(context, offset);
}
}
class _InkSplashes extends OneChildRenderObjectWidget {
_InkSplashes({
Key key,
Widget child,
this.onTap,
this.onDoubleTap,
this.onLongPress,
this.onHighlightChanged
}) : super(key: key, child: child);
final GestureTapCallback onTap;
final GestureTapCallback onDoubleTap;
final GestureLongPressCallback onLongPress;
final _HighlightChangedCallback onHighlightChanged;
_RenderInkSplashes createRenderObject() => new _RenderInkSplashes(
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged
);
void updateRenderObject(_RenderInkSplashes renderObject, _InkSplashes oldWidget) {
renderObject.onTap = onTap;
renderObject.onDoubleTap = onDoubleTap;
renderObject.onLongPress = onLongPress;
renderObject.onHighlightChanged = onHighlightChanged;
}
} }
...@@ -2,23 +2,76 @@ ...@@ -2,23 +2,76 @@
// 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:math' as math;
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:vector_math/vector_math_64.dart';
import 'constants.dart'; import 'constants.dart';
import 'shadows.dart'; import 'shadows.dart';
import 'theme.dart'; import 'theme.dart';
enum MaterialType { canvas, card, circle, button } enum MaterialType {
/// Infinite extent using default theme canvas color.
canvas,
/// Rounded edges, card theme color.
card,
const Map<MaterialType, double> _kEdges = const <MaterialType, double>{ /// A circle, no color by default (used for floating action buttons).
circle,
/// Rounded edges, no color by default (used for MaterialButton buttons).
button
}
const Map<MaterialType, double> kMaterialEdges = const <MaterialType, double>{
MaterialType.canvas: null, MaterialType.canvas: null,
MaterialType.card: 2.0, MaterialType.card: 2.0,
MaterialType.circle: null, MaterialType.circle: null,
MaterialType.button: 2.0, MaterialType.button: 2.0,
}; };
class Material extends StatelessComponent { abstract class InkSplash {
void confirm();
void cancel();
void dispose();
}
abstract class InkHighlight {
void activate();
void deactivate();
void dispose();
bool get active;
Color get color;
void set color(Color value);
}
abstract class MaterialInkController {
/// The color of the material
Color get color;
/// Begin a splash, centered at position relative to referenceBox.
/// If containedInWell is true, then the splash will be sized to fit
/// the referenceBox, then clipped to it when drawn.
/// When the splash is removed, onRemoved will be invoked.
InkSplash splashAt({ RenderBox referenceBox, Point position, bool containedInWell, VoidCallback onRemoved });
/// Begin a highlight, coincident with the referenceBox.
InkHighlight highlightRectAt({ RenderBox referenceBox, Color color, VoidCallback onRemoved });
/// Add an arbitrary InkFeature to this InkController.
void addInkFeature(InkFeature feature);
}
/// Describes a sheet of Material. If the layout changes (e.g. because there's a
/// list on the paper, and it's been scrolled), a LayoutChangedNotification must
/// be dispatched at the relevant subtree. (This in particular means that
/// Transitions should not be placed inside Material.)
class Material extends StatefulComponent {
Material({ Material({
Key key, Key key,
this.child, this.child,
...@@ -36,10 +89,21 @@ class Material extends StatelessComponent { ...@@ -36,10 +89,21 @@ class Material extends StatelessComponent {
final Color color; final Color color;
final TextStyle textStyle; final TextStyle textStyle;
static MaterialInkController of(BuildContext context) {
final RenderInkFeatures result = context.ancestorRenderObjectOfType(RenderInkFeatures);
return result;
}
_MaterialState createState() => new _MaterialState();
}
class _MaterialState extends State<Material> {
final GlobalKey _inkFeatureRenderer = new GlobalKey(debugLabel: 'ink renderer');
Color _getBackgroundColor(BuildContext context) { Color _getBackgroundColor(BuildContext context) {
if (color != null) if (config.color != null)
return color; return config.color;
switch (type) { switch (config.type) {
case MaterialType.canvas: case MaterialType.canvas:
return Theme.of(context).canvasColor; return Theme.of(context).canvasColor;
case MaterialType.card: case MaterialType.card:
...@@ -50,33 +114,333 @@ class Material extends StatelessComponent { ...@@ -50,33 +114,333 @@ class Material extends StatelessComponent {
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget contents = child; Color backgroundColor = _getBackgroundColor(context);
if (child != null) { Widget contents = config.child;
if (contents != null) {
contents = new DefaultTextStyle( contents = new DefaultTextStyle(
style: textStyle ?? Theme.of(context).text.body1, style: config.textStyle ?? Theme.of(context).text.body1,
child: contents child: contents
); );
if (_kEdges[type] != null) { }
contents = new NotificationListener<LayoutChangedNotification>(
onNotification: (LayoutChangedNotification notification) {
_inkFeatureRenderer.currentContext.findRenderObject().markNeedsPaint();
},
child: new InkFeatures(
key: _inkFeatureRenderer,
color: backgroundColor,
child: contents
)
);
if (config.type == MaterialType.circle) {
contents = new ClipOval(child: contents);
} else if (kMaterialEdges[config.type] != null) {
contents = new ClipRRect( contents = new ClipRRect(
xRadius: _kEdges[type], xRadius: kMaterialEdges[config.type],
yRadius: _kEdges[type], yRadius: kMaterialEdges[config.type],
child: contents child: contents
); );
} }
} contents = new AnimatedContainer(
return new DefaultTextStyle(
style: Theme.of(context).text.body1,
child: new AnimatedContainer(
curve: Curves.ease, curve: Curves.ease,
duration: kThemeChangeDuration, duration: kThemeChangeDuration,
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: _getBackgroundColor(context), backgroundColor: backgroundColor,
borderRadius: _kEdges[type], borderRadius: kMaterialEdges[config.type],
boxShadow: elevation == 0 ? null : elevationToShadow[elevation], boxShadow: config.elevation == 0 ? null : elevationToShadow[config.elevation],
shape: type == MaterialType.circle ? Shape.circle : Shape.rectangle shape: config.type == MaterialType.circle ? Shape.circle : Shape.rectangle
), ),
child: contents child: contents
)
); );
return contents;
} }
} }
const Duration _kHighlightFadeDuration = const Duration(milliseconds: 100);
const double _kDefaultSplashRadius = 35.0; // logical pixels
const int _kSplashInitialAlpha = 0x30; // 0..255
const double _kSplashCanceledVelocity = 0.7; // logical pixels per millisecond
const double _kSplashConfirmedVelocity = 0.7; // logical pixels per millisecond
const double _kSplashInitialSize = 0.0; // logical pixels
const double _kSplashUnconfirmedVelocity = 0.2; // logical pixels per millisecond
class RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
RenderInkFeatures({ RenderBox child, this.color }) : super(child);
// This is here to satisfy the MaterialInkController contract.
// The actual painting of this color is done by a Container in the
// MaterialState build method.
Color color;
final List<InkFeature> _inkFeatures = <InkFeature>[];
InkSplash splashAt({ RenderBox referenceBox, Point position, bool containedInWell, VoidCallback onRemoved }) {
double radius;
if (containedInWell) {
radius = _getSplashTargetSize(referenceBox.size, position);
} else {
radius = _kDefaultSplashRadius;
}
_InkSplash splash = new _InkSplash(
renderer: this,
referenceBox: referenceBox,
position: position,
targetRadius: radius,
clipToReferenceBox: containedInWell,
onRemoved: onRemoved
);
addInkFeature(splash);
return splash;
}
double _getSplashTargetSize(Size bounds, Point position) {
double d1 = (position - bounds.topLeft(Point.origin)).distance;
double d2 = (position - bounds.topRight(Point.origin)).distance;
double d3 = (position - bounds.bottomLeft(Point.origin)).distance;
double d4 = (position - bounds.bottomRight(Point.origin)).distance;
return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble();
}
InkHighlight highlightRectAt({ RenderBox referenceBox, Color color, VoidCallback onRemoved }) {
_InkHighlight highlight = new _InkHighlight(
renderer: this,
referenceBox: referenceBox,
color: color,
onRemoved: onRemoved
);
addInkFeature(highlight);
return highlight;
}
void addInkFeature(InkFeature feature) {
assert(!feature._debugDisposed);
assert(feature.renderer == this);
assert(!_inkFeatures.contains(feature));
_inkFeatures.add(feature);
markNeedsPaint();
}
void _removeFeature(InkFeature feature) {
_inkFeatures.remove(feature);
markNeedsPaint();
}
bool hitTestSelf(Point position) => true;
void paint(PaintingContext context, Offset offset) {
if (_inkFeatures.isNotEmpty) {
final Canvas canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
canvas.clipRect(Point.origin & size);
for (InkFeature inkFeature in _inkFeatures)
inkFeature._paint(canvas);
canvas.restore();
}
super.paint(context, offset);
}
}
class InkFeatures extends OneChildRenderObjectWidget {
InkFeatures({ Key key, this.color, Widget child }) : super(key: key, child: child);
final Color color;
RenderInkFeatures createRenderObject() => new RenderInkFeatures(color: color);
void updateRenderObject(RenderInkFeatures renderObject, InkFeatures oldWidget) {
renderObject.color = color;
}
}
abstract class InkFeature {
InkFeature({
this.renderer,
this.referenceBox,
this.onRemoved
});
final RenderInkFeatures renderer;
final RenderBox referenceBox;
final VoidCallback onRemoved;
bool _debugDisposed = false;
void dispose() {
assert(!_debugDisposed);
assert(() { _debugDisposed = true; return true; });
renderer._removeFeature(this);
if (onRemoved != null)
onRemoved();
}
void _paint(Canvas canvas) {
assert(referenceBox.attached);
assert(!_debugDisposed);
// find the chain of renderers from us to the feature's referenceBox
List<RenderBox> descendants = <RenderBox>[];
RenderBox node = referenceBox;
while (node != renderer) {
descendants.add(node);
node = node.parent;
assert(node != null);
}
// determine the transform that gets our coordinate system to be like theirs
Matrix4 transform = new Matrix4.identity();
for (RenderBox descendant in descendants.reversed)
descendant.applyPaintTransform(transform);
paintFeature(canvas, transform);
}
void paintFeature(Canvas canvas, Matrix4 transform);
String toString() => "$runtimeType@$hashCode";
}
class _InkSplash extends InkFeature implements InkSplash {
_InkSplash({
RenderInkFeatures renderer,
RenderBox referenceBox,
this.position,
this.targetRadius,
this.clipToReferenceBox,
VoidCallback onRemoved
}) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
_radius = new ValuePerformance<double>(
variable: new AnimatedValue<double>(
_kSplashInitialSize,
end: targetRadius,
curve: Curves.easeOut
),
duration: new Duration(milliseconds: (targetRadius / _kSplashUnconfirmedVelocity).floor())
)..addListener(_handleRadiusChange)
..play();
}
final Point position;
final double targetRadius;
final bool clipToReferenceBox;
double _pinnedRadius;
ValuePerformance<double> _radius;
void confirm() {
_updateVelocity(_kSplashConfirmedVelocity);
}
void cancel() {
_updateVelocity(_kSplashCanceledVelocity);
_pinnedRadius = _radius.value;
}
void _updateVelocity(double velocity) {
int duration = (targetRadius / velocity).floor();
_radius.duration = new Duration(milliseconds: duration);
_radius.play();
}
void _handleRadiusChange() {
if (_radius.value == targetRadius)
dispose();
else
renderer.markNeedsPaint();
}
void dispose() {
_radius.stop();
super.dispose();
}
void paintFeature(Canvas canvas, Matrix4 transform) {
int alpha = (_kSplashInitialAlpha * (1.1 - (_radius.value / targetRadius))).floor();
Paint paint = new Paint()..color = new Color(alpha << 24); // TODO(ianh): in dark theme, this isn't very visible
double radius = _pinnedRadius == null ? _radius.value : _pinnedRadius;
Offset originOffset = MatrixUtils.getAsTranslation(transform);
if (originOffset == null) {
canvas.save();
canvas.concat(transform.storage);
if (clipToReferenceBox)
canvas.clipRect(Point.origin & referenceBox.size);
canvas.drawCircle(position, radius, paint);
canvas.restore();
} else {
if (clipToReferenceBox) {
canvas.save();
canvas.clipRect(originOffset.toPoint() & referenceBox.size);
}
canvas.drawCircle(position + originOffset, radius, paint);
if (clipToReferenceBox)
canvas.restore();
}
}
}
class _InkHighlight extends InkFeature implements InkHighlight {
_InkHighlight({
RenderInkFeatures renderer,
RenderBox referenceBox,
Color color,
VoidCallback onRemoved
}) : _color = color,
super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
_alpha = new ValuePerformance<int>(
variable: new AnimatedIntValue(
0,
end: color.alpha,
curve: Curves.linear
),
duration: _kHighlightFadeDuration
)..addListener(_handleAlphaChange)
..play();
}
Color get color => _color;
Color _color;
void set color(Color value) {
if (value == _color)
return;
_color = value;
renderer.markNeedsPaint();
}
bool get active => _active;
bool _active = true;
ValuePerformance<int> _alpha;
void activate() {
_active = true;
_alpha.forward();
}
void deactivate() {
_active = false;
_alpha.reverse();
}
void _handleAlphaChange() {
if (_alpha.value == 0.0 && !_active)
dispose();
else
renderer.markNeedsPaint();
}
void dispose() {
_alpha.stop();
super.dispose();
}
void paintFeature(Canvas canvas, Matrix4 transform) {
Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Offset originOffset = MatrixUtils.getAsTranslation(transform);
if (originOffset == null) {
canvas.save();
canvas.concat(transform.storage);
canvas.drawRect(Point.origin & referenceBox.size, paint);
canvas.restore();
} else {
canvas.drawRect(originOffset.toPoint() & referenceBox.size, paint);
}
}
}
...@@ -36,12 +36,16 @@ abstract class MaterialButton extends StatefulComponent { ...@@ -36,12 +36,16 @@ abstract class MaterialButton extends StatefulComponent {
MaterialButton({ MaterialButton({
Key key, Key key,
this.child, this.child,
this.textTheme,
this.textColor, this.textColor,
this.disabledTextColor,
this.onPressed this.onPressed
}) : super(key: key); }) : super(key: key);
final Widget child; final Widget child;
final ButtonColor textColor; final ButtonColor textTheme;
final Color textColor;
final Color disabledTextColor;
final VoidCallback onPressed; final VoidCallback onPressed;
bool get enabled => onPressed != null; bool get enabled => onPressed != null;
...@@ -57,12 +61,14 @@ abstract class MaterialButtonState<T extends MaterialButton> extends State<T> { ...@@ -57,12 +61,14 @@ abstract class MaterialButtonState<T extends MaterialButton> extends State<T> {
bool highlight = false; bool highlight = false;
int get elevation; int get elevation;
Color getColor(BuildContext context, { bool highlight }); Color getColor(BuildContext context);
ThemeBrightness getColorBrightness(BuildContext context); ThemeBrightness getColorBrightness(BuildContext context);
Color getTextColor(BuildContext context) { Color getTextColor(BuildContext context) {
if (config.enabled) { 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: case ButtonColor.accent:
return Theme.of(context).accentColor; return Theme.of(context).accentColor;
case ButtonColor.normal: case ButtonColor.normal:
...@@ -74,6 +80,8 @@ abstract class MaterialButtonState<T extends MaterialButton> extends State<T> { ...@@ -74,6 +80,8 @@ abstract class MaterialButtonState<T extends MaterialButton> extends State<T> {
} }
} }
} }
if (config.disabledTextColor != null)
return config.disabledTextColor;
switch (getColorBrightness(context)) { switch (getColorBrightness(context)) {
case ThemeBrightness.light: case ThemeBrightness.light:
return Colors.black26; return Colors.black26;
...@@ -84,34 +92,45 @@ abstract class MaterialButtonState<T extends MaterialButton> extends State<T> { ...@@ -84,34 +92,45 @@ abstract class MaterialButtonState<T extends MaterialButton> extends State<T> {
void _handleHighlightChanged(bool value) { void _handleHighlightChanged(bool value) {
setState(() { setState(() {
// mostly just used by the RaisedButton subclass to change the elevation
highlight = value; highlight = value;
}); });
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget contents = new Container( Widget contents = new InkWell(
onTap: config.onPressed,
onHighlightChanged: _handleHighlightChanged,
child: new Container(
padding: new EdgeDims.symmetric(horizontal: 8.0), padding: new EdgeDims.symmetric(horizontal: 8.0),
child: new Center( child: new Center(
widthFactor: 1.0, widthFactor: 1.0,
child: config.child 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( return new Container(
height: 36.0, height: 36.0,
constraints: new BoxConstraints(minWidth: 88.0), constraints: new BoxConstraints(minWidth: 88.0),
margin: new EdgeDims.all(8.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 { ...@@ -12,36 +12,56 @@ class RaisedButton extends MaterialButton {
RaisedButton({ RaisedButton({
Key key, Key key,
Widget child, Widget child,
this.color,
this.colorBrightness,
this.disabledColor,
this.elevation: 2,
this.highlightElevation: 8,
this.disabledElevation: 0,
VoidCallback onPressed VoidCallback onPressed
}) : super(key: key, }) : super(key: key,
child: child, child: child,
onPressed: onPressed); 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(); _RaisedButtonState createState() => new _RaisedButtonState();
} }
class _RaisedButtonState extends MaterialButtonState<RaisedButton> { 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.enabled) {
if (config.color != null)
return config.color;
switch (Theme.of(context).brightness) { switch (Theme.of(context).brightness) {
case ThemeBrightness.light: case ThemeBrightness.light:
if (highlight)
return Colors.grey[350];
else
return Colors.grey[300]; return Colors.grey[300];
break;
case ThemeBrightness.dark: case ThemeBrightness.dark:
Map<int, Color> swatch = Theme.of(context).primarySwatch ?? Colors.blue; Map<int, Color> swatch = Theme.of(context).primarySwatch ?? Colors.blue;
if (highlight)
return swatch[700];
else
return swatch[600]; return swatch[600];
break;
} }
} else { } else {
if (config.disabledColor != null)
return config.disabledColor;
switch (Theme.of(context).brightness) { switch (Theme.of(context).brightness) {
case ThemeBrightness.light: case ThemeBrightness.light:
return Colors.black12; return Colors.black12;
...@@ -52,7 +72,7 @@ class _RaisedButtonState extends MaterialButtonState<RaisedButton> { ...@@ -52,7 +72,7 @@ class _RaisedButtonState extends MaterialButtonState<RaisedButton> {
} }
ThemeBrightness getColorBrightness(BuildContext context) { 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> { ...@@ -115,7 +115,10 @@ class ScaffoldState extends State<Scaffold> {
} }
ScaffoldFeatureController<SnackBar> controller; ScaffoldFeatureController<SnackBar> controller;
controller = new ScaffoldFeatureController<SnackBar>._( 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(), new Completer(),
() { () {
assert(_snackBars.first == controller); assert(_snackBars.first == controller);
......
...@@ -5,14 +5,18 @@ ...@@ -5,14 +5,18 @@
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'flat_button.dart';
import 'material.dart'; import 'material.dart';
import 'material_button.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
import 'typography.dart'; import 'typography.dart';
// https://www.google.com/design/spec/components/snackbars-toasts.html#snackbars-toasts-specs // https://www.google.com/design/spec/components/snackbars-toasts.html#snackbars-toasts-specs
const double _kSideMargins = 24.0; const double _kSideMargins = 24.0;
const double _kSingleLineVerticalPadding = 14.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); const Color _kSnackBackground = const Color(0xFF323232);
// TODO(ianh): We should check if the given text and actions are going to fit on // TODO(ianh): We should check if the given text and actions are going to fit on
...@@ -35,11 +39,11 @@ class SnackBarAction extends StatelessComponent { ...@@ -35,11 +39,11 @@ class SnackBarAction extends StatelessComponent {
final VoidCallback onPressed; final VoidCallback onPressed;
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new GestureDetector( return new Container(
onTap: onPressed,
child: new Container(
margin: const EdgeDims.only(left: _kSideMargins), margin: const EdgeDims.only(left: _kSideMargins),
padding: const EdgeDims.symmetric(vertical: _kSingleLineVerticalPadding), child: new FlatButton(
onPressed: onPressed,
textTheme: ButtonColor.accent,
child: new Text(label) child: new Text(label)
) )
); );
...@@ -77,6 +81,7 @@ class SnackBar extends StatelessComponent { ...@@ -77,6 +81,7 @@ class SnackBar extends StatelessComponent {
]; ];
if (actions != null) if (actions != null)
children.addAll(actions); children.addAll(actions);
ThemeData theme = Theme.of(context);
return new ClipRect( return new ClipRect(
child: new AlignTransition( child: new AlignTransition(
performance: performance, performance: performance,
...@@ -87,12 +92,20 @@ class SnackBar extends StatelessComponent { ...@@ -87,12 +92,20 @@ class SnackBar extends StatelessComponent {
color: _kSnackBackground, color: _kSnackBackground,
child: new Container( child: new Container(
margin: const EdgeDims.symmetric(horizontal: _kSideMargins), margin: const EdgeDims.symmetric(horizontal: _kSideMargins),
child: new DefaultTextStyle( child: new Theme(
style: new TextStyle(color: Theme.of(context).accentColor), data: new ThemeData(
brightness: ThemeBrightness.dark,
accentColor: theme.accentColor,
accentColorBrightness: theme.accentColorBrightness,
text: Typography.white
),
child: new FadeTransition( child: new FadeTransition(
performance: performance, performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: _snackBarFadeCurve), 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 { ...@@ -110,9 +123,9 @@ class SnackBar extends StatelessComponent {
); );
} }
SnackBar withPerformance(Performance newPerformance) { SnackBar withPerformance(Performance newPerformance, { Key fallbackKey }) {
return new SnackBar( return new SnackBar(
key: key, key: key ?? fallbackKey,
content: content, content: content,
actions: actions, actions: actions,
duration: duration, duration: duration,
......
...@@ -10,11 +10,11 @@ import 'package:flutter/rendering.dart'; ...@@ -10,11 +10,11 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'constants.dart';
import 'icon.dart'; import 'icon.dart';
import 'icon_theme.dart'; import 'icon_theme.dart';
import 'icon_theme_data.dart'; import 'icon_theme_data.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material.dart';
import 'theme.dart'; import 'theme.dart';
typedef void TabSelectedIndexChanged(int selectedIndex); typedef void TabSelectedIndexChanged(int selectedIndex);
...@@ -403,6 +403,10 @@ class TabBarSelection { ...@@ -403,6 +403,10 @@ class TabBarSelection {
int _previousIndex = 0; 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 { class TabBar extends Scrollable {
TabBar({ TabBar({
Key key, Key key,
...@@ -551,13 +555,13 @@ class _TabBarState extends ScrollableState<TabBar> { ...@@ -551,13 +555,13 @@ class _TabBarState extends ScrollableState<TabBar> {
Widget buildContent(BuildContext context) { Widget buildContent(BuildContext context) {
assert(config.labels != null && config.labels.isNotEmpty); assert(config.labels != null && config.labels.isNotEmpty);
assert(Material.of(context) != null);
ThemeData themeData = Theme.of(context); ThemeData themeData = Theme.of(context);
Color backgroundColor = themeData.primaryColor; Color backgroundColor = Material.of(context).color;
Color indicatorColor = themeData.accentColor; Color indicatorColor = themeData.accentColor;
if (indicatorColor == backgroundColor) { if (indicatorColor == backgroundColor)
indicatorColor = Colors.white; indicatorColor = Colors.white;
}
TextStyle textStyle = themeData.primaryTextTheme.body1; TextStyle textStyle = themeData.primaryTextTheme.body1;
IconThemeData iconTheme = themeData.primaryIconTheme; IconThemeData iconTheme = themeData.primaryIconTheme;
...@@ -571,7 +575,7 @@ class _TabBarState extends ScrollableState<TabBar> { ...@@ -571,7 +575,7 @@ class _TabBarState extends ScrollableState<TabBar> {
textAndIcons = true; textAndIcons = true;
} }
Widget content = new IconTheme( Widget contents = new IconTheme(
data: iconTheme, data: iconTheme,
child: new DefaultTextStyle( child: new DefaultTextStyle(
style: textStyle, style: textStyle,
...@@ -594,23 +598,17 @@ class _TabBarState extends ScrollableState<TabBar> { ...@@ -594,23 +598,17 @@ class _TabBarState extends ScrollableState<TabBar> {
); );
if (config.isScrollable) { if (config.isScrollable) {
content = new SizeObserver( contents = new SizeObserver(
onSizeChanged: _handleViewportSizeChanged, onSizeChanged: _handleViewportSizeChanged,
child: new Viewport( child: new Viewport(
scrollDirection: ScrollDirection.horizontal, scrollDirection: ScrollDirection.horizontal,
scrollOffset: new Offset(scrollOffset, 0.0), scrollOffset: new Offset(scrollOffset, 0.0),
child: content child: contents
) )
); );
} }
return new AnimatedContainer( return contents;
decoration: new BoxDecoration(
backgroundColor: backgroundColor
),
duration: kThemeChangeDuration,
child: content
);
} }
} }
......
...@@ -27,15 +27,7 @@ class ThemeData { ...@@ -27,15 +27,7 @@ class ThemeData {
// Some users want the pre-multiplied color, others just want the opacity. // Some users want the pre-multiplied color, others just want the opacity.
hintColor = brightness == ThemeBrightness.dark ? const Color(0x42FFFFFF) : const Color(0x4C000000), hintColor = brightness == ThemeBrightness.dark ? const Color(0x42FFFFFF) : const Color(0x4C000000),
hintOpacity = brightness == ThemeBrightness.dark ? 0.26 : 0.30, hintOpacity = brightness == ThemeBrightness.dark ? 0.26 : 0.30,
// TODO(eseidel): Where are highlight and selected colors documented? highlightColor = brightness == ThemeBrightness.dark ? const Color(0x42FFFFFF) : const Color(0x1F000000),
// 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),
text = brightness == ThemeBrightness.dark ? Typography.white : Typography.black { text = brightness == ThemeBrightness.dark ? Typography.white : Typography.black {
assert(brightness != null); assert(brightness != null);
...@@ -63,6 +55,13 @@ class ThemeData { ...@@ -63,6 +55,13 @@ class ThemeData {
/// The brightness of the overall theme of the application. Used by widgets /// 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 /// like buttons to determine what color to pick when not using the primary or
/// accent color. /// 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 ThemeBrightness brightness;
final Map<int, Color> primarySwatch; final Map<int, Color> primarySwatch;
...@@ -71,8 +70,9 @@ class ThemeData { ...@@ -71,8 +70,9 @@ class ThemeData {
final Color dividerColor; final Color dividerColor;
final Color hintColor; final Color hintColor;
final Color highlightColor; final Color highlightColor;
final Color selectedColor;
final double hintOpacity; final double hintOpacity;
/// Text with a color that contrasts with the card and canvas colors.
final TextTheme text; final TextTheme text;
/// The background colour for major parts of the app (toolbars, tab bars, etc) /// The background colour for major parts of the app (toolbars, tab bars, etc)
...@@ -128,7 +128,6 @@ class ThemeData { ...@@ -128,7 +128,6 @@ class ThemeData {
(otherData.dividerColor == dividerColor) && (otherData.dividerColor == dividerColor) &&
(otherData.hintColor == hintColor) && (otherData.hintColor == hintColor) &&
(otherData.highlightColor == highlightColor) && (otherData.highlightColor == highlightColor) &&
(otherData.selectedColor == selectedColor) &&
(otherData.hintOpacity == hintOpacity) && (otherData.hintOpacity == hintOpacity) &&
(otherData.text == text) && (otherData.text == text) &&
(otherData.primaryColorBrightness == primaryColorBrightness) && (otherData.primaryColorBrightness == primaryColorBrightness) &&
...@@ -143,7 +142,6 @@ class ThemeData { ...@@ -143,7 +142,6 @@ class ThemeData {
value = 37 * value + dividerColor.hashCode; value = 37 * value + dividerColor.hashCode;
value = 37 * value + hintColor.hashCode; value = 37 * value + hintColor.hashCode;
value = 37 * value + highlightColor.hashCode; value = 37 * value + highlightColor.hashCode;
value = 37 * value + selectedColor.hashCode;
value = 37 * value + hintOpacity.hashCode; value = 37 * value + hintOpacity.hashCode;
value = 37 * value + text.hashCode; value = 37 * value + text.hashCode;
value = 37 * value + primaryColorBrightness.hashCode; value = 37 * value + primaryColorBrightness.hashCode;
......
...@@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart'; ...@@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
import 'icon_theme.dart'; import 'icon_theme.dart';
import 'icon_theme_data.dart'; import 'icon_theme_data.dart';
import 'shadows.dart'; import 'material.dart';
import 'tabs.dart'; import 'tabs.dart';
import 'theme.dart'; import 'theme.dart';
import 'typography.dart'; import 'typography.dart';
...@@ -36,9 +36,9 @@ class ToolBar extends StatelessComponent { ...@@ -36,9 +36,9 @@ class ToolBar extends StatelessComponent {
final TextTheme textTheme; final TextTheme textTheme;
final EdgeDims padding; final EdgeDims padding;
ToolBar withPadding(EdgeDims newPadding) { ToolBar withPadding(EdgeDims newPadding, { Key fallbackKey }) {
return new ToolBar( return new ToolBar(
key: key, key: key ?? fallbackKey,
left: left, left: left,
center: center, center: center,
right: right, right: right,
...@@ -67,52 +67,63 @@ class ToolBar extends StatelessComponent { ...@@ -67,52 +67,63 @@ class ToolBar extends StatelessComponent {
sideStyle ??= primaryTextTheme.body2; sideStyle ??= primaryTextTheme.body2;
} }
List<Widget> children = new List<Widget>(); final List<Widget> firstRow = <Widget>[];
if (left != null) if (left != null)
children.add(left); firstRow.add(left);
firstRow.add(
children.add(
new Flexible( new Flexible(
child: new Padding( 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) if (right != null)
children.addAll(right); firstRow.addAll(right);
final List<Widget> columnChildren = <Widget>[ final List<Widget> rows = <Widget>[
new Container(height: kToolBarHeight, child: new Row(children)) new Container(
height: kToolBarHeight,
child: new DefaultTextStyle(
style: sideStyle,
child: new Row(firstRow)
)
)
]; ];
if (bottom != null) {
if (bottom != null) rows.add(
columnChildren.add(new DefaultTextStyle( new DefaultTextStyle(
style: centerStyle, style: centerStyle,
child: new Container(height: kExtendedToolBarHeight - kToolBarHeight, child: bottom) child: new Container(
)); height: kExtendedToolBarHeight - kToolBarHeight,
child: bottom
)
)
);
}
if (tabBar != null) if (tabBar != null)
columnChildren.add(tabBar); rows.add(tabBar);
Widget content = new AnimatedContainer( EdgeDims combinedPadding = new EdgeDims.symmetric(horizontal: 8.0);
duration: kThemeChangeDuration, if (padding != null)
padding: new EdgeDims.symmetric(horizontal: 8.0), combinedPadding += padding;
decoration: new BoxDecoration(
backgroundColor: color, Widget contents = new Material(
boxShadow: elevationToShadow[elevation] color: color,
), elevation: elevation,
child: new DefaultTextStyle( child: new Container(
style: sideStyle, padding: combinedPadding,
child: new Container(padding: padding, child: new Column(columnChildren, justifyContent: FlexJustifyContent.collapse)) child: new Column(
rows,
justifyContent: FlexJustifyContent.collapse
)
) )
); );
if (iconThemeData != null) if (iconThemeData != null)
content = new IconTheme(data: iconThemeData, child: content); contents = new IconTheme(data: iconThemeData, child: contents);
return content;
return contents;
} }
} }
...@@ -386,8 +386,17 @@ abstract class State<T extends StatefulComponent> { ...@@ -386,8 +386,17 @@ abstract class State<T extends StatefulComponent> {
_element.markNeedsBuild(); _element.markNeedsBuild();
} }
/// Called when this object is removed from the tree. Override this to clean /// Called when this object is removed from the tree.
/// up any resources allocated by this object. /// 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 /// If you override this, make sure to end your method with a call to
/// super.dispose(). /// super.dispose().
...@@ -405,6 +414,11 @@ abstract class State<T extends StatefulComponent> { ...@@ -405,6 +414,11 @@ abstract class State<T extends StatefulComponent> {
/// provides the set of inherited widgets for this location in the tree. /// provides the set of inherited widgets for this location in the tree.
Widget build(BuildContext context); 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() { String toString() {
final List<String> data = <String>[]; final List<String> data = <String>[];
debugFillDescription(data); debugFillDescription(data);
...@@ -540,6 +554,7 @@ abstract class BuildContext { ...@@ -540,6 +554,7 @@ abstract class BuildContext {
InheritedWidget inheritFromWidgetOfType(Type targetType); InheritedWidget inheritFromWidgetOfType(Type targetType);
Widget ancestorWidgetOfType(Type targetType); Widget ancestorWidgetOfType(Type targetType);
State ancestorStateOfType(Type targetType); State ancestorStateOfType(Type targetType);
RenderObject ancestorRenderObjectOfType(Type targetType);
void visitAncestorElements(bool visitor(Element element)); void visitAncestorElements(bool visitor(Element element));
void visitChildElements(void visitor(Element element)); void visitChildElements(void visitor(Element element));
} }
...@@ -777,7 +792,7 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -777,7 +792,7 @@ abstract class Element<T extends Widget> implements BuildContext {
assert(child._parent == this); assert(child._parent == this);
child._parent = null; child._parent = null;
child.detachRenderObject(); child.detachRenderObject();
_inactiveElements.add(child); _inactiveElements.add(child); // this eventually calls child.deactivate()
} }
void deactivate() { void deactivate() {
...@@ -839,13 +854,24 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -839,13 +854,24 @@ abstract class Element<T extends Widget> implements BuildContext {
return statefulAncestor?.state; 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)) { void visitAncestorElements(bool visitor(Element element)) {
Element ancestor = _parent; Element ancestor = _parent;
while (ancestor != null && visitor(ancestor)) while (ancestor != null && visitor(ancestor))
ancestor = ancestor._parent; ancestor = ancestor._parent;
} }
void dependenciesChanged() { void dependenciesChanged(Type affectedWidgetType) {
assert(false); assert(false);
} }
...@@ -1024,7 +1050,7 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -1024,7 +1050,7 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
/// Called by rebuild() after the appropriate checks have been made. /// Called by rebuild() after the appropriate checks have been made.
void performRebuild(); void performRebuild();
void dependenciesChanged() { void dependenciesChanged(Type affectedWidgetType) {
markNeedsBuild(); markNeedsBuild();
} }
...@@ -1169,6 +1195,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>> ...@@ -1169,6 +1195,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>>
rebuild(); rebuild();
} }
void deactivate() {
_state.deactivate();
super.deactivate();
}
void unmount() { void unmount() {
super.unmount(); super.unmount();
_state.dispose(); _state.dispose();
...@@ -1183,6 +1214,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>> ...@@ -1183,6 +1214,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>>
_state = null; _state = null;
} }
void dependenciesChanged(Type affectedWidgetType) {
super.dependenciesChanged(affectedWidgetType);
_state.dependenciesChanged(affectedWidgetType);
}
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
if (state != null) if (state != null)
...@@ -1252,7 +1288,7 @@ class InheritedElement extends ProxyElement<InheritedWidget> { ...@@ -1252,7 +1288,7 @@ class InheritedElement extends ProxyElement<InheritedWidget> {
void notifyChildren(Element child) { void notifyChildren(Element child) {
if (child._dependencies != null && if (child._dependencies != null &&
child._dependencies.contains(ourRuntimeType)) { child._dependencies.contains(ourRuntimeType)) {
child.dependenciesChanged(); child.dependenciesChanged(ourRuntimeType);
} }
if (child.runtimeType != ourRuntimeType) if (child.runtimeType != ourRuntimeType)
child.visitChildren(notifyChildren); 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'; ...@@ -16,6 +16,7 @@ import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'homogeneous_viewport.dart'; import 'homogeneous_viewport.dart';
import 'mixed_viewport.dart'; import 'mixed_viewport.dart';
import 'notification_listener.dart';
import 'page_storage.dart'; import 'page_storage.dart';
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
...@@ -50,6 +51,8 @@ abstract class Scrollable extends StatefulComponent { ...@@ -50,6 +51,8 @@ abstract class Scrollable extends StatefulComponent {
final ScrollListener onScrollEnd; final ScrollListener onScrollEnd;
final SnapOffsetCallback snapOffsetCallback; final SnapOffsetCallback snapOffsetCallback;
final double snapAlignmentOffset; final double snapAlignmentOffset;
ScrollableState createState();
} }
abstract class ScrollableState<T extends Scrollable> extends State<T> { abstract class ScrollableState<T extends Scrollable> extends State<T> {
...@@ -180,6 +183,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -180,6 +183,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
_scrollOffset = newScrollOffset; _scrollOffset = newScrollOffset;
}); });
PageStorage.of(context)?.writeState(context, _scrollOffset); PageStorage.of(context)?.writeState(context, _scrollOffset);
new ScrollNotification(this, _scrollOffset).dispatch(context);
dispatchOnScroll(); dispatchOnScroll();
} }
...@@ -271,6 +275,12 @@ ScrollableState findScrollableAncestor(BuildContext context) { ...@@ -271,6 +275,12 @@ ScrollableState findScrollableAncestor(BuildContext context) {
return result; 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 }) { Future ensureWidgetIsVisible(BuildContext context, { Duration duration, Curve curve }) {
assert(context.findRenderObject() is RenderBox); assert(context.findRenderObject() is RenderBox);
// TODO(abarth): This function doesn't handle nested scrollable widgets. // TODO(abarth): This function doesn't handle nested scrollable widgets.
......
...@@ -25,6 +25,7 @@ export 'src/widgets/mimic.dart'; ...@@ -25,6 +25,7 @@ export 'src/widgets/mimic.dart';
export 'src/widgets/mixed_viewport.dart'; export 'src/widgets/mixed_viewport.dart';
export 'src/widgets/modal_barrier.dart'; export 'src/widgets/modal_barrier.dart';
export 'src/widgets/navigator.dart'; export 'src/widgets/navigator.dart';
export 'src/widgets/notification_listener.dart';
export 'src/widgets/overlay.dart'; export 'src/widgets/overlay.dart';
export 'src/widgets/page_storage.dart'; export 'src/widgets/page_storage.dart';
export 'src/widgets/placeholder.dart'; export 'src/widgets/placeholder.dart';
......
...@@ -2,6 +2,10 @@ import 'package:flutter/animation.dart'; ...@@ -2,6 +2,10 @@ import 'package:flutter/animation.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
test("Check for a time dilation being in effect", () {
expect(timeDilation, equals(1.0));
});
test("Can cancel queued callback", () { test("Can cancel queued callback", () {
int secondId; int secondId;
......
...@@ -11,7 +11,8 @@ void main() { ...@@ -11,7 +11,8 @@ void main() {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
DateTime currentValue; DateTime currentValue;
Widget widget = new Block(<Widget>[ Widget widget = new Material(
child: new Block(<Widget>[
new DatePicker( new DatePicker(
selectedDate: new DateTime.utc(2015, 6, 9, 7, 12), selectedDate: new DateTime.utc(2015, 6, 9, 7, 12),
firstDate: new DateTime.utc(2013), firstDate: new DateTime.utc(2013),
...@@ -20,7 +21,8 @@ void main() { ...@@ -20,7 +21,8 @@ void main() {
currentValue = dateTime; currentValue = dateTime;
} }
) )
]); ])
);
tester.pumpWidget(widget); tester.pumpWidget(widget);
......
...@@ -11,18 +11,22 @@ import 'test_matchers.dart'; ...@@ -11,18 +11,22 @@ import 'test_matchers.dart';
Key firstKey = new Key('first'); Key firstKey = new Key('first');
Key secondKey = new Key('second'); Key secondKey = new Key('second');
final Map<String, RouteBuilder> routes = <String, RouteBuilder>{ final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
'/': (RouteArguments args) => new Block([ '/': (RouteArguments args) => new Material(
child: new Block([
new Container(height: 100.0, width: 100.0), 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 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 Container(height: 100.0, width: 100.0),
new FlatButton(child: new Text('button'), onPressed: () => Navigator.pushNamed(args.context, '/two')), new FlatButton(child: new Text('button'), onPressed: () => Navigator.pushNamed(args.context, '/two')),
]), ])
'/two': (RouteArguments args) => new Block([ ),
'/two': (RouteArguments args) => new Material(
child: new Block([
new Container(height: 150.0, width: 150.0), 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 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 Container(height: 150.0, width: 150.0),
new FlatButton(child: new Text('button'), onPressed: () => Navigator.pop(args.context)), new FlatButton(child: new Text('button'), onPressed: () => Navigator.pop(args.context)),
]), ])
),
}; };
void main() { void main() {
......
...@@ -10,10 +10,12 @@ import 'package:test/test.dart'; ...@@ -10,10 +10,12 @@ import 'package:test/test.dart';
TabBarSelection selection; TabBarSelection selection;
Widget buildFrame({ List<String> tabs, bool isScrollable: false }) { Widget buildFrame({ List<String> tabs, bool isScrollable: false }) {
return new TabBar( return new Material(
child: new TabBar(
labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(), labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(),
selection: selection, selection: selection,
isScrollable: isScrollable 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