diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index 40093366fd5..d89f3792a1e 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -16,7 +16,9 @@ class InkResponse extends StatefulComponent { this.onTap, this.onDoubleTap, this.onLongPress, - this.onHighlightChanged + this.onHighlightChanged, + this.containedInWell: false, + this.highlightShape: Shape.circle }) : super(key: key); final Widget child; @@ -24,16 +26,44 @@ class InkResponse extends StatefulComponent { final GestureTapCallback onDoubleTap; final GestureLongPressCallback onLongPress; final ValueChanged onHighlightChanged; + final bool containedInWell; + final Shape highlightShape; _InkResponseState createState() => new _InkResponseState(); } class _InkResponseState extends State { - bool get containedInWell => false; - Set _splashes; InkSplash _currentSplash; + 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).highlightAt( + referenceBox: referenceBox, + color: Theme.of(context).highlightColor, + shape: config.highlightShape, + onRemoved: () { + assert(_lastHighlight != null); + _lastHighlight = null; + } + ); + } else { + _lastHighlight.activate(); + } + } else { + _lastHighlight.deactivate(); + } + if (config.onHighlightChanged != null) + config.onHighlightChanged(value != null); + } + void _handleTapDown(Point position) { RenderBox referenceBox = context.findRenderObject(); @@ -42,7 +72,8 @@ class _InkResponseState extends State { splash = Material.of(context).splashAt( referenceBox: referenceBox, position: referenceBox.globalToLocal(position), - containedInWell: containedInWell, + color: Theme.of(context).splashColor, + containedInWell: config.containedInWell, onRemoved: () { if (_splashes != null) { assert(_splashes.contains(splash)); @@ -55,11 +86,13 @@ class _InkResponseState extends State { _splashes ??= new Set(); _splashes.add(splash); _currentSplash = splash; + updateHighlight(true); } void _handleTap() { _currentSplash?.confirm(); _currentSplash = null; + updateHighlight(false); if (config.onTap != null) config.onTap(); } @@ -67,6 +100,7 @@ class _InkResponseState extends State { void _handleTapCancel() { _currentSplash?.cancel(); _currentSplash = null; + updateHighlight(false); } void _handleDoubleTap() { @@ -92,9 +126,16 @@ class _InkResponseState extends State { _currentSplash = null; } assert(_currentSplash == null); + _lastHighlight?.dispose(); + _lastHighlight = null; super.deactivate(); } + void dependenciesChanged(Type affectedWidgetType) { + if (affectedWidgetType == Theme && _lastHighlight != null) + _lastHighlight.color = Theme.of(context).highlightColor; + } + Widget build(BuildContext context) { final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null; return new GestureDetector( @@ -120,75 +161,15 @@ class InkWell extends InkResponse { GestureTapCallback onTap, GestureTapCallback onDoubleTap, GestureLongPressCallback onLongPress, - this.onHighlightChanged + ValueChanged onHighlightChanged }) : super( key: key, child: child, onTap: onTap, onDoubleTap: onDoubleTap, - onLongPress: onLongPress + onLongPress: onLongPress, + onHighlightChanged: onHighlightChanged, + containedInWell: true, + highlightShape: Shape.rectangle ); - - final ValueChanged onHighlightChanged; - - _InkWellState createState() => new _InkWellState(); -} - -class _InkWellState extends _InkResponseState { - - 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 { - _lastHighlight.deactivate(); - } - if (config.onHighlightChanged != null) - config.onHighlightChanged(value != null); - } - - void _handleTapDown(Point position) { - super._handleTapDown(position); - updateHighlight(true); - } - - void _handleTap() { - super._handleTap(); - updateHighlight(false); - } - - void _handleTapCancel() { - super._handleTapCancel(); - updateHighlight(false); - } - - void deactivate() { - _lastHighlight?.dispose(); - _lastHighlight = null; - super.deactivate(); - } - - void dependenciesChanged(Type affectedWidgetType) { - if (affectedWidgetType == Theme && _lastHighlight != null) - _lastHighlight.color = Theme.of(context).highlightColor; - } - } diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index d074820ee88..e1a81a42582 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -58,10 +58,10 @@ abstract class MaterialInkController { /// 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 }); + InkSplash splashAt({ RenderBox referenceBox, Point position, Color color, bool containedInWell, VoidCallback onRemoved }); /// Begin a highlight, coincident with the referenceBox. - InkHighlight highlightRectAt({ RenderBox referenceBox, Color color, VoidCallback onRemoved }); + InkHighlight highlightAt({ RenderBox referenceBox, Color color, Shape shape: Shape.rectangle, VoidCallback onRemoved }); /// Add an arbitrary InkFeature to this InkController. void addInkFeature(InkFeature feature); @@ -156,14 +156,12 @@ class _MaterialState extends State { } } -const Duration _kHighlightFadeDuration = const Duration(milliseconds: 100); +const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200); +const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1); 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 _kSplashConfirmedVelocity = 1.0; // 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); @@ -175,7 +173,13 @@ class RenderInkFeatures extends RenderProxyBox implements MaterialInkController final List _inkFeatures = []; - InkSplash splashAt({ RenderBox referenceBox, Point position, bool containedInWell, VoidCallback onRemoved }) { + InkSplash splashAt({ + RenderBox referenceBox, + Point position, + Color color, + bool containedInWell, + VoidCallback onRemoved + }) { double radius; if (containedInWell) { radius = _getSplashTargetSize(referenceBox.size, position); @@ -186,8 +190,10 @@ class RenderInkFeatures extends RenderProxyBox implements MaterialInkController renderer: this, referenceBox: referenceBox, position: position, + color: color, targetRadius: radius, clipToReferenceBox: containedInWell, + repositionToReferenceBox: !containedInWell, onRemoved: onRemoved ); addInkFeature(splash); @@ -202,11 +208,17 @@ class RenderInkFeatures extends RenderProxyBox implements MaterialInkController return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble(); } - InkHighlight highlightRectAt({ RenderBox referenceBox, Color color, VoidCallback onRemoved }) { + InkHighlight highlightAt({ + RenderBox referenceBox, + Color color, + Shape shape: Shape.rectangle, + VoidCallback onRemoved + }) { _InkHighlight highlight = new _InkHighlight( renderer: this, referenceBox: referenceBox, color: color, + shape: shape, onRemoved: onRemoved ); addInkFeature(highlight); @@ -303,45 +315,45 @@ class _InkSplash extends InkFeature implements InkSplash { RenderInkFeatures renderer, RenderBox referenceBox, this.position, + this.color, this.targetRadius, this.clipToReferenceBox, + this.repositionToReferenceBox, VoidCallback onRemoved }) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) { _radius = new ValuePerformance( - variable: new AnimatedValue( - _kSplashInitialSize, - end: targetRadius, - curve: Curves.easeOut - ), - duration: new Duration(milliseconds: (targetRadius / _kSplashUnconfirmedVelocity).floor()) - )..addListener(_handleRadiusChange) + variable: new AnimatedValue(_kSplashInitialSize, end: targetRadius), + duration: _kUnconfirmedSplashDuration + )..addListener(renderer.markNeedsPaint) ..play(); + _alpha = new ValuePerformance( + variable: new AnimatedIntValue(color.alpha, end: 0), + duration: _kHighlightFadeDuration + )..addListener(_handleAlphaChange); } final Point position; + final Color color; final double targetRadius; final bool clipToReferenceBox; + final bool repositionToReferenceBox; - double _pinnedRadius; ValuePerformance _radius; + ValuePerformance _alpha; void confirm() { - _updateVelocity(_kSplashConfirmedVelocity); + int duration = (targetRadius / _kSplashConfirmedVelocity).floor(); + _radius.duration = new Duration(milliseconds: duration); + _radius.play(); + _alpha.play(); } void cancel() { - _updateVelocity(_kSplashCanceledVelocity); - _pinnedRadius = _radius.value; + _alpha.play(); } - void _updateVelocity(double velocity) { - int duration = (targetRadius / velocity).floor(); - _radius.duration = new Duration(milliseconds: duration); - _radius.play(); - } - - void _handleRadiusChange() { - if (_radius.value == targetRadius) + void _handleAlphaChange() { + if (_alpha.value == _alpha.variable.end) dispose(); else renderer.markNeedsPaint(); @@ -349,27 +361,31 @@ class _InkSplash extends InkFeature implements InkSplash { void dispose() { _radius.stop(); + _alpha.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; + Paint paint = new Paint()..color = color.withAlpha(_alpha.value); + Point center = position; 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); + if (repositionToReferenceBox) + center = Point.lerp(center, Point.origin, _radius.progress); + canvas.drawCircle(center, _radius.value, paint); canvas.restore(); } else { if (clipToReferenceBox) { canvas.save(); canvas.clipRect(originOffset.toPoint() & referenceBox.size); } - canvas.drawCircle(position + originOffset, radius, paint); + if (repositionToReferenceBox) + center = Point.lerp(center, referenceBox.size.center(Point.origin), _radius.progress); + canvas.drawCircle(center + originOffset, _radius.value, paint); if (clipToReferenceBox) canvas.restore(); } @@ -381,15 +397,12 @@ class _InkHighlight extends InkFeature implements InkHighlight { RenderInkFeatures renderer, RenderBox referenceBox, Color color, + this.shape, VoidCallback onRemoved }) : _color = color, super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) { _alpha = new ValuePerformance( - variable: new AnimatedIntValue( - 0, - end: color.alpha, - curve: Curves.linear - ), + variable: new AnimatedIntValue(0, end: color.alpha), duration: _kHighlightFadeDuration )..addListener(_handleAlphaChange) ..play(); @@ -404,6 +417,8 @@ class _InkHighlight extends InkFeature implements InkHighlight { renderer.markNeedsPaint(); } + final Shape shape; + bool get active => _active; bool _active = true; ValuePerformance _alpha; @@ -430,16 +445,23 @@ class _InkHighlight extends InkFeature implements InkHighlight { super.dispose(); } + void _paintHighlight(Canvas canvas, Rect rect, paint) { + if (shape == Shape.rectangle) + canvas.drawRect(rect, paint); + else + canvas.drawCircle(rect.center, _kDefaultSplashRadius, paint); + } + 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); + _paintHighlight(canvas, Point.origin & referenceBox.size, paint); canvas.restore(); } else { - canvas.drawRect(originOffset.toPoint() & referenceBox.size, paint); + _paintHighlight(canvas, originOffset.toPoint() & referenceBox.size, paint); } } diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 5f2100e9b9b..5ee6a07063b 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -10,6 +10,23 @@ import 'typography.dart'; enum ThemeBrightness { dark, light } +// Deriving these values is black magic. The spec claims that pressed buttons +// have a highlight of 0x66999999, but that's clearly wrong. The videos in the +// spec show that buttons have a composited highlight of #E1E1E1 on a background +// of #FAFAFA. Assuming that the highlight really has an opacity of 0x66, we can +// solve for the actual color of the highlight: +const Color _kLightThemeHighlightColor = const Color(0x66BCBCBC); + +// The same video shows the splash compositing to #D7D7D7 on a background of +// #E1E1E1. Again, assuming the splash has an opacity of 0x66, we can solve for +// the actual color of the splash: +const Color _kLightThemeSplashColor = const Color(0x66C8C8C8); + +// Unfortunately, a similar video isn't available for the dark theme, which +// means we assume the values in the spec are actually correct. +const Color _kDarkThemeHighlightColor = const Color(0x40CCCCCC); +const Color _kDarkThemeSplashColor = const Color(0x40CCCCCC); + class ThemeData { ThemeData({ @@ -27,7 +44,8 @@ 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, - highlightColor = brightness == ThemeBrightness.dark ? const Color(0x42FFFFFF) : const Color(0x1F000000), + highlightColor = brightness == ThemeBrightness.dark ? _kDarkThemeHighlightColor : _kLightThemeHighlightColor, + splashColor = brightness == ThemeBrightness.dark ? _kDarkThemeSplashColor : _kLightThemeSplashColor, text = brightness == ThemeBrightness.dark ? Typography.white : Typography.black { assert(brightness != null); @@ -70,6 +88,7 @@ class ThemeData { final Color dividerColor; final Color hintColor; final Color highlightColor; + final Color splashColor; final double hintOpacity; /// Text with a color that contrasts with the card and canvas colors.