diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index 08e646e3b0..02fb410d84 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -357,8 +357,14 @@ class _MaterialState extends State 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 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 with TickerProviderStateMixin { } return ClipPath( child: child, - clipper: ShapeBorderClipper(shape: shape), + clipper: ShapeBorderClipper( + shape: shape, + textDirection: Directionality.of(context), + ), clipBehavior: clipBehavior, ); } diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 568a21a471..5a6927f51e 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -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]. /// * [ClipOval], which can be customized with a [CustomClipper]. /// * [ClipPath], which can be customized with a [CustomClipper]. +/// * [ShapeBorderClipper], for specifying a clip path using a [ShapeBorder]. abstract class CustomClipper { /// Creates a custom clipper. /// @@ -1141,7 +1142,8 @@ class ShapeBorderClipper extends CustomClipper { if (oldClipper.runtimeType != ShapeBorderClipper) return true; final ShapeBorderClipper typedOldClipper = oldClipper; - return typedOldClipper.shape != shape; + return typedOldClipper.shape != shape + || typedOldClipper.textDirection != textDirection; } } diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 08955dba0c..9887a1f670 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -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. /// diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index 64ce454213..ac4b4649db 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -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]. diff --git a/packages/flutter/test/material/material_test.dart b/packages/flutter/test/material/material_test.dart index 202cd811b8..3900a70f56 100644 --- a/packages/flutter/test/material/material_test.dart +++ b/packages/flutter/test/material/material_test.dart @@ -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 logs = []; + 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, [ + '--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', () { diff --git a/packages/flutter/test/widgets/clip_test.dart b/packages/flutter/test/widgets/clip_test.dart index 30c5646db6..ef695a1311 100644 --- a/packages/flutter/test/widgets/clip_test.dart +++ b/packages/flutter/test/widgets/clip_test.dart @@ -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 log = []; @@ -686,4 +687,61 @@ void main() { matchesGoldenFile('clip.PhysicalShape.default.png'), ); }); + + testWidgets('ClipPath.shape', (WidgetTester tester) async { + final List logs = []; + 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, [ + '--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', + ]); + }); }