diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index adc97ffac29..51ce067307e 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -49,6 +49,8 @@ export 'src/material/icon_theme.dart'; export 'src/material/icon_theme_data.dart'; export 'src/material/icons.dart'; export 'src/material/image_icon.dart'; +export 'src/material/ink_highlight.dart'; +export 'src/material/ink_splash.dart'; export 'src/material/ink_well.dart'; export 'src/material/input.dart'; export 'src/material/list.dart'; diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index f38d70dc6fd..fb4a71bef61 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -136,7 +136,7 @@ class IconButton extends StatelessWidget { maxHeight: size, child: new ConstrainedBox( constraints: new BoxConstraints.loose( - new Size.square(math.max(size, InkSplash.defaultRadius * 2.0)) + new Size.square(math.max(size, Material.defaultSplashRadius * 2.0)) ), child: new Align( alignment: alignment, @@ -161,7 +161,7 @@ class IconButton extends StatelessWidget { return new InkResponse( onTap: onPressed, child: result, - radius: math.max(size, InkSplash.defaultRadius), + radius: math.max(size, Material.defaultSplashRadius), ); } diff --git a/packages/flutter/lib/src/material/ink_highlight.dart b/packages/flutter/lib/src/material/ink_highlight.dart new file mode 100644 index 00000000000..c89b8c3a1cf --- /dev/null +++ b/packages/flutter/lib/src/material/ink_highlight.dart @@ -0,0 +1,132 @@ +// Copyright 2017 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 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'material.dart'; + +const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200); + +/// A visual emphasis on a part of a [Material] receiving user interaction. +/// +/// This object is rarely created directly. Instead of creating an ink highlight +/// directly, consider using an [InkResponse] or [InkWell] widget, which uses +/// gestures (such as tap and long-press) to trigger ink highlights. +/// +/// See also: +/// +/// * [InkResponse], which uses gestures to trigger ink highlights and ink +/// splashes in the parent [Material]. +/// * [InkWell], which is a rectangular [InkResponse] (the most common type of +/// ink response). +/// * [Material], which is the widget on which the ink highlight is painted. +/// * [InkSplash], which is an ink feature that shows a reaction to user input +/// on a [Material]. +class InkHighlight extends InkFeature { + /// Begin a highlight animation. + /// + /// The [controller] argument is typically obtained via + /// `Material.of(context)`. + /// + /// If a `rectCallback` is given, then it provides the highlight rectangle, + /// otherwise, the highlight rectangle is coincident with the [referenceBox]. + /// + /// When the highlight is removed, `onRemoved` will be called. + InkHighlight({ + @required MaterialInkController controller, + @required RenderBox referenceBox, + @required Color color, + BoxShape shape: BoxShape.rectangle, + RectCallback rectCallback, + VoidCallback onRemoved, + }) : _color = color, + _shape = shape, + _rectCallback = rectCallback, + super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { + assert(color != null); + assert(shape != null); + _alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: controller.vsync) + ..addListener(controller.markNeedsPaint) + ..addStatusListener(_handleAlphaStatusChanged) + ..forward(); + _alpha = new IntTween( + begin: 0, + end: color.alpha + ).animate(_alphaController); + + controller.addInkFeature(this); + } + + final BoxShape _shape; + final RectCallback _rectCallback; + + Animation _alpha; + AnimationController _alphaController; + + /// The color of the ink used to emphasize part of the material. + Color get color => _color; + Color _color; + set color(Color value) { + if (value == _color) + return; + _color = value; + controller.markNeedsPaint(); + } + + /// Whether this part of the material is being visually emphasized. + bool get active => _active; + bool _active = true; + + /// Start visually emphasizing this part of the material. + void activate() { + _active = true; + _alphaController.forward(); + } + + /// Stop visually emphasizing this part of the material. + void deactivate() { + _active = false; + _alphaController.reverse(); + } + + void _handleAlphaStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.dismissed && !_active) + dispose(); + } + + @override + void dispose() { + _alphaController.dispose(); + super.dispose(); + } + + void _paintHighlight(Canvas canvas, Rect rect, Paint paint) { + assert(_shape != null); + switch (_shape) { + case BoxShape.circle: + canvas.drawCircle(rect.center, Material.defaultSplashRadius, paint); + break; + case BoxShape.rectangle: + canvas.drawRect(rect, paint); + break; + } + } + + @override + void paintFeature(Canvas canvas, Matrix4 transform) { + final Paint paint = new Paint()..color = color.withAlpha(_alpha.value); + final Offset originOffset = MatrixUtils.getAsTranslation(transform); + final Rect rect = (_rectCallback != null ? _rectCallback() : Point.origin & referenceBox.size); + if (originOffset == null) { + canvas.save(); + canvas.transform(transform.storage); + _paintHighlight(canvas, rect, paint); + canvas.restore(); + } else { + _paintHighlight(canvas, rect.shift(originOffset), paint); + } + } +} diff --git a/packages/flutter/lib/src/material/ink_splash.dart b/packages/flutter/lib/src/material/ink_splash.dart new file mode 100644 index 00000000000..c40753ae0b0 --- /dev/null +++ b/packages/flutter/lib/src/material/ink_splash.dart @@ -0,0 +1,175 @@ +// Copyright 2017 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 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'material.dart'; + +const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1); +const Duration _kSplashFadeDuration = const Duration(milliseconds: 200); + +const double _kSplashInitialSize = 0.0; // logical pixels +const double _kSplashConfirmedVelocity = 1.0; // logical pixels per millisecond + +RectCallback _getClipCallback(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback) { + if (rectCallback != null) { + assert(containedInkWell); + return rectCallback; + } + if (containedInkWell) + return () => Point.origin & referenceBox.size; + return null; +} + +double _getTargetRadius(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback, Point position) { + if (containedInkWell) { + final Size size = rectCallback != null ? rectCallback().size : referenceBox.size; + return _getSplashRadiusForPoistionInSize(size, position); + } + return Material.defaultSplashRadius; +} + +double _getSplashRadiusForPoistionInSize(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(); +} + +/// A visual reaction on a piece of [Material] to user input. +/// +/// This object is rarely created directly. Instead of creating an ink splash +/// directly, consider using an [InkResponse] or [InkWell] widget, which uses +/// gestures (such as tap and long-press) to trigger ink splashes. +/// +/// See also: +/// +/// * [InkResponse], which uses gestures to trigger ink highlights and ink +/// splashes in the parent [Material]. +/// * [InkWell], which is a rectangular [InkResponse] (the most common type of +/// ink response). +/// * [Material], which is the widget on which the ink splash is painted. +/// * [InkHighlight], which is an ink feature that emphasizes a part of a +/// [Material]. +class InkSplash extends InkFeature { + /// Begin a splash, centered at position relative to [referenceBox]. + /// + /// The [controller] argument is typically obtained via + /// `Material.of(context)`. + /// + /// If `containedInkWell` is true, then the splash will be sized to fit + /// the well rectangle, then clipped to it when drawn. The well + /// rectangle is the box returned by `rectCallback`, if provided, or + /// otherwise is the bounds of the [referenceBox]. + /// + /// If `containedInkWell` is false, then `rectCallback` should be null. + /// The ink splash is clipped only to the edges of the [Material]. + /// This is the default. + /// + /// When the splash is removed, `onRemoved` will be called. + InkSplash({ + @required MaterialInkController controller, + @required RenderBox referenceBox, + Point position, + Color color, + bool containedInkWell: false, + RectCallback rectCallback, + double radius, + VoidCallback onRemoved, + }) : _position = position, + _color = color, + _targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position), + _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback), + _repositionToReferenceBox = !containedInkWell, + super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { + _radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration, vsync: controller.vsync) + ..addListener(controller.markNeedsPaint) + ..forward(); + _radius = new Tween( + begin: _kSplashInitialSize, + end: _targetRadius + ).animate(_radiusController); + _alphaController = new AnimationController(duration: _kSplashFadeDuration, vsync: controller.vsync) + ..addListener(controller.markNeedsPaint) + ..addStatusListener(_handleAlphaStatusChanged); + _alpha = new IntTween( + begin: color.alpha, + end: 0 + ).animate(_alphaController); + + controller.addInkFeature(this); + } + + final Point _position; + final Color _color; + final double _targetRadius; + final RectCallback _clipCallback; + final bool _repositionToReferenceBox; + + Animation _radius; + AnimationController _radiusController; + + Animation _alpha; + AnimationController _alphaController; + + /// The user input is confirmed. + /// + /// Causes the reaction to propagate faster across the material. + void confirm() { + final int duration = (_targetRadius / _kSplashConfirmedVelocity).floor(); + _radiusController + ..duration = new Duration(milliseconds: duration) + ..forward(); + _alphaController.forward(); + } + + /// The user input was canceled. + /// + /// Causes the reaction to gradually disappear. + void cancel() { + _alphaController.forward(); + } + + void _handleAlphaStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.completed) + dispose(); + } + + @override + void dispose() { + _radiusController.dispose(); + _alphaController.dispose(); + super.dispose(); + } + + @override + void paintFeature(Canvas canvas, Matrix4 transform) { + final Paint paint = new Paint()..color = _color.withAlpha(_alpha.value); + Point center = _position; + if (_repositionToReferenceBox) + center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value); + final Offset originOffset = MatrixUtils.getAsTranslation(transform); + if (originOffset == null) { + canvas.save(); + canvas.transform(transform.storage); + if (_clipCallback != null) + canvas.clipRect(_clipCallback()); + canvas.drawCircle(center, _radius.value, paint); + canvas.restore(); + } else { + if (_clipCallback != null) { + canvas.save(); + canvas.clipRect(_clipCallback().shift(originOffset)); + } + canvas.drawCircle(center + originOffset, _radius.value, paint); + if (_clipCallback != null) + canvas.restore(); + } + } +} diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index 668a313c44f..eda3d5b5910 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -7,8 +7,11 @@ import 'dart:collection'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; import 'debug.dart'; +import 'ink_highlight.dart'; +import 'ink_splash.dart'; import 'material.dart'; import 'theme.dart'; @@ -88,6 +91,7 @@ class InkResponse extends StatefulWidget { /// specialize [InkResponse] for unusual cases. For example, /// [TableRowInkWell] implements this method to verify that the widget is /// in a table. + @mustCallSuper bool debugCheckContext(BuildContext context) { assert(debugCheckHasMaterial(context)); return true; @@ -108,9 +112,9 @@ class _InkResponseState extends State { return; if (value) { if (_lastHighlight == null) { - RenderBox referenceBox = context.findRenderObject(); - assert(Material.of(context) != null); - _lastHighlight = Material.of(context).highlightAt( + final RenderBox referenceBox = context.findRenderObject(); + _lastHighlight = new InkHighlight( + controller: Material.of(context), referenceBox: referenceBox, color: Theme.of(context).highlightColor, shape: config.highlightShape, @@ -118,7 +122,7 @@ class _InkResponseState extends State { onRemoved: () { assert(_lastHighlight != null); _lastHighlight = null; - } + }, ); } else { _lastHighlight.activate(); @@ -132,11 +136,11 @@ class _InkResponseState extends State { } void _handleTapDown(TapDownDetails details) { - RenderBox referenceBox = context.findRenderObject(); - assert(Material.of(context) != null); + final RenderBox referenceBox = context.findRenderObject(); + final RectCallback rectCallback = config.getRectCallback(referenceBox); InkSplash splash; - RectCallback rectCallback = config.getRectCallback(referenceBox); - splash = Material.of(context).splashAt( + splash = new InkSplash( + controller: Material.of(context), referenceBox: referenceBox, position: referenceBox.globalToLocal(details.globalPosition), color: Theme.of(context).splashColor, @@ -189,7 +193,7 @@ class _InkResponseState extends State { @override void deactivate() { if (_splashes != null) { - Set splashes = _splashes; + final Set splashes = _splashes; _splashes = null; for (InkSplash splash in splashes) splash.dispose(); @@ -219,7 +223,7 @@ class _InkResponseState extends State { } -/// A rectangular area of a Material that responds to touch. +/// A rectangular area of a [Material] that responds to touch. /// /// Must have an ancestor [Material] widget in which to cause ink reactions. /// diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index ef72d60017d..680b7176359 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -2,8 +2,6 @@ // 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/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -14,7 +12,7 @@ import 'theme.dart'; /// Signature for the callback used by ink effects to obtain the rectangle for the effect. /// -/// Used by [MaterialInkController.splashAt], for example. +/// Used by [InkHighlight] and [InkSplash], for example. typedef Rect RectCallback(); /// The various kinds of material in material design. Used to @@ -55,48 +53,6 @@ final Map kMaterialEdges = with TickerProviderStateMixin { @@ -321,10 +258,6 @@ class _MaterialState extends State with TickerProviderStateMixin { } const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200); -const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1); - -const double _kSplashConfirmedVelocity = 1.0; // logical pixels per millisecond -const double _kSplashInitialSize = 0.0; // logical pixels class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { _RenderInkFeatures({ RenderBox child, @required this.vsync, this.color }) : super(child) { @@ -334,6 +267,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController // This class should exist in a 1:1 relationship with a MaterialState object, // since there's no current support for dynamically changing the ticker // provider. + @override final TickerProvider vsync; // This is here to satisfy the MaterialInkController contract. @@ -344,75 +278,6 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController final List _inkFeatures = []; - @override - InkSplash splashAt({ - RenderBox referenceBox, - Point position, - Color color, - bool containedInkWell: false, - RectCallback rectCallback, - VoidCallback onRemoved, - double radius, - }) { - RectCallback clipCallback; - if (containedInkWell) { - Size size; - if (rectCallback != null) { - size = rectCallback().size; - clipCallback = rectCallback; - } else { - size = referenceBox.size; - clipCallback = () => Point.origin & referenceBox.size; - } - radius ??= _getSplashTargetSize(size, position); - } else { - assert(rectCallback == null); - radius ??= InkSplash.defaultRadius; - } - _InkSplash splash = new _InkSplash( - controller: this, - referenceBox: referenceBox, - position: position, - color: color, - targetRadius: radius, - clipCallback: clipCallback, - repositionToReferenceBox: !containedInkWell, - onRemoved: onRemoved, - vsync: vsync, - ); - 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(); - } - - @override - InkHighlight highlightAt({ - RenderBox referenceBox, - Color color, - BoxShape shape: BoxShape.rectangle, - RectCallback rectCallback, - VoidCallback onRemoved - }) { - _InkHighlight highlight = new _InkHighlight( - controller: this, - referenceBox: referenceBox, - color: color, - shape: shape, - rectCallback: rectCallback, - onRemoved: onRemoved, - vsync: vsync, - ); - addInkFeature(highlight); - return highlight; - } - @override void addInkFeature(InkFeature feature) { assert(!feature._debugDisposed); @@ -478,11 +343,15 @@ class _InkFeatures extends SingleChildRenderObjectWidget { abstract class InkFeature { /// Initializes fields for subclasses. InkFeature({ - MaterialInkController controller, - this.referenceBox, + @required MaterialInkController controller, + @required this.referenceBox, this.onRemoved - }) : _controller = controller; + }) : _controller = controller { + assert(_controller != null); + assert(referenceBox != null); + } + MaterialInkController get controller => _controller; _RenderInkFeatures _controller; /// The render box whose visual position defines the frame of reference for this ink feature. @@ -494,6 +363,7 @@ abstract class InkFeature { bool _debugDisposed = false; /// Free up the resources associated with this ink feature. + @mustCallSuper void dispose() { assert(!_debugDisposed); assert(() { _debugDisposed = true; return true; }); @@ -506,7 +376,7 @@ abstract class InkFeature { assert(referenceBox.attached); assert(!_debugDisposed); // find the chain of renderers from us to the feature's referenceBox - List descendants = [referenceBox]; + final List descendants = [referenceBox]; RenderBox node = referenceBox; while (node != _controller) { node = node.parent; @@ -514,7 +384,7 @@ abstract class InkFeature { descendants.add(node); } // determine the transform that gets our coordinate system to be like theirs - Matrix4 transform = new Matrix4.identity(); + final Matrix4 transform = new Matrix4.identity(); assert(descendants.length >= 2); for (int index = descendants.length - 1; index > 0; index -= 1) descendants[index].applyPaintTransform(descendants[index - 1], transform); @@ -525,190 +395,9 @@ abstract class InkFeature { /// /// The transform argument gives the coordinate conversion from the coordinate /// system of the canvas to the coodinate system of the [referenceBox]. + @protected void paintFeature(Canvas canvas, Matrix4 transform); @override - String toString() => "$runtimeType@$hashCode"; -} - -class _InkSplash extends InkFeature implements InkSplash { - _InkSplash({ - _RenderInkFeatures controller, - RenderBox referenceBox, - this.position, - this.color, - this.targetRadius, - this.clipCallback, - this.repositionToReferenceBox, - VoidCallback onRemoved, - @required TickerProvider vsync, - }) : super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { - _radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration, vsync: vsync) - ..addListener(controller.markNeedsPaint) - ..forward(); - _radius = new Tween( - begin: _kSplashInitialSize, - end: targetRadius - ).animate(_radiusController); - _alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: vsync) - ..addListener(controller.markNeedsPaint) - ..addStatusListener(_handleAlphaStatusChanged); - _alpha = new IntTween( - begin: color.alpha, - end: 0 - ).animate(_alphaController); - } - - final Point position; - final Color color; - final double targetRadius; - final RectCallback clipCallback; - final bool repositionToReferenceBox; - - Animation _radius; - AnimationController _radiusController; - - Animation _alpha; - AnimationController _alphaController; - - @override - void confirm() { - int duration = (targetRadius / _kSplashConfirmedVelocity).floor(); - _radiusController - ..duration = new Duration(milliseconds: duration) - ..forward(); - _alphaController.forward(); - } - - @override - void cancel() { - _alphaController.forward(); - } - - void _handleAlphaStatusChanged(AnimationStatus status) { - if (status == AnimationStatus.completed) - dispose(); - } - - @override - void dispose() { - _radiusController.stop(); - _alphaController.stop(); - super.dispose(); - } - - @override - void paintFeature(Canvas canvas, Matrix4 transform) { - Paint paint = new Paint()..color = color.withAlpha(_alpha.value); - Point center = position; - if (repositionToReferenceBox) - center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value); - Offset originOffset = MatrixUtils.getAsTranslation(transform); - if (originOffset == null) { - canvas.save(); - canvas.transform(transform.storage); - if (clipCallback != null) - canvas.clipRect(clipCallback()); - canvas.drawCircle(center, _radius.value, paint); - canvas.restore(); - } else { - if (clipCallback != null) { - canvas.save(); - canvas.clipRect(clipCallback().shift(originOffset)); - } - canvas.drawCircle(center + originOffset, _radius.value, paint); - if (clipCallback != null) - canvas.restore(); - } - } -} - -class _InkHighlight extends InkFeature implements InkHighlight { - _InkHighlight({ - _RenderInkFeatures controller, - RenderBox referenceBox, - this.rectCallback, - Color color, - this.shape, - VoidCallback onRemoved, - @required TickerProvider vsync, - }) : _color = color, - super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { - _alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: vsync) - ..addListener(controller.markNeedsPaint) - ..addStatusListener(_handleAlphaStatusChanged) - ..forward(); - _alpha = new IntTween( - begin: 0, - end: color.alpha - ).animate(_alphaController); - } - - final RectCallback rectCallback; - - @override - Color get color => _color; - Color _color; - - @override - set color(Color value) { - if (value == _color) - return; - _color = value; - _controller.markNeedsPaint(); - } - - final BoxShape shape; - - @override - bool get active => _active; - bool _active = true; - - Animation _alpha; - AnimationController _alphaController; - - @override - void activate() { - _active = true; - _alphaController.forward(); - } - - @override - void deactivate() { - _active = false; - _alphaController.reverse(); - } - - void _handleAlphaStatusChanged(AnimationStatus status) { - if (status == AnimationStatus.dismissed && !_active) - dispose(); - } - - @override - void dispose() { - _alphaController.stop(); - super.dispose(); - } - - void _paintHighlight(Canvas canvas, Rect rect, Paint paint) { - if (shape == BoxShape.rectangle) - canvas.drawRect(rect, paint); - else - canvas.drawCircle(rect.center, InkSplash.defaultRadius, paint); - } - - @override - void paintFeature(Canvas canvas, Matrix4 transform) { - Paint paint = new Paint()..color = color.withAlpha(_alpha.value); - Offset originOffset = MatrixUtils.getAsTranslation(transform); - final Rect rect = (rectCallback != null ? rectCallback() : Point.origin & referenceBox.size); - if (originOffset == null) { - canvas.save(); - canvas.transform(transform.storage); - _paintHighlight(canvas, rect, paint); - canvas.restore(); - } else { - _paintHighlight(canvas, rect.shift(originOffset), paint); - } - } + String toString() => '$runtimeType@$hashCode'; }