diff --git a/packages/flutter/lib/src/painting/box_painter.dart b/packages/flutter/lib/src/painting/box_painter.dart index 394beef51b7..9d9b413106c 100644 --- a/packages/flutter/lib/src/painting/box_painter.dart +++ b/packages/flutter/lib/src/painting/box_painter.dart @@ -1051,7 +1051,7 @@ class BoxDecoration extends Decoration { /// * If [backgroundColor] is null, this decoration does not paint a background color. /// * If [backgroundImage] is null, this decoration does not paint a background image. /// * If [border] is null, this decoration does not paint a border. - /// * If [borderRadius] is null, this decoration use more efficient background + /// * If [borderRadius] is null, this decoration uses more efficient background /// painting commands. The [borderRadius] argument must be be null if [shape] is /// [BoxShape.circle]. /// * If [boxShadow] is null, this decoration does not paint a shadow. @@ -1079,7 +1079,8 @@ class BoxDecoration extends Decoration { /// potentially with a border radius, or a circle). final Color backgroundColor; - /// An image to paint above the background color. + /// An image to paint above the background color. If [shape] is [BoxShape.circle] + /// then the image is clipped to the circle's boundary. final BackgroundImage backgroundImage; /// A border to draw above the background. @@ -1333,6 +1334,17 @@ class _BoxDecorationPainter extends BoxPainter { final ui.Image image = _image?.image; if (image == null) return; + + Path clipPath; + if (_decoration.shape == BoxShape.circle) + clipPath = new Path()..addOval(rect); + else if (_decoration.borderRadius != null) + clipPath = new Path()..addRRect(_decoration.borderRadius.toRRect(rect)); + if (clipPath != null) { + canvas.save(); + canvas.clipPath(clipPath); + } + paintImage( canvas: canvas, rect: rect, @@ -1342,6 +1354,9 @@ class _BoxDecorationPainter extends BoxPainter { fit: backgroundImage.fit, repeat: backgroundImage.repeat ); + + if (clipPath != null) + canvas.restore(); } void _imageListener(ImageInfo value, bool synchronousCall) { diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart index 356541b17ac..98400cadbad 100644 --- a/packages/flutter/test/painting/decoration_test.dart +++ b/packages/flutter/test/painting/decoration_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:ui' as ui show Image; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; @@ -13,8 +14,14 @@ import 'package:test/test.dart'; import '../services/mocks_for_image_cache.dart'; class TestCanvas implements Canvas { + TestCanvas([this.invocations]); + + final List invocations; + @override - void noSuchMethod(Invocation invocation) {} + void noSuchMethod(Invocation invocation) { + invocations?.add(invocation); + } } class SynchronousTestImageProvider extends ImageProvider { @@ -45,6 +52,43 @@ class AsyncTestImageProvider extends ImageProvider { } } +class BackgroundImageProvider extends ImageProvider { + final Completer _completer = new Completer(); + + @override + Future obtainKey(ImageConfiguration configuration) { + return new SynchronousFuture(this); + } + + @override + ImageStream resolve(ImageConfiguration configuration) { + return super.resolve(configuration); + } + + @override + ImageStreamCompleter load(BackgroundImageProvider key) { + return new OneFrameImageStreamCompleter(_completer.future); + } + + void complete() { + _completer.complete(new ImageInfo(image: new TestImage())); + } + + @override + String toString() => '$runtimeType($hashCode)'; +} + +class TestImage extends ui.Image { + @override + int get width => 100; + + @override + int get height => 100; + + @override + void dispose() { } +} + void main() { test("Decoration.lerp()", () { BoxDecoration a = const BoxDecoration(backgroundColor: const Color(0xFFFFFFFF)); @@ -99,4 +143,59 @@ void main() { expect(onChangedCalled, equals(true)); }); }); + + // Regression test for https://github.com/flutter/flutter/issues/7289. + // A reference test would be better. + test("BoxDecoration backgroundImage clip", () { + void testDecoration({ BoxShape shape, BorderRadius borderRadius, bool expectClip}) { + new FakeAsync().run((FakeAsync async) { + BackgroundImageProvider imageProvider = new BackgroundImageProvider(); + BackgroundImage backgroundImage = new BackgroundImage(image: imageProvider); + + BoxDecoration boxDecoration = new BoxDecoration( + shape: shape, + borderRadius: borderRadius, + backgroundImage: backgroundImage, + ); + + List invocations = []; + TestCanvas canvas = new TestCanvas(invocations); + ImageConfiguration imageConfiguration = const ImageConfiguration( + size: const Size(100.0, 100.0) + ); + bool onChangedCalled = false; + BoxPainter boxPainter = boxDecoration.createBoxPainter(() { + onChangedCalled = true; + }); + + // _BoxDecorationPainter._paintBackgroundImage() resolves the background + // image and adds a listener to the resolved image stream. + boxPainter.paint(canvas, Offset.zero, imageConfiguration); + imageProvider.complete(); + + // Run the listener which calls onChanged() which saves an internal + // reference to the TestImage. + async.flushMicrotasks(); + expect(onChangedCalled, isTrue); + boxPainter.paint(canvas, Offset.zero, imageConfiguration); + + // We expect a clip to preceed the drawImageRect call. + List commands = canvas.invocations.where((Invocation invocation) { + return invocation.memberName == #clipPath || invocation.memberName == #drawImageRect; + }).toList(); + if (expectClip) { // We expect a clip to preceed the drawImageRect call. + expect(commands.length, 2); + expect(commands[0].memberName, equals(#clipPath)); + expect(commands[1].memberName, equals(#drawImageRect)); + } else { + expect(commands.length, 1); + expect(commands[0].memberName, equals(#drawImageRect)); + } + }); + } + + testDecoration(shape: BoxShape.circle, expectClip: true); + testDecoration(borderRadius: new BorderRadius.all(const Radius.circular(16.0)), expectClip: true); + testDecoration(expectClip: false); + }); }