Unverified Commit e2398725 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

ClipPath.shape and related fixes (#24816)

parent c5ad1067
......@@ -357,8 +357,14 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
final ShapeBorder shape = _getShape();
if (widget.type == MaterialType.transparency)
return _transparentInterior(shape: shape, clipBehavior: widget.clipBehavior, contents: contents);
if (widget.type == MaterialType.transparency) {
return _transparentInterior(
context: context,
shape: shape,
clipBehavior: widget.clipBehavior,
contents: contents,
);
}
return _MaterialInterior(
curve: Curves.fastOutSlowIn,
......@@ -372,7 +378,12 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
);
}
static Widget _transparentInterior({ShapeBorder shape, Clip clipBehavior, Widget contents}) {
static Widget _transparentInterior({
@required BuildContext context,
@required ShapeBorder shape,
@required Clip clipBehavior,
@required Widget contents,
}) {
final _ShapeBorderPaint child = _ShapeBorderPaint(
child: contents,
shape: shape,
......@@ -382,7 +393,10 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
}
return ClipPath(
child: child,
clipper: ShapeBorderClipper(shape: shape),
clipper: ShapeBorderClipper(
shape: shape,
textDirection: Directionality.of(context),
),
clipBehavior: clipBehavior,
);
}
......
......@@ -1053,7 +1053,7 @@ class RenderBackdropFilter extends RenderProxyBox {
/// information.
///
/// The most efficient way to update the clip provided by this class is to
/// supply a reclip argument to the constructor of the [CustomClipper]. The
/// supply a `reclip` argument to the constructor of the [CustomClipper]. The
/// custom object will listen to this animation and update the clip whenever the
/// animation ticks, avoiding both the build and layout phases of the pipeline.
///
......@@ -1063,6 +1063,7 @@ class RenderBackdropFilter extends RenderProxyBox {
/// * [ClipRRect], which can be customized with a [CustomClipper<RRect>].
/// * [ClipOval], which can be customized with a [CustomClipper<Rect>].
/// * [ClipPath], which can be customized with a [CustomClipper<Path>].
/// * [ShapeBorderClipper], for specifying a clip path using a [ShapeBorder].
abstract class CustomClipper<T> {
/// Creates a custom clipper.
///
......@@ -1141,7 +1142,8 @@ class ShapeBorderClipper extends CustomClipper<Path> {
if (oldClipper.runtimeType != ShapeBorderClipper)
return true;
final ShapeBorderClipper typedOldClipper = oldClipper;
return typedOldClipper.shape != shape;
return typedOldClipper.shape != shape
|| typedOldClipper.textDirection != textDirection;
}
}
......
......@@ -690,6 +690,10 @@ class ClipOval extends SingleChildRenderObjectWidget {
/// * To clip to a rectangle, consider [ClipRect].
/// * To clip to an oval or circle, consider [ClipOval].
/// * To clip to a rounded rectangle, consider [ClipRRect].
///
/// To clip to a particular [ShapeBorder], consider using either the
/// [ClipPath.shape] static method or the [ShapeBorderClipper] custom clipper
/// class.
class ClipPath extends SingleChildRenderObjectWidget {
/// Creates a path clip.
///
......@@ -697,7 +701,38 @@ class ClipPath extends SingleChildRenderObjectWidget {
/// size and location of the child. However, rather than use this default,
/// consider using a [ClipRect], which can achieve the same effect more
/// efficiently.
const ClipPath({ Key key, this.clipper, this.clipBehavior = Clip.antiAlias, Widget child }) : super(key: key, child: child);
const ClipPath({
Key key,
this.clipper,
this.clipBehavior = Clip.antiAlias,
Widget child,
}) : super(key: key, child: child);
/// Creates a shape clip.
///
/// Uses a [ShapeBorderClipper] to configure the [ClipPath] to clip to the
/// given [ShapeBorder].
static Widget shape({
Key key,
@required ShapeBorder shape,
Clip clipBehavior = Clip.antiAlias,
Widget child,
}) {
assert(shape != null);
return Builder(
key: key,
builder: (BuildContext context) {
return ClipPath(
clipper: ShapeBorderClipper(
shape: shape,
textDirection: Directionality.of(context),
),
clipBehavior: clipBehavior,
child: child,
);
},
);
}
/// If non-null, determines which clip to use.
///
......
......@@ -20,6 +20,9 @@ import 'image.dart';
///
/// Commonly used with [BoxDecoration].
///
/// The [child] is not clipped. To clip a child to the shape of a particular
/// [ShapeDecoration], consider using a [ClipPath] widget.
///
/// {@tool sample}
///
/// This sample shows a radial gradient that draws a moon on a night sky:
......@@ -313,6 +316,9 @@ class Container extends StatelessWidget {
/// A shorthand for specifying just a solid color is available in the
/// constructor: set the `color` argument instead of the `decoration`
/// argument.
///
/// The [child] is not clipped to the decoration. To clip a child to the shape
/// of a particular [ShapeDecoration], consider using a [ClipPath] widget.
final Decoration decoration;
/// The decoration to paint in front of the [child].
......
......@@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/shape_decoration_test.dart' show TestBorder;
class NotifyMaterial extends StatelessWidget {
@override
......@@ -237,6 +238,69 @@ void main() {
),
);
});
testWidgets('supports directional clips', (WidgetTester tester) async {
final List<String> logs = <String>[];
final ShapeBorder shape = TestBorder((String message) { logs.add(message); });
Widget buildMaterial() {
return Material(
type: MaterialType.transparency,
shape: shape,
child: const SizedBox(width: 100.0, height: 100.0),
clipBehavior: Clip.antiAlias,
);
}
final Widget material = buildMaterial();
// verify that a regular clip works as one would expect
logs.add('--0');
await tester.pumpWidget(material);
// verify that pumping again doesn't recompute the clip
// even though the widget itself is new (the shape doesn't change identity)
logs.add('--1');
await tester.pumpWidget(buildMaterial());
// verify that Material passes the TextDirection on to its shape when it's transparent
logs.add('--2');
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: material,
));
// verify that changing the text direction from LTR to RTL has an effect
// even though the widget itself is identical
logs.add('--3');
await tester.pumpWidget(Directionality(
textDirection: TextDirection.rtl,
child: material,
));
// verify that pumping again with a text direction has no effect
logs.add('--4');
await tester.pumpWidget(Directionality(
textDirection: TextDirection.rtl,
child: buildMaterial(),
));
logs.add('--5');
// verify that changing the text direction and the widget at the same time
// works as expected
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: material,
));
expect(logs, <String>[
'--0',
'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) null',
'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) null',
'--1',
'--2',
'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
'--3',
'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.rtl',
'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.rtl',
'--4',
'--5',
'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
'paint Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
]);
});
});
group('PhysicalModels', () {
......
......@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
import 'shape_decoration_test.dart' show TestBorder;
final List<String> log = <String>[];
......@@ -686,4 +687,61 @@ void main() {
matchesGoldenFile('clip.PhysicalShape.default.png'),
);
});
testWidgets('ClipPath.shape', (WidgetTester tester) async {
final List<String> logs = <String>[];
final ShapeBorder shape = TestBorder((String message) { logs.add(message); });
Widget buildClipPath() {
return ClipPath.shape(
shape: shape,
child: const SizedBox(width: 100.0, height: 100.0),
);
}
final Widget clipPath = buildClipPath();
// verify that a regular clip works as one would expect
logs.add('--0');
await tester.pumpWidget(clipPath);
// verify that pumping again doesn't recompute the clip
// even though the widget itself is new (the shape doesn't change identity)
logs.add('--1');
await tester.pumpWidget(buildClipPath());
// verify that ClipPath passes the TextDirection on to its shape
logs.add('--2');
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: clipPath,
));
// verify that changing the text direction from LTR to RTL has an effect
// even though the widget itself is identical
logs.add('--3');
await tester.pumpWidget(Directionality(
textDirection: TextDirection.rtl,
child: clipPath,
));
// verify that pumping again with a text direction has no effect
logs.add('--4');
await tester.pumpWidget(Directionality(
textDirection: TextDirection.rtl,
child: buildClipPath(),
));
logs.add('--5');
// verify that changing the text direction and the widget at the same time
// works as expected
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: clipPath,
));
expect(logs, <String>[
'--0',
'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) null',
'--1',
'--2',
'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
'--3',
'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.rtl',
'--4',
'--5',
'getOuterPath Rect.fromLTRB(0.0, 0.0, 800.0, 600.0) TextDirection.ltr',
]);
});
}
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