From f68cdacdd5ae701de7c8e532342f91f85d5a071f Mon Sep 17 00:00:00 2001 From: Sahand Akbarzadeh Date: Tue, 3 Dec 2019 18:13:01 +0000 Subject: [PATCH] Add clip behaviour to Container (#44971) --- .../lib/src/painting/box_decoration.dart | 15 +++++++ .../flutter/lib/src/painting/decoration.dart | 3 ++ .../lib/src/painting/shape_decoration.dart | 5 +++ .../flutter/lib/src/widgets/container.dart | 42 +++++++++++++++++++ .../test/painting/box_decoration_test.dart | 14 +++++++ .../test/painting/shape_decoration_test.dart | 11 +++++ .../flutter/test/widgets/container_test.dart | 32 ++++++++++++++ 7 files changed, 122 insertions(+) diff --git a/packages/flutter/lib/src/painting/box_decoration.dart b/packages/flutter/lib/src/painting/box_decoration.dart index bb32750155..9bf5851580 100644 --- a/packages/flutter/lib/src/painting/box_decoration.dart +++ b/packages/flutter/lib/src/painting/box_decoration.dart @@ -213,6 +213,21 @@ class BoxDecoration extends Decoration { @override EdgeInsetsGeometry get padding => border?.dimensions; + @override + Path getClipPath(Rect rect, TextDirection textDirection) { + Path clipPath; + switch (shape) { + case BoxShape.circle: + clipPath = Path()..addOval(rect); + break; + case BoxShape.rectangle: + if (borderRadius != null) + clipPath = Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect)); + break; + } + return clipPath; + } + /// Returns a new box decoration that is scaled by the given factor. BoxDecoration scale(double factor) { return BoxDecoration( diff --git a/packages/flutter/lib/src/painting/decoration.dart b/packages/flutter/lib/src/painting/decoration.dart index a54a2f47ce..96574ad21a 100644 --- a/packages/flutter/lib/src/painting/decoration.dart +++ b/packages/flutter/lib/src/painting/decoration.dart @@ -165,6 +165,9 @@ abstract class Decoration extends Diagnosticable { /// omitted if there is no chance that the painter will change (for example, /// if it is a [BoxDecoration] with definitely no [DecorationImage]). BoxPainter createBoxPainter([ VoidCallback onChanged ]); + + /// Returns a closed [Path] that describes the outer edge of this decoration. + Path getClipPath(Rect rect, TextDirection textDirection) => null; } /// A stateful class that can paint a particular [Decoration]. diff --git a/packages/flutter/lib/src/painting/shape_decoration.dart b/packages/flutter/lib/src/painting/shape_decoration.dart index 409f210f81..50ae38f452 100644 --- a/packages/flutter/lib/src/painting/shape_decoration.dart +++ b/packages/flutter/lib/src/painting/shape_decoration.dart @@ -122,6 +122,11 @@ class ShapeDecoration extends Decoration { ); } + @override + Path getClipPath(Rect rect, TextDirection textDirection) { + return shape.getOuterPath(rect, textDirection: textDirection); + } + /// The color to fill in the background of the shape. /// /// The color is under the [image]. diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index f0796b2836..2800c61815 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -312,10 +312,12 @@ class Container extends StatelessWidget { this.margin, this.transform, this.child, + this.clipBehavior = Clip.none, }) : assert(margin == null || margin.isNonNegative), assert(padding == null || padding.isNonNegative), assert(decoration == null || decoration.debugAssertIsValid()), assert(constraints == null || constraints.debugAssertIsValid()), + assert(clipBehavior != null), assert(color == null || decoration == null, 'Cannot provide both a color and a decoration\n' 'The color argument is just a shorthand for "decoration: new BoxDecoration(color: color)".' @@ -388,6 +390,11 @@ class Container extends StatelessWidget { /// The transformation matrix to apply before painting the container. final Matrix4 transform; + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + EdgeInsetsGeometry get _paddingIncludingDecoration { if (decoration == null || decoration.padding == null) return padding; @@ -436,6 +443,17 @@ class Container extends StatelessWidget { if (transform != null) current = Transform(transform: transform, child: current); + if (clipBehavior != Clip.none) { + current = ClipPath( + clipper: _DecorationClipper( + textDirection: Directionality.of(context), + decoration: decoration + ), + clipBehavior: clipBehavior, + child: current, + ); + } + return current; } @@ -444,6 +462,7 @@ class Container extends StatelessWidget { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('alignment', alignment, showName: false, defaultValue: null)); properties.add(DiagnosticsProperty('padding', padding, defaultValue: null)); + properties.add(DiagnosticsProperty('clipBehavior', clipBehavior, defaultValue: Clip.none)); properties.add(DiagnosticsProperty('bg', decoration, defaultValue: null)); properties.add(DiagnosticsProperty('fg', foregroundDecoration, defaultValue: null)); properties.add(DiagnosticsProperty('constraints', constraints, defaultValue: null)); @@ -451,3 +470,26 @@ class Container extends StatelessWidget { properties.add(ObjectFlagProperty.has('transform', transform)); } } + +/// A clipper that uses [Decoration.getClipPath] to clip. +class _DecorationClipper extends CustomClipper { + _DecorationClipper({ + TextDirection textDirection, + @required this.decoration + }) : assert (decoration != null), + textDirection = textDirection ?? TextDirection.ltr; + + final TextDirection textDirection; + final Decoration decoration; + + @override + Path getClip(Size size) { + return decoration.getClipPath(Offset.zero & size, textDirection); + } + + @override + bool shouldReclip(_DecorationClipper oldClipper) { + return oldClipper.decoration != decoration + || oldClipper.textDirection != textDirection; + } +} diff --git a/packages/flutter/test/painting/box_decoration_test.dart b/packages/flutter/test/painting/box_decoration_test.dart index 54a99514f2..a9d31fb878 100644 --- a/packages/flutter/test/painting/box_decoration_test.dart +++ b/packages/flutter/test/painting/box_decoration_test.dart @@ -68,4 +68,18 @@ void main() { paints..rect(rect: Offset.zero & size), ); }); + + test('BoxDecoration.getClipPath', () { + const double radius = 10; + final BoxDecoration decoration = BoxDecoration( + borderRadius: BorderRadius.circular(radius), + ); + const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 20.0); + final Path clipPath = decoration.getClipPath(rect, TextDirection.ltr); + final Matcher isLookLikeExpectedPath = isPathThat( + includes: const [ Offset(30.0, 10.0), Offset(50.0, 10.0), ], + excludes: const [ Offset(1.0, 1.0), Offset(99.0, 19.0), ], + ); + expect(clipPath, isLookLikeExpectedPath); + }); } diff --git a/packages/flutter/test/painting/shape_decoration_test.dart b/packages/flutter/test/painting/shape_decoration_test.dart index 7c8374e56f..73edff3997 100644 --- a/packages/flutter/test/painting/shape_decoration_test.dart +++ b/packages/flutter/test/painting/shape_decoration_test.dart @@ -98,6 +98,17 @@ void main() { ); expect(log, isEmpty); }); + + test('ShapeDecoration.getClipPath', () { + const ShapeDecoration decoration = ShapeDecoration(shape: CircleBorder(side: BorderSide.none)); + const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 20.0); + final Path clipPath = decoration.getClipPath(rect, TextDirection.ltr); + final Matcher isLookLikeExpectedPath = isPathThat( + includes: const [ Offset(50.0, 10.0), ], + excludes: const [ Offset(1.0, 1.0), Offset(30.0, 10.0), Offset(99.0, 19.0), ], + ); + expect(clipPath, isLookLikeExpectedPath); + }); } class TestImageProvider extends ImageProvider { diff --git a/packages/flutter/test/widgets/container_test.dart b/packages/flutter/test/widgets/container_test.dart index b910cddd89..0f1f05b8c6 100644 --- a/packages/flutter/test/widgets/container_test.dart +++ b/packages/flutter/test/widgets/container_test.dart @@ -498,6 +498,38 @@ void main() { ), ); }); + + testWidgets('giving clipBehaviour Clip.None, will not add a ClipPath to the tree', (WidgetTester tester) async { + await tester.pumpWidget(Container( + clipBehavior: Clip.none, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1), + ), + child: const SizedBox(), + )); + + expect( + find.byType(ClipPath), + findsNothing, + ); + }); + + testWidgets('giving clipBehaviour not a Clip.None, will add a ClipPath to the tree', (WidgetTester tester) async { + final Container container = Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1), + ), + child: const SizedBox(), + ); + + await tester.pumpWidget(container); + + expect( + find.byType(ClipPath), + findsOneWidget, + ); + }); } class _MockPaintingContext extends Mock implements PaintingContext {}