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 {
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<bool> onHighlightChanged;
final bool containedInWell;
final Shape highlightShape;
_InkResponseState createState() => new _InkResponseState<InkResponse>();
}
class _InkResponseState<T extends InkResponse> extends State<T> {
bool get containedInWell => false;
Set<InkSplash> _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<T extends InkResponse> extends State<T> {
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<T extends InkResponse> extends State<T> {
_splashes ??= new Set<InkSplash>();
_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<T extends InkResponse> extends State<T> {
void _handleTapCancel() {
_currentSplash?.cancel();
_currentSplash = null;
updateHighlight(false);
}
void _handleDoubleTap() {
......@@ -92,9 +126,16 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
_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<bool> onHighlightChanged
}) : super(
key: key,
child: child,
onTap: onTap,
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 {
/// 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<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 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<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;
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<double>(
variable: new AnimatedValue<double>(
_kSplashInitialSize,
end: targetRadius,
curve: Curves.easeOut
),
duration: new Duration(milliseconds: (targetRadius / _kSplashUnconfirmedVelocity).floor())
)..addListener(_handleRadiusChange)
variable: new AnimatedValue<double>(_kSplashInitialSize, end: targetRadius),
duration: _kUnconfirmedSplashDuration
)..addListener(renderer.markNeedsPaint)
..play();
_alpha = new ValuePerformance<int>(
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<double> _radius;
ValuePerformance<int> _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<int>(
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<int> _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);
}
}
......
......@@ -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.
......
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