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(
......
......@@ -2,331 +2,193 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
// This file has the following classes:
// InkWell - the widget for material-design-style inkly-reacting material, showing splashes and a highlight
// _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);
import 'material.dart';
import 'theme.dart';
class InkWell extends StatefulComponent {
InkWell({
class InkResponse extends StatefulComponent {
InkResponse({
Key key,
this.child,
this.onTap,
this.onDoubleTap,
this.onLongPress,
this.onHighlightChanged,
this.defaultColor,
this.highlightColor
this.onHighlightChanged
}) : super(key: key);
final Widget child;
final GestureTapCallback onTap;
final GestureTapCallback onDoubleTap;
final GestureLongPressCallback onLongPress;
final _HighlightChangedCallback onHighlightChanged;
final Color defaultColor;
final Color highlightColor;
final ValueChanged<bool> onHighlightChanged;
_InkWellState createState() => new _InkWellState();
_InkResponseState createState() => new _InkResponseState<InkResponse>();
}
class _InkWellState extends State<InkWell> {
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
)
);
}
}
class _InkResponseState<T extends InkResponse> extends State<T> {
bool get containedInWell => false;
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)).ceil().toDouble();
}
Set<InkSplash> _splashes;
InkSplash _currentSplash;
class _InkSplash {
_InkSplash(this.position, this.renderer) {
_targetRadius = _getSplashTargetSize(renderer.size, position);
_radius = new ValuePerformance<double>(
variable: new AnimatedValue<double>(
_kSplashInitialSize,
end: _targetRadius,
curve: Curves.easeOut
),
duration: new Duration(milliseconds: (_targetRadius / _kSplashUnconfirmedVelocity).floor())
)..addListener(_handleRadiusChange)
..play();
void _handleTapDown(Point position) {
RenderBox referenceBox = context.findRenderObject();
assert(Material.of(context) != null);
InkSplash splash;
splash = Material.of(context).splashAt(
referenceBox: referenceBox,
position: referenceBox.globalToLocal(position),
containedInWell: containedInWell,
onRemoved: () {
if (_splashes != null) {
assert(_splashes.contains(splash));
_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;
final _RenderInkSplashes renderer;
double _targetRadius;
double _pinnedRadius;
ValuePerformance<double> _radius;
void _updateVelocity(double velocity) {
int duration = (_targetRadius / velocity).floor();
_radius.duration = new Duration(milliseconds: duration);
_radius.play();
void _handleTap() {
_currentSplash?.confirm();
_currentSplash = null;
if (config.onTap != null)
config.onTap();
}
void confirm() {
_updateVelocity(_kSplashConfirmedVelocity);
_pinnedRadius = null;
void _handleTapCancel() {
_currentSplash?.cancel();
_currentSplash = null;
}
void cancel() {
_updateVelocity(_kSplashCanceledVelocity);
_pinnedRadius = _radius.value;
void _handleDoubleTap() {
_currentSplash?.confirm();
_currentSplash = null;
if (config.onDoubleTap != null)
config.onDoubleTap();
}
void _handleRadiusChange() {
if (_radius.value == _targetRadius)
renderer._removeSplash(this);
renderer.markNeedsPaint();
void _handleLongPress() {
_currentSplash?.confirm();
_currentSplash = null;
if (config.onLongPress != null)
config.onLongPress();
}
void deactivate() {
if (_splashes != null) {
Set<InkSplash> splashes = _splashes;
_splashes = null;
for (InkSplash splash in splashes)
splash.dispose();
_currentSplash = null;
}
assert(_currentSplash == null);
super.deactivate();
}
void paint(PaintingCanvas canvas) {
int opacity = (_kSplashInitialOpacity * (1.1 - (_radius.value / _targetRadius))).floor();
Paint paint = new Paint()..color = new Color(opacity << 24);
double radius = _pinnedRadius == null ? _radius.value : _pinnedRadius;
canvas.drawCircle(position, radius, paint);
Widget build(BuildContext context) {
final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null;
return new GestureDetector(
onTapDown: enabled ? _handleTapDown : null,
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 {
_RenderInkSplashes({
RenderBox child,
/// An area of a Material that responds to touch.
///
/// Must have an ancestor Material widget in which to cause ink reactions.
class InkWell extends InkResponse {
InkWell({
Key key,
Widget child,
GestureTapCallback onTap,
GestureTapCallback onDoubleTap,
GestureLongPressCallback onLongPress,
this.onHighlightChanged
}) : super(child) {
this.onTap = onTap;
this.onDoubleTap = onDoubleTap;
this.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;
DoubleTapGestureRecognizer _doubleTap;
LongPressGestureRecognizer _longPress;
void _removeSplash(_InkSplash splash) {
_splashes.remove(splash);
if (_lastSplash == splash)
_lastSplash = null;
}
void handleEvent(InputEvent event, BoxHitTestEntry entry) {
if (event.type == 'pointerdown' && (onTap != null || onDoubleTap != null || onLongPress != null)) {
_tap?.addPointer(event);
_doubleTap?.addPointer(event);
_longPress?.addPointer(event);
}
}
void attach() {
super.attach();
_syncTapRecognizer();
_syncDoubleTapRecognizer();
_syncLongPressRecognizer();
}
void detach() {
_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;
}
}
}) : super(
key: key,
child: child,
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress
);
void _disposeTapRecognizer() {
_tap?.dispose();
_tap = null;
}
final ValueChanged<bool> onHighlightChanged;
void _syncDoubleTapRecognizer() {
if (onDoubleTap == null) {
_disposeDoubleTapRecognizer();
} else {
_doubleTap ??= new DoubleTapGestureRecognizer(router: FlutterBinding.instance.pointerRouter)
..onDoubleTap = _handleDoubleTap;
}
}
void _disposeDoubleTapRecognizer() {
_doubleTap?.dispose();
_doubleTap = null;
}
_InkWellState createState() => new _InkWellState();
}
void _syncLongPressRecognizer() {
if (onLongPress == null) {
_disposeLongPressRecognizer();
class _InkWellState extends _InkResponseState<InkWell> {
bool get containedInWell => true;
InkHighlight _lastHighlight;
void updateHighlight(bool value) {
if (value == (_lastHighlight != null && _lastHighlight.active))
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;
}
);
} else {
_lastHighlight.activate();
}
} else {
_longPress ??= new LongPressGestureRecognizer(router: FlutterBinding.instance.pointerRouter)
..onLongPress = _handleLongPress;
_lastHighlight.deactivate();
}
}
void _disposeLongPressRecognizer() {
_longPress?.dispose();
_longPress = null;
if (config.onHighlightChanged != null)
config.onHighlightChanged(value != null);
}
void _handleTapDown(Point position) {
_lastSplash = new _InkSplash(globalToLocal(position), this);
_splashes.add(_lastSplash);
if (onHighlightChanged != null)
onHighlightChanged(true);
super._handleTapDown(position);
updateHighlight(true);
}
void _handleTap() {
_lastSplash?.confirm();
_lastSplash = null;
if (onHighlightChanged != null)
onHighlightChanged(false);
if (onTap != null)
onTap();
super._handleTap();
updateHighlight(false);
}
void _handleTapCancel() {
_lastSplash?.cancel();
_lastSplash = null;
if (onHighlightChanged != null)
onHighlightChanged(false);
}
void _handleDoubleTap() {
_lastSplash?.confirm();
_lastSplash = null;
if (onDoubleTap != null)
onDoubleTap();
super._handleTapCancel();
updateHighlight(false);
}
void _handleLongPress() {
_lastSplash?.confirm();
_lastSplash = null;
if (onLongPress != null)
onLongPress();
void deactivate() {
_lastHighlight?.dispose();
_lastHighlight = null;
super.deactivate();
}
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);
void dependenciesChanged(Type affectedWidgetType) {
if (affectedWidgetType == Theme && _lastHighlight != null)
_lastHighlight.color = Theme.of(context).highlightColor;
}
}
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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:vector_math/vector_math_64.dart';
import 'constants.dart';
import 'shadows.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.card: 2.0,
MaterialType.circle: null,
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({
Key key,
this.child,
......@@ -36,10 +89,21 @@ class Material extends StatelessComponent {
final Color color;
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) {
if (color != null)
return color;
switch (type) {
if (config.color != null)
return config.color;
switch (config.type) {
case MaterialType.canvas:
return Theme.of(context).canvasColor;
case MaterialType.card:
......@@ -50,33 +114,333 @@ class Material extends StatelessComponent {
}
Widget build(BuildContext context) {
Widget contents = child;
if (child != null) {
Color backgroundColor = _getBackgroundColor(context);
Widget contents = config.child;
if (contents != null) {
contents = new DefaultTextStyle(
style: textStyle ?? Theme.of(context).text.body1,
style: config.textStyle ?? Theme.of(context).text.body1,
child: contents
);
if (_kEdges[type] != null) {
contents = new ClipRRect(
xRadius: _kEdges[type],
yRadius: _kEdges[type],
child: contents
);
}
}
return new DefaultTextStyle(
style: Theme.of(context).text.body1,
child: new AnimatedContainer(
curve: Curves.ease,
duration: kThemeChangeDuration,
decoration: new BoxDecoration(
backgroundColor: _getBackgroundColor(context),
borderRadius: _kEdges[type],
boxShadow: elevation == 0 ? null : elevationToShadow[elevation],
shape: type == MaterialType.circle ? Shape.circle : Shape.rectangle
),
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(
xRadius: kMaterialEdges[config.type],
yRadius: kMaterialEdges[config.type],
child: contents
);
}
contents = new AnimatedContainer(
curve: Curves.ease,
duration: kThemeChangeDuration,
decoration: new BoxDecoration(
backgroundColor: backgroundColor,
borderRadius: kMaterialEdges[config.type],
boxShadow: config.elevation == 0 ? null : elevationToShadow[config.elevation],
shape: config.type == MaterialType.circle ? Shape.circle : Shape.rectangle
),
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 {
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