Commit a2b8f8b9 authored by Adam Barth's avatar Adam Barth

Merge pull request #721 from abarth/ink_response

Improve material ink response
parents dc907830 0608a02a
...@@ -16,7 +16,9 @@ class InkResponse extends StatefulComponent { ...@@ -16,7 +16,9 @@ class InkResponse extends StatefulComponent {
this.onTap, this.onTap,
this.onDoubleTap, this.onDoubleTap,
this.onLongPress, this.onLongPress,
this.onHighlightChanged this.onHighlightChanged,
this.containedInWell: false,
this.highlightShape: Shape.circle
}) : super(key: key); }) : super(key: key);
final Widget child; final Widget child;
...@@ -24,16 +26,44 @@ class InkResponse extends StatefulComponent { ...@@ -24,16 +26,44 @@ class InkResponse extends StatefulComponent {
final GestureTapCallback onDoubleTap; final GestureTapCallback onDoubleTap;
final GestureLongPressCallback onLongPress; final GestureLongPressCallback onLongPress;
final ValueChanged<bool> onHighlightChanged; final ValueChanged<bool> onHighlightChanged;
final bool containedInWell;
final Shape highlightShape;
_InkResponseState createState() => new _InkResponseState<InkResponse>(); _InkResponseState createState() => new _InkResponseState<InkResponse>();
} }
class _InkResponseState<T extends InkResponse> extends State<T> { class _InkResponseState<T extends InkResponse> extends State<T> {
bool get containedInWell => false;
Set<InkSplash> _splashes; Set<InkSplash> _splashes;
InkSplash _currentSplash; 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) { void _handleTapDown(Point position) {
RenderBox referenceBox = context.findRenderObject(); RenderBox referenceBox = context.findRenderObject();
...@@ -42,7 +72,8 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -42,7 +72,8 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
splash = Material.of(context).splashAt( splash = Material.of(context).splashAt(
referenceBox: referenceBox, referenceBox: referenceBox,
position: referenceBox.globalToLocal(position), position: referenceBox.globalToLocal(position),
containedInWell: containedInWell, color: Theme.of(context).splashColor,
containedInWell: config.containedInWell,
onRemoved: () { onRemoved: () {
if (_splashes != null) { if (_splashes != null) {
assert(_splashes.contains(splash)); assert(_splashes.contains(splash));
...@@ -55,11 +86,13 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -55,11 +86,13 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
_splashes ??= new Set<InkSplash>(); _splashes ??= new Set<InkSplash>();
_splashes.add(splash); _splashes.add(splash);
_currentSplash = splash; _currentSplash = splash;
updateHighlight(true);
} }
void _handleTap() { void _handleTap() {
_currentSplash?.confirm(); _currentSplash?.confirm();
_currentSplash = null; _currentSplash = null;
updateHighlight(false);
if (config.onTap != null) if (config.onTap != null)
config.onTap(); config.onTap();
} }
...@@ -67,6 +100,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -67,6 +100,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
void _handleTapCancel() { void _handleTapCancel() {
_currentSplash?.cancel(); _currentSplash?.cancel();
_currentSplash = null; _currentSplash = null;
updateHighlight(false);
} }
void _handleDoubleTap() { void _handleDoubleTap() {
...@@ -92,9 +126,16 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -92,9 +126,16 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
_currentSplash = null; _currentSplash = null;
} }
assert(_currentSplash == null); assert(_currentSplash == null);
_lastHighlight?.dispose();
_lastHighlight = null;
super.deactivate(); super.deactivate();
} }
void dependenciesChanged(Type affectedWidgetType) {
if (affectedWidgetType == Theme && _lastHighlight != null)
_lastHighlight.color = Theme.of(context).highlightColor;
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null; final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null;
return new GestureDetector( return new GestureDetector(
...@@ -120,75 +161,15 @@ class InkWell extends InkResponse { ...@@ -120,75 +161,15 @@ class InkWell extends InkResponse {
GestureTapCallback onTap, GestureTapCallback onTap,
GestureTapCallback onDoubleTap, GestureTapCallback onDoubleTap,
GestureLongPressCallback onLongPress, GestureLongPressCallback onLongPress,
this.onHighlightChanged ValueChanged<bool> onHighlightChanged
}) : super( }) : super(
key: key, key: key,
child: child, child: child,
onTap: onTap, onTap: onTap,
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
onLongPress: onLongPress onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
containedInWell: true,
highlightShape: Shape.rectangle
); );
final ValueChanged<bool> onHighlightChanged;
_InkWellState createState() => new _InkWellState();
}
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 {
_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;
}
} }
...@@ -58,10 +58,10 @@ abstract class MaterialInkController { ...@@ -58,10 +58,10 @@ abstract class MaterialInkController {
/// If containedInWell is true, then the splash will be sized to fit /// If containedInWell is true, then the splash will be sized to fit
/// the referenceBox, then clipped to it when drawn. /// the referenceBox, then clipped to it when drawn.
/// When the splash is removed, onRemoved will be invoked. /// 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. /// 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. /// Add an arbitrary InkFeature to this InkController.
void addInkFeature(InkFeature feature); void addInkFeature(InkFeature feature);
...@@ -156,14 +156,12 @@ class _MaterialState extends State<Material> { ...@@ -156,14 +156,12 @@ class _MaterialState extends State<Material> {
} }
} }
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 double _kDefaultSplashRadius = 35.0; // logical pixels
const int _kSplashInitialAlpha = 0x30; // 0..255 const double _kSplashConfirmedVelocity = 1.0; // logical pixels per millisecond
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 _kSplashInitialSize = 0.0; // logical pixels
const double _kSplashUnconfirmedVelocity = 0.2; // logical pixels per millisecond
class RenderInkFeatures extends RenderProxyBox implements MaterialInkController { class RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
RenderInkFeatures({ RenderBox child, this.color }) : super(child); RenderInkFeatures({ RenderBox child, this.color }) : super(child);
...@@ -175,7 +173,13 @@ class RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -175,7 +173,13 @@ class RenderInkFeatures extends RenderProxyBox implements MaterialInkController
final List<InkFeature> _inkFeatures = <InkFeature>[]; final List<InkFeature> _inkFeatures = <InkFeature>[];
InkSplash splashAt({ RenderBox referenceBox, Point position, bool containedInWell, VoidCallback onRemoved }) { InkSplash splashAt({
RenderBox referenceBox,
Point position,
Color color,
bool containedInWell,
VoidCallback onRemoved
}) {
double radius; double radius;
if (containedInWell) { if (containedInWell) {
radius = _getSplashTargetSize(referenceBox.size, position); radius = _getSplashTargetSize(referenceBox.size, position);
...@@ -186,8 +190,10 @@ class RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -186,8 +190,10 @@ class RenderInkFeatures extends RenderProxyBox implements MaterialInkController
renderer: this, renderer: this,
referenceBox: referenceBox, referenceBox: referenceBox,
position: position, position: position,
color: color,
targetRadius: radius, targetRadius: radius,
clipToReferenceBox: containedInWell, clipToReferenceBox: containedInWell,
repositionToReferenceBox: !containedInWell,
onRemoved: onRemoved onRemoved: onRemoved
); );
addInkFeature(splash); addInkFeature(splash);
...@@ -202,11 +208,17 @@ class RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -202,11 +208,17 @@ class RenderInkFeatures extends RenderProxyBox implements MaterialInkController
return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble(); 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( _InkHighlight highlight = new _InkHighlight(
renderer: this, renderer: this,
referenceBox: referenceBox, referenceBox: referenceBox,
color: color, color: color,
shape: shape,
onRemoved: onRemoved onRemoved: onRemoved
); );
addInkFeature(highlight); addInkFeature(highlight);
...@@ -303,45 +315,45 @@ class _InkSplash extends InkFeature implements InkSplash { ...@@ -303,45 +315,45 @@ class _InkSplash extends InkFeature implements InkSplash {
RenderInkFeatures renderer, RenderInkFeatures renderer,
RenderBox referenceBox, RenderBox referenceBox,
this.position, this.position,
this.color,
this.targetRadius, this.targetRadius,
this.clipToReferenceBox, this.clipToReferenceBox,
this.repositionToReferenceBox,
VoidCallback onRemoved VoidCallback onRemoved
}) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) { }) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
_radius = new ValuePerformance<double>( _radius = new ValuePerformance<double>(
variable: new AnimatedValue<double>( variable: new AnimatedValue<double>(_kSplashInitialSize, end: targetRadius),
_kSplashInitialSize, duration: _kUnconfirmedSplashDuration
end: targetRadius, )..addListener(renderer.markNeedsPaint)
curve: Curves.easeOut
),
duration: new Duration(milliseconds: (targetRadius / _kSplashUnconfirmedVelocity).floor())
)..addListener(_handleRadiusChange)
..play(); ..play();
_alpha = new ValuePerformance<int>(
variable: new AnimatedIntValue(color.alpha, end: 0),
duration: _kHighlightFadeDuration
)..addListener(_handleAlphaChange);
} }
final Point position; final Point position;
final Color color;
final double targetRadius; final double targetRadius;
final bool clipToReferenceBox; final bool clipToReferenceBox;
final bool repositionToReferenceBox;
double _pinnedRadius;
ValuePerformance<double> _radius; ValuePerformance<double> _radius;
ValuePerformance<int> _alpha;
void confirm() { void confirm() {
_updateVelocity(_kSplashConfirmedVelocity); int duration = (targetRadius / _kSplashConfirmedVelocity).floor();
_radius.duration = new Duration(milliseconds: duration);
_radius.play();
_alpha.play();
} }
void cancel() { void cancel() {
_updateVelocity(_kSplashCanceledVelocity); _alpha.play();
_pinnedRadius = _radius.value;
} }
void _updateVelocity(double velocity) { void _handleAlphaChange() {
int duration = (targetRadius / velocity).floor(); if (_alpha.value == _alpha.variable.end)
_radius.duration = new Duration(milliseconds: duration);
_radius.play();
}
void _handleRadiusChange() {
if (_radius.value == targetRadius)
dispose(); dispose();
else else
renderer.markNeedsPaint(); renderer.markNeedsPaint();
...@@ -349,27 +361,31 @@ class _InkSplash extends InkFeature implements InkSplash { ...@@ -349,27 +361,31 @@ class _InkSplash extends InkFeature implements InkSplash {
void dispose() { void dispose() {
_radius.stop(); _radius.stop();
_alpha.stop();
super.dispose(); super.dispose();
} }
void paintFeature(Canvas canvas, Matrix4 transform) { void paintFeature(Canvas canvas, Matrix4 transform) {
int alpha = (_kSplashInitialAlpha * (1.1 - (_radius.value / targetRadius))).floor(); Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Paint paint = new Paint()..color = new Color(alpha << 24); // TODO(ianh): in dark theme, this isn't very visible Point center = position;
double radius = _pinnedRadius == null ? _radius.value : _pinnedRadius;
Offset originOffset = MatrixUtils.getAsTranslation(transform); Offset originOffset = MatrixUtils.getAsTranslation(transform);
if (originOffset == null) { if (originOffset == null) {
canvas.save(); canvas.save();
canvas.concat(transform.storage); canvas.concat(transform.storage);
if (clipToReferenceBox) if (clipToReferenceBox)
canvas.clipRect(Point.origin & referenceBox.size); 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(); canvas.restore();
} else { } else {
if (clipToReferenceBox) { if (clipToReferenceBox) {
canvas.save(); canvas.save();
canvas.clipRect(originOffset.toPoint() & referenceBox.size); 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) if (clipToReferenceBox)
canvas.restore(); canvas.restore();
} }
...@@ -381,15 +397,12 @@ class _InkHighlight extends InkFeature implements InkHighlight { ...@@ -381,15 +397,12 @@ class _InkHighlight extends InkFeature implements InkHighlight {
RenderInkFeatures renderer, RenderInkFeatures renderer,
RenderBox referenceBox, RenderBox referenceBox,
Color color, Color color,
this.shape,
VoidCallback onRemoved VoidCallback onRemoved
}) : _color = color, }) : _color = color,
super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) { super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
_alpha = new ValuePerformance<int>( _alpha = new ValuePerformance<int>(
variable: new AnimatedIntValue( variable: new AnimatedIntValue(0, end: color.alpha),
0,
end: color.alpha,
curve: Curves.linear
),
duration: _kHighlightFadeDuration duration: _kHighlightFadeDuration
)..addListener(_handleAlphaChange) )..addListener(_handleAlphaChange)
..play(); ..play();
...@@ -404,6 +417,8 @@ class _InkHighlight extends InkFeature implements InkHighlight { ...@@ -404,6 +417,8 @@ class _InkHighlight extends InkFeature implements InkHighlight {
renderer.markNeedsPaint(); renderer.markNeedsPaint();
} }
final Shape shape;
bool get active => _active; bool get active => _active;
bool _active = true; bool _active = true;
ValuePerformance<int> _alpha; ValuePerformance<int> _alpha;
...@@ -430,16 +445,23 @@ class _InkHighlight extends InkFeature implements InkHighlight { ...@@ -430,16 +445,23 @@ class _InkHighlight extends InkFeature implements InkHighlight {
super.dispose(); 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) { void paintFeature(Canvas canvas, Matrix4 transform) {
Paint paint = new Paint()..color = color.withAlpha(_alpha.value); Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Offset originOffset = MatrixUtils.getAsTranslation(transform); Offset originOffset = MatrixUtils.getAsTranslation(transform);
if (originOffset == null) { if (originOffset == null) {
canvas.save(); canvas.save();
canvas.concat(transform.storage); canvas.concat(transform.storage);
canvas.drawRect(Point.origin & referenceBox.size, paint); _paintHighlight(canvas, Point.origin & referenceBox.size, paint);
canvas.restore(); canvas.restore();
} else { } else {
canvas.drawRect(originOffset.toPoint() & referenceBox.size, paint); _paintHighlight(canvas, originOffset.toPoint() & referenceBox.size, paint);
} }
} }
......
...@@ -10,6 +10,23 @@ import 'typography.dart'; ...@@ -10,6 +10,23 @@ import 'typography.dart';
enum ThemeBrightness { dark, light } 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 { class ThemeData {
ThemeData({ ThemeData({
...@@ -27,7 +44,8 @@ class ThemeData { ...@@ -27,7 +44,8 @@ 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,
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 { text = brightness == ThemeBrightness.dark ? Typography.white : Typography.black {
assert(brightness != null); assert(brightness != null);
...@@ -70,6 +88,7 @@ class ThemeData { ...@@ -70,6 +88,7 @@ class ThemeData {
final Color dividerColor; final Color dividerColor;
final Color hintColor; final Color hintColor;
final Color highlightColor; final Color highlightColor;
final Color splashColor;
final double hintOpacity; final double hintOpacity;
/// Text with a color that contrasts with the card and canvas colors. /// Text with a color that contrasts with the card and canvas colors.
......
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