Add a PhysicalShape widget and a render object for it. (#13682)
This CL also refactors common logic for RenderPhysicalModel and RenderPhysicalShape into a base class _RenderPhysicalModelBase.
This commit is contained in:
parent
6a42ed3f55
commit
0230a87476
@ -954,6 +954,42 @@ abstract class CustomClipper<T> {
|
||||
String toString() => '$runtimeType';
|
||||
}
|
||||
|
||||
/// A [CustomClipper] that clips to the outer path of a [ShapeBorder].
|
||||
class ShapeBorderClipper extends CustomClipper<Path> {
|
||||
/// Creates a [ShapeBorder] clipper.
|
||||
///
|
||||
/// The [shapeBorder] argument must not be null.
|
||||
///
|
||||
/// The [textDirection] argument must be provided non-null if [shapeBorder]
|
||||
/// has a text direction dependency (for example if it is expressed in terms
|
||||
/// of "start" and "end" instead of "left" and "right"). It may be null if
|
||||
/// the border will not need the text direction to paint itself.
|
||||
const ShapeBorderClipper({
|
||||
@required this.shapeBorder,
|
||||
this.textDirection,
|
||||
}) : assert(shapeBorder != null);
|
||||
|
||||
// The shape border whose outer path this clipper clips to.
|
||||
final ShapeBorder shapeBorder;
|
||||
|
||||
/// The text direction to use for getting the outer path for [shapeBorder].
|
||||
///
|
||||
/// [ShapeBorder]s can depend on the text direction (e.g having a "dent"
|
||||
/// towards the start of the shape).
|
||||
final TextDirection textDirection;
|
||||
|
||||
/// Returns the outer path of [shapeBorder] as the clip.
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
return shapeBorder.getOuterPath(Offset.zero & size, textDirection: textDirection);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant ShapeBorderClipper oldClipper) {
|
||||
return oldClipper.shapeBorder != shapeBorder;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _RenderCustomClip<T> extends RenderProxyBox {
|
||||
_RenderCustomClip({
|
||||
RenderBox child,
|
||||
@ -1292,64 +1328,25 @@ class RenderClipPath extends _RenderCustomClip<Path> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a physical model layer that clips its children to a rounded
|
||||
/// rectangle.
|
||||
///
|
||||
/// A physical model layer casts a shadow based on its [elevation].
|
||||
class RenderPhysicalModel extends _RenderCustomClip<RRect> {
|
||||
/// Creates a rounded-rectangular clip.
|
||||
///
|
||||
/// The [color] is required.
|
||||
///
|
||||
///
|
||||
/// The concrete implementations [RenderPhysicalModel] and [RenderPhysicalShape]
|
||||
/// determine the actual shape of the physical model.
|
||||
abstract class _RenderPhysicalModelBase<T> extends _RenderCustomClip<T> {
|
||||
/// The [shape], [elevation], [color], and [shadowColor] must not be null.
|
||||
RenderPhysicalModel({
|
||||
RenderBox child,
|
||||
BoxShape shape: BoxShape.rectangle,
|
||||
BorderRadius borderRadius,
|
||||
double elevation: 0.0,
|
||||
_RenderPhysicalModelBase({
|
||||
@required RenderBox child,
|
||||
@required double elevation,
|
||||
@required Color color,
|
||||
Color shadowColor: const Color(0xFF000000),
|
||||
}) : assert(shape != null),
|
||||
assert(elevation != null),
|
||||
@required Color shadowColor,
|
||||
CustomClipper<T> clipper,
|
||||
}) : assert(elevation != null),
|
||||
assert(color != null),
|
||||
assert(shadowColor != null),
|
||||
_shape = shape,
|
||||
_borderRadius = borderRadius,
|
||||
_elevation = elevation,
|
||||
_color = color,
|
||||
_shadowColor = shadowColor,
|
||||
super(child: child);
|
||||
|
||||
/// The shape of the layer.
|
||||
///
|
||||
/// Defaults to [BoxShape.rectangle]. The [borderRadius] affects the corners
|
||||
/// of the rectangle.
|
||||
BoxShape get shape => _shape;
|
||||
BoxShape _shape;
|
||||
set shape(BoxShape value) {
|
||||
assert(value != null);
|
||||
if (shape == value)
|
||||
return;
|
||||
_shape = value;
|
||||
_markNeedsClip();
|
||||
}
|
||||
|
||||
/// The border radius of the rounded corners.
|
||||
///
|
||||
/// Values are clamped so that horizontal and vertical radii sums do not
|
||||
/// exceed width/height.
|
||||
///
|
||||
/// This property is ignored if the [shape] is not [BoxShape.rectangle].
|
||||
///
|
||||
/// The value null is treated like [BorderRadius.zero].
|
||||
BorderRadius get borderRadius => _borderRadius;
|
||||
BorderRadius _borderRadius;
|
||||
set borderRadius(BorderRadius value) {
|
||||
if (borderRadius == value)
|
||||
return;
|
||||
_borderRadius = value;
|
||||
_markNeedsClip();
|
||||
}
|
||||
super(child: child, clipper: clipper);
|
||||
|
||||
/// The z-coordinate at which to place this material.
|
||||
double get elevation => _elevation;
|
||||
@ -1387,6 +1384,83 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
static final Paint _defaultPaint = new Paint();
|
||||
static final Paint _transparentPaint = new Paint()..color = const Color(0x00000000);
|
||||
|
||||
// On Fuchsia, the system compositor is responsible for drawing shadows
|
||||
// for physical model layers with non-zero elevation.
|
||||
@override
|
||||
bool get alwaysNeedsCompositing => _elevation != 0.0 && defaultTargetPlatform == TargetPlatform.fuchsia;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description.add(new DoubleProperty('elevation', elevation));
|
||||
description.add(new DiagnosticsProperty<Color>('color', color));
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a physical model layer that clips its child to a rounded
|
||||
/// rectangle.
|
||||
///
|
||||
/// A physical model layer casts a shadow based on its [elevation].
|
||||
class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> {
|
||||
/// Creates a rounded-rectangular clip.
|
||||
///
|
||||
/// The [color] is required.
|
||||
///
|
||||
/// The [shape], [elevation], [color], and [shadowColor] must not be null.
|
||||
RenderPhysicalModel({
|
||||
RenderBox child,
|
||||
BoxShape shape: BoxShape.rectangle,
|
||||
BorderRadius borderRadius,
|
||||
double elevation: 0.0,
|
||||
@required Color color,
|
||||
Color shadowColor: const Color(0xFF000000),
|
||||
}) : assert(shape != null),
|
||||
assert(elevation != null),
|
||||
assert(color != null),
|
||||
assert(shadowColor != null),
|
||||
_shape = shape,
|
||||
_borderRadius = borderRadius,
|
||||
super(
|
||||
child: child,
|
||||
elevation: elevation,
|
||||
color: color,
|
||||
shadowColor: shadowColor
|
||||
);
|
||||
|
||||
/// The shape of the layer.
|
||||
///
|
||||
/// Defaults to [BoxShape.rectangle]. The [borderRadius] affects the corners
|
||||
/// of the rectangle.
|
||||
BoxShape get shape => _shape;
|
||||
BoxShape _shape;
|
||||
set shape(BoxShape value) {
|
||||
assert(value != null);
|
||||
if (shape == value)
|
||||
return;
|
||||
_shape = value;
|
||||
_markNeedsClip();
|
||||
}
|
||||
|
||||
/// The border radius of the rounded corners.
|
||||
///
|
||||
/// Values are clamped so that horizontal and vertical radii sums do not
|
||||
/// exceed width/height.
|
||||
///
|
||||
/// This property is ignored if the [shape] is not [BoxShape.rectangle].
|
||||
///
|
||||
/// The value null is treated like [BorderRadius.zero].
|
||||
BorderRadius get borderRadius => _borderRadius;
|
||||
BorderRadius _borderRadius;
|
||||
set borderRadius(BorderRadius value) {
|
||||
if (borderRadius == value)
|
||||
return;
|
||||
_borderRadius = value;
|
||||
_markNeedsClip();
|
||||
}
|
||||
|
||||
@override
|
||||
RRect get _defaultClip {
|
||||
assert(hasSize);
|
||||
@ -1412,14 +1486,6 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
|
||||
return super.hitTest(result, position: position);
|
||||
}
|
||||
|
||||
static final Paint _defaultPaint = new Paint();
|
||||
static final Paint _transparentPaint = new Paint()..color = const Color(0x00000000);
|
||||
|
||||
// On Fuchsia, the system compositor is responsible for drawing shadows
|
||||
// for physical model layers with non-zero elevation.
|
||||
@override
|
||||
bool get alwaysNeedsCompositing => _elevation != 0.0 && defaultTargetPlatform == TargetPlatform.fuchsia;
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child != null) {
|
||||
@ -1443,7 +1509,7 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
|
||||
// TODO(jsimmons): remove this when Skia does it for us.
|
||||
canvas.drawRect(
|
||||
offsetBounds.inflate(20.0),
|
||||
_transparentPaint,
|
||||
_RenderPhysicalModelBase._transparentPaint,
|
||||
);
|
||||
canvas.drawShadow(
|
||||
offsetClipPath,
|
||||
@ -1462,7 +1528,7 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
|
||||
// the side of correctness here.
|
||||
// TODO(ianh): Find a better solution.
|
||||
if (!offsetClipRRect.isRect)
|
||||
canvas.saveLayer(offsetBounds, _defaultPaint);
|
||||
canvas.saveLayer(offsetBounds, _RenderPhysicalModelBase._defaultPaint);
|
||||
super.paint(context, offset);
|
||||
if (!offsetClipRRect.isRect)
|
||||
canvas.restore();
|
||||
@ -1477,8 +1543,101 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
|
||||
super.debugFillProperties(description);
|
||||
description.add(new DiagnosticsProperty<BoxShape>('shape', shape));
|
||||
description.add(new DiagnosticsProperty<BorderRadius>('borderRadius', borderRadius));
|
||||
description.add(new DoubleProperty('elevation', elevation));
|
||||
description.add(new DiagnosticsProperty<Color>('color', color));
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a physical shape layer that clips its child to a [Path].
|
||||
///
|
||||
/// A physical shape layer casts a shadow based on its [elevation].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RenderPhysicalModel], which is optimized for rounded rectangles and
|
||||
/// circles.
|
||||
class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> {
|
||||
/// Creates an arbitrary shape clip.
|
||||
///
|
||||
/// The [color] and [shape] parameters are required.
|
||||
///
|
||||
/// The [clipper], [elevation], [color] and [shadowColor] must
|
||||
/// not be null.
|
||||
RenderPhysicalShape({
|
||||
RenderBox child,
|
||||
@required CustomClipper<Path> clipper,
|
||||
double elevation: 0.0,
|
||||
@required Color color,
|
||||
Color shadowColor: const Color(0xFF000000),
|
||||
}) : assert(clipper != null),
|
||||
assert(elevation != null),
|
||||
assert(color != null),
|
||||
assert(shadowColor != null),
|
||||
super(
|
||||
child: child,
|
||||
elevation: elevation,
|
||||
color: color,
|
||||
shadowColor: shadowColor,
|
||||
clipper: clipper,
|
||||
);
|
||||
|
||||
@override
|
||||
Path get _defaultClip => new Path()..addRect(Offset.zero & size);
|
||||
|
||||
@override
|
||||
bool hitTest(HitTestResult result, { Offset position }) {
|
||||
if (_clipper != null) {
|
||||
_updateClip();
|
||||
assert(_clip != null);
|
||||
if (!_clip.contains(position))
|
||||
return false;
|
||||
}
|
||||
return super.hitTest(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child != null) {
|
||||
_updateClip();
|
||||
final Rect offsetBounds = offset & size;
|
||||
final Path offsetPath = _clip.shift(offset);
|
||||
if (needsCompositing) {
|
||||
final PhysicalModelLayer physicalModel = new PhysicalModelLayer(
|
||||
clipPath: offsetPath,
|
||||
elevation: elevation,
|
||||
color: color,
|
||||
);
|
||||
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds);
|
||||
} else {
|
||||
final Canvas canvas = context.canvas;
|
||||
if (elevation != 0.0) {
|
||||
// The drawShadow call doesn't add the region of the shadow to the
|
||||
// picture's bounds, so we draw a hardcoded amount of extra space to
|
||||
// account for the maximum potential area of the shadow.
|
||||
// TODO(jsimmons): remove this when Skia does it for us.
|
||||
canvas.drawRect(
|
||||
offsetBounds.inflate(20.0),
|
||||
_RenderPhysicalModelBase._transparentPaint,
|
||||
);
|
||||
canvas.drawShadow(
|
||||
offsetPath,
|
||||
shadowColor,
|
||||
elevation,
|
||||
color.alpha != 0xFF,
|
||||
);
|
||||
}
|
||||
canvas.drawPath(offsetPath, new Paint()..color = color..style = PaintingStyle.fill);
|
||||
canvas.saveLayer(offsetBounds, _RenderPhysicalModelBase._defaultPaint);
|
||||
canvas.clipPath(offsetPath);
|
||||
super.paint(context, offset);
|
||||
canvas.restore();
|
||||
assert(context.canvas == canvas, 'canvas changed even though needsCompositing was false');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description.add(new DiagnosticsProperty<CustomClipper<Path>>('clipper', clipper));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -642,6 +642,9 @@ class ClipPath extends SingleChildRenderObjectWidget {
|
||||
/// Physical layers cast shadows based on an [elevation] which is nominally in
|
||||
/// logical pixels, coming vertically out of the rendering surface.
|
||||
///
|
||||
/// For shapes that cannot be expressed as a rectangle with rounded corners use
|
||||
/// [PhysicalShape].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DecoratedBox], which can apply more arbitrary shadow effects.
|
||||
@ -717,6 +720,73 @@ class PhysicalModel extends SingleChildRenderObjectWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget representing a physical layer that clips its children to a path.
|
||||
///
|
||||
/// Physical layers cast shadows based on an [elevation] which is nominally in
|
||||
/// logical pixels, coming vertically out of the rendering surface.
|
||||
///
|
||||
/// [PhysicalModel] does the same but only supports shapes that can be expressed
|
||||
/// as rectangles with rounded corners.
|
||||
class PhysicalShape extends SingleChildRenderObjectWidget {
|
||||
/// Creates a physical model with an arbitrary shape clip.
|
||||
///
|
||||
/// The [color] is required; physical things have a color.
|
||||
///
|
||||
/// The [clipper], [elevation], [color], and [shadowColor] must not be null.
|
||||
const PhysicalShape({
|
||||
Key key,
|
||||
@required this.clipper,
|
||||
this.elevation: 0.0,
|
||||
@required this.color,
|
||||
this.shadowColor: const Color(0xFF000000),
|
||||
Widget child,
|
||||
}) : assert(clipper != null),
|
||||
assert(elevation != null),
|
||||
assert(color != null),
|
||||
assert(shadowColor != null),
|
||||
super(key: key, child: child);
|
||||
|
||||
/// Determines which clip to use.
|
||||
final CustomClipper<Path> clipper;
|
||||
|
||||
/// The z-coordinate at which to place this physical object.
|
||||
final double elevation;
|
||||
|
||||
/// The background color.
|
||||
final Color color;
|
||||
|
||||
/// When elevation is non zero the color to use for the shadow color.
|
||||
final Color shadowColor;
|
||||
|
||||
@override
|
||||
RenderPhysicalShape createRenderObject(BuildContext context) {
|
||||
return new RenderPhysicalShape(
|
||||
clipper: clipper,
|
||||
elevation: elevation,
|
||||
color: color,
|
||||
shadowColor: shadowColor
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderPhysicalShape renderObject) {
|
||||
renderObject
|
||||
..clipper = clipper
|
||||
..elevation = elevation
|
||||
..color = color
|
||||
..shadowColor = shadowColor;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description.add(new EnumProperty<CustomClipper<Path>>('clipper', clipper));
|
||||
description.add(new DoubleProperty('elevation', elevation));
|
||||
description.add(new DiagnosticsProperty<Color>('color', color));
|
||||
description.add(new DiagnosticsProperty<Color>('shadowColor', shadowColor));
|
||||
}
|
||||
}
|
||||
|
||||
// POSITIONING AND SIZING NODES
|
||||
|
||||
/// A widget that applies a transformation before painting its child.
|
||||
|
@ -91,4 +91,47 @@ void main() {
|
||||
expect(config.getActionHandler(SemanticsAction.scrollLeft), isNotNull);
|
||||
expect(config.getActionHandler(SemanticsAction.scrollRight), isNull);
|
||||
});
|
||||
|
||||
group('RenderPhysicalShape', () {
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
});
|
||||
|
||||
test('shape change triggers repaint', () {
|
||||
final RenderPhysicalShape root = new RenderPhysicalShape(
|
||||
color: const Color(0xffff00ff),
|
||||
clipper: const ShapeBorderClipper(shapeBorder: const CircleBorder()),
|
||||
);
|
||||
layout(root, phase: EnginePhase.composite);
|
||||
expect(root.debugNeedsPaint, isFalse);
|
||||
|
||||
// Same shape, no repaint.
|
||||
root.clipper = const ShapeBorderClipper(shapeBorder: const CircleBorder());
|
||||
expect(root.debugNeedsPaint, isFalse);
|
||||
|
||||
// Different shape triggers repaint.
|
||||
root.clipper = const ShapeBorderClipper(shapeBorder: const StadiumBorder());
|
||||
expect(root.debugNeedsPaint, isTrue);
|
||||
});
|
||||
|
||||
test('compositing on non-Fuchsia', () {
|
||||
final RenderPhysicalShape root = new RenderPhysicalShape(
|
||||
color: const Color(0xffff00ff),
|
||||
clipper: const ShapeBorderClipper(shapeBorder: const CircleBorder()),
|
||||
);
|
||||
layout(root, phase: EnginePhase.composite);
|
||||
expect(root.needsCompositing, isFalse);
|
||||
|
||||
// On non-Fuchsia platforms, Flutter draws its own shadows.
|
||||
root.elevation = 1.0;
|
||||
pumpFrame(phase: EnginePhase.composite);
|
||||
expect(root.needsCompositing, isFalse);
|
||||
|
||||
root.elevation = 0.0;
|
||||
pumpFrame(phase: EnginePhase.composite);
|
||||
expect(root.needsCompositing, isFalse);
|
||||
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
97
packages/flutter/test/widgets/basic_test.dart
Normal file
97
packages/flutter/test/widgets/basic_test.dart
Normal file
@ -0,0 +1,97 @@
|
||||
// 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_test/flutter_test.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
|
||||
void main() {
|
||||
group('PhysicalShape', () {
|
||||
testWidgets('properties', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const PhysicalShape(
|
||||
clipper: const ShapeBorderClipper(shapeBorder: const CircleBorder()),
|
||||
elevation: 2.0,
|
||||
color: const Color(0xFF0000FF),
|
||||
shadowColor: const Color(0xFF00FF00),
|
||||
)
|
||||
);
|
||||
final RenderPhysicalShape renderObject = tester.renderObject(find.byType(PhysicalShape));
|
||||
expect(renderObject.clipper, const ShapeBorderClipper(shapeBorder: const CircleBorder()));
|
||||
expect(renderObject.color, const Color(0xFF0000FF));
|
||||
expect(renderObject.shadowColor, const Color(0xFF00FF00));
|
||||
expect(renderObject.elevation, 2.0);
|
||||
});
|
||||
|
||||
testWidgets('hit test', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new PhysicalShape(
|
||||
clipper: const ShapeBorderClipper(shapeBorder: const CircleBorder()),
|
||||
elevation: 2.0,
|
||||
color: const Color(0xFF0000FF),
|
||||
shadowColor: const Color(0xFF00FF00),
|
||||
child: new Container(color: const Color(0xFF0000FF)),
|
||||
)
|
||||
);
|
||||
|
||||
final RenderPhysicalShape renderPhysicalShape =
|
||||
tester.renderObject(find.byType(PhysicalShape));
|
||||
|
||||
// The viewport is 800x600, the CircleBorder is centered and fits
|
||||
// the shortest edge, so we get a circle of radius 300, centered at
|
||||
// (400, 300).
|
||||
//
|
||||
// We test by sampling a few points around the left-most point of the
|
||||
// circle (100, 300).
|
||||
|
||||
expect(tester.hitTestOnBinding(const Offset(99.0, 300.0)), doesNotHit(renderPhysicalShape));
|
||||
expect(tester.hitTestOnBinding(const Offset(100.0, 300.0)), hits(renderPhysicalShape));
|
||||
expect(tester.hitTestOnBinding(const Offset(100.0, 299.0)), doesNotHit(renderPhysicalShape));
|
||||
expect(tester.hitTestOnBinding(const Offset(100.0, 301.0)), doesNotHit(renderPhysicalShape));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
HitsRenderBox hits(RenderBox renderBox) => new HitsRenderBox(renderBox);
|
||||
|
||||
class HitsRenderBox extends Matcher {
|
||||
const HitsRenderBox(this.renderBox);
|
||||
|
||||
final RenderBox renderBox;
|
||||
|
||||
@override
|
||||
Description describe(Description description) =>
|
||||
description.add('hit test result contains ').addDescriptionOf(renderBox);
|
||||
|
||||
@override
|
||||
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
||||
final HitTestResult hitTestResult = item;
|
||||
return hitTestResult.path.where(
|
||||
(HitTestEntry entry) => entry.target == renderBox
|
||||
).isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
DoesNotHitRenderBox doesNotHit(RenderBox renderBox) => new DoesNotHitRenderBox(renderBox);
|
||||
|
||||
class DoesNotHitRenderBox extends Matcher {
|
||||
const DoesNotHitRenderBox(this.renderBox);
|
||||
|
||||
final RenderBox renderBox;
|
||||
|
||||
@override
|
||||
Description describe(Description description) =>
|
||||
description.add('hit test result doesn\'t contain ').addDescriptionOf(renderBox);
|
||||
|
||||
@override
|
||||
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
||||
final HitTestResult hitTestResult = item;
|
||||
return hitTestResult.path.where(
|
||||
(HitTestEntry entry) => entry.target == renderBox
|
||||
).isEmpty;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user