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 { ...@@ -357,8 +357,14 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
final ShapeBorder shape = _getShape(); final ShapeBorder shape = _getShape();
if (widget.type == MaterialType.transparency) if (widget.type == MaterialType.transparency) {
return _transparentInterior(shape: shape, clipBehavior: widget.clipBehavior, contents: contents); return _transparentInterior(
context: context,
shape: shape,
clipBehavior: widget.clipBehavior,
contents: contents,
);
}
return _MaterialInterior( return _MaterialInterior(
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
...@@ -372,7 +378,12 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin { ...@@ -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( final _ShapeBorderPaint child = _ShapeBorderPaint(
child: contents, child: contents,
shape: shape, shape: shape,
...@@ -382,7 +393,10 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin { ...@@ -382,7 +393,10 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
} }
return ClipPath( return ClipPath(
child: child, child: child,
clipper: ShapeBorderClipper(shape: shape), clipper: ShapeBorderClipper(
shape: shape,
textDirection: Directionality.of(context),
),
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
); );
} }
......
...@@ -1053,7 +1053,7 @@ class RenderBackdropFilter extends RenderProxyBox { ...@@ -1053,7 +1053,7 @@ class RenderBackdropFilter extends RenderProxyBox {
/// information. /// information.
/// ///
/// The most efficient way to update the clip provided by this class is to /// 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 /// 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. /// animation ticks, avoiding both the build and layout phases of the pipeline.
/// ///
...@@ -1063,6 +1063,7 @@ class RenderBackdropFilter extends RenderProxyBox { ...@@ -1063,6 +1063,7 @@ class RenderBackdropFilter extends RenderProxyBox {
/// * [ClipRRect], which can be customized with a [CustomClipper<RRect>]. /// * [ClipRRect], which can be customized with a [CustomClipper<RRect>].
/// * [ClipOval], which can be customized with a [CustomClipper<Rect>]. /// * [ClipOval], which can be customized with a [CustomClipper<Rect>].
/// * [ClipPath], which can be customized with a [CustomClipper<Path>]. /// * [ClipPath], which can be customized with a [CustomClipper<Path>].
/// * [ShapeBorderClipper], for specifying a clip path using a [ShapeBorder].
abstract class CustomClipper<T> { abstract class CustomClipper<T> {
/// Creates a custom clipper. /// Creates a custom clipper.
/// ///
...@@ -1141,7 +1142,8 @@ class ShapeBorderClipper extends CustomClipper<Path> { ...@@ -1141,7 +1142,8 @@ class ShapeBorderClipper extends CustomClipper<Path> {
if (oldClipper.runtimeType != ShapeBorderClipper) if (oldClipper.runtimeType != ShapeBorderClipper)
return true; return true;
final ShapeBorderClipper typedOldClipper = oldClipper; final ShapeBorderClipper typedOldClipper = oldClipper;
return typedOldClipper.shape != shape; return typedOldClipper.shape != shape
|| typedOldClipper.textDirection != textDirection;
} }
} }
......
...@@ -690,6 +690,10 @@ class ClipOval extends SingleChildRenderObjectWidget { ...@@ -690,6 +690,10 @@ class ClipOval extends SingleChildRenderObjectWidget {
/// * To clip to a rectangle, consider [ClipRect]. /// * To clip to a rectangle, consider [ClipRect].
/// * To clip to an oval or circle, consider [ClipOval]. /// * To clip to an oval or circle, consider [ClipOval].
/// * To clip to a rounded rectangle, consider [ClipRRect]. /// * 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 { class ClipPath extends SingleChildRenderObjectWidget {
/// Creates a path clip. /// Creates a path clip.
/// ///
...@@ -697,7 +701,38 @@ class ClipPath extends SingleChildRenderObjectWidget { ...@@ -697,7 +701,38 @@ class ClipPath extends SingleChildRenderObjectWidget {
/// size and location of the child. However, rather than use this default, /// size and location of the child. However, rather than use this default,
/// consider using a [ClipRect], which can achieve the same effect more /// consider using a [ClipRect], which can achieve the same effect more
/// efficiently. /// 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. /// If non-null, determines which clip to use.
/// ///
......
...@@ -20,6 +20,9 @@ import 'image.dart'; ...@@ -20,6 +20,9 @@ import 'image.dart';
/// ///
/// Commonly used with [BoxDecoration]. /// 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} /// {@tool sample}
/// ///
/// This sample shows a radial gradient that draws a moon on a night sky: /// This sample shows a radial gradient that draws a moon on a night sky:
...@@ -313,6 +316,9 @@ class Container extends StatelessWidget { ...@@ -313,6 +316,9 @@ class Container extends StatelessWidget {
/// A shorthand for specifying just a solid color is available in the /// A shorthand for specifying just a solid color is available in the
/// constructor: set the `color` argument instead of the `decoration` /// constructor: set the `color` argument instead of the `decoration`
/// argument. /// 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; final Decoration decoration;
/// The decoration to paint in front of the [child]. /// The decoration to paint in front of the [child].
......
...@@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../widgets/shape_decoration_test.dart' show TestBorder;
class NotifyMaterial extends StatelessWidget { class NotifyMaterial extends StatelessWidget {
@override @override
...@@ -237,6 +238,69 @@ void main() { ...@@ -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', () { group('PhysicalModels', () {
......
...@@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import 'shape_decoration_test.dart' show TestBorder;
final List<String> log = <String>[]; final List<String> log = <String>[];
...@@ -686,4 +687,61 @@ void main() { ...@@ -686,4 +687,61 @@ void main() {
matchesGoldenFile('clip.PhysicalShape.default.png'), 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