Unverified Commit 0230a874 authored by amirh's avatar amirh Committed by GitHub

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.
parent 6a42ed3f
......@@ -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;
});
});
}
// 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;
}
}
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