Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 69421c1

Browse files
[framework] use shader tiling instead of repeated calls to drawImage (#119495)
* [framework] use shader tiling instead of repeated calls to drawImage * ++ * ++ * review and test updates * ++ * Update decoration_image.dart * Update decoration_image.dart * ++ * ++ * ++ * ++ * ++ * Update painting.dart * Update decoration_test.dart
1 parent f3effce commit 69421c1

File tree

4 files changed

+160
-110
lines changed

4 files changed

+160
-110
lines changed

packages/flutter/lib/src/painting/decoration_image.dart

+65-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:ui' as ui show FlutterView, Image;
88

99
import 'package:flutter/foundation.dart';
1010
import 'package:flutter/scheduler.dart';
11+
import 'package:vector_math/vector_math_64.dart';
1112

1213
import 'alignment.dart';
1314
import 'basic_types.dart';
@@ -404,6 +405,61 @@ void debugFlushLastFrameImageSizeInfo() {
404405
}());
405406
}
406407

408+
/// Information that describes how to tile an image for a given [ImageRepeat]
409+
/// enum.
410+
///
411+
/// Used with [createTilingInfo].
412+
@visibleForTesting
413+
@immutable
414+
class ImageTilingInfo {
415+
/// Create a new [ImageTilingInfo] object.
416+
const ImageTilingInfo({
417+
required this.tmx,
418+
required this.tmy,
419+
required this.transform,
420+
});
421+
422+
/// The tile mode for the x-axis.
423+
final TileMode tmx;
424+
425+
/// The tile mode for the y-axis.
426+
final TileMode tmy;
427+
428+
/// The transform to apply to the image shader.
429+
final Matrix4 transform;
430+
431+
@override
432+
String toString() {
433+
if (!kDebugMode) {
434+
return 'ImageTilingInfo';
435+
}
436+
return 'ImageTilingInfo($tmx, $tmy, $transform)';
437+
}
438+
}
439+
440+
/// Create the [ImageTilingInfo] for a given [ImageRepeat], canvas [rect],
441+
/// [destinationRect], and [sourceRect].
442+
@visibleForTesting
443+
ImageTilingInfo createTilingInfo(ImageRepeat repeat, Rect rect, Rect destinationRect, Rect sourceRect) {
444+
assert(repeat != ImageRepeat.noRepeat);
445+
final TileMode tmx = (repeat == ImageRepeat.repeatX || repeat == ImageRepeat.repeat)
446+
? TileMode.repeated
447+
: TileMode.decal;
448+
final TileMode tmy = (repeat == ImageRepeat.repeatY || repeat == ImageRepeat.repeat)
449+
? TileMode.repeated
450+
: TileMode.decal;
451+
final Rect data = _generateImageTileRects(rect, destinationRect, repeat).first;
452+
final Matrix4 transform = Matrix4.identity()
453+
..scale(data.width / sourceRect.width, data.height / sourceRect.height)
454+
..setTranslationRaw(data.topLeft.dx, data.topLeft.dy, 0);
455+
456+
return ImageTilingInfo(
457+
tmx: tmx,
458+
tmy: tmy,
459+
transform: transform,
460+
);
461+
}
462+
407463
/// Paints an image into the given rectangle on the canvas.
408464
///
409465
/// The arguments have the following meanings:
@@ -626,7 +682,8 @@ void paintImage({
626682
if (needSave) {
627683
canvas.save();
628684
}
629-
if (repeat != ImageRepeat.noRepeat) {
685+
if (repeat != ImageRepeat.noRepeat && centerSlice != null) {
686+
// Don't clip if an image shader is used.
630687
canvas.clipRect(rect);
631688
}
632689
if (flipHorizontally) {
@@ -642,9 +699,12 @@ void paintImage({
642699
if (repeat == ImageRepeat.noRepeat) {
643700
canvas.drawImageRect(image, sourceRect, destinationRect, paint);
644701
} else {
645-
for (final Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) {
646-
canvas.drawImageRect(image, sourceRect, tileRect, paint);
647-
}
702+
final ImageTilingInfo info = createTilingInfo(repeat, rect, destinationRect, sourceRect);
703+
final ImageShader shader = ImageShader(image, info.tmx, info.tmy, info.transform.storage, filterQuality: filterQuality);
704+
canvas.drawRect(
705+
rect,
706+
paint..shader = shader
707+
);
648708
}
649709
} else {
650710
canvas.scale(1 / scale);
@@ -665,7 +725,7 @@ void paintImage({
665725
}
666726
}
667727

668-
Iterable<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) {
728+
List<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) {
669729
int startX = 0;
670730
int startY = 0;
671731
int stopX = 0;

packages/flutter/test/painting/decoration_test.dart

+75-37
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:fake_async/fake_async.dart';
99
import 'package:flutter/foundation.dart';
1010
import 'package:flutter/painting.dart';
1111
import 'package:flutter_test/flutter_test.dart';
12+
import 'package:vector_math/vector_math_64.dart';
1213

1314
import '../image_data.dart';
1415
import '../painting/mocks_for_image_cache.dart';
@@ -672,7 +673,8 @@ void main() {
672673
final TestCanvas canvas = TestCanvas();
673674

674675
// Paint a square image into an output rect that is twice as wide as it is
675-
// tall. Two copies of the image should be painted, one next to the other.
676+
// tall. One copy of the image should be painted, aligned so that a repeating
677+
// tile mode causes it to appear twice.
676678
const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 400.0, 200.0);
677679
final ui.Image image = await createTestImage(width: 100, height: 100);
678680

@@ -685,34 +687,29 @@ void main() {
685687
repeat: ImageRepeat.repeatX,
686688
);
687689

688-
const Size imageSize = Size(100.0, 100.0);
689-
690-
final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawImageRect).toList();
691-
final Set<Rect> tileRects = <Rect>{};
692-
693-
expect(calls, hasLength(2));
694-
for (final Invocation call in calls) {
695-
expect(call.isMethod, isTrue);
696-
expect(call.positionalArguments, hasLength(4));
690+
final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawRect).toList();
697691

698-
expect(call.positionalArguments[0], isA<ui.Image>());
699-
700-
// sourceRect should contain all pixels of the source image
701-
expect(call.positionalArguments[1], Offset.zero & imageSize);
692+
expect(calls, hasLength(1));
693+
final Invocation call = calls[0];
694+
expect(call.isMethod, isTrue);
695+
expect(call.positionalArguments, hasLength(2));
702696

703-
tileRects.add(call.positionalArguments[2] as Rect);
697+
// A tiled image is drawn as a rect with a shader.
698+
expect(call.positionalArguments[0], isA<Rect>());
699+
expect(call.positionalArguments[1], isA<Paint>());
704700

705-
expect(call.positionalArguments[3], isA<Paint>());
706-
}
701+
final Paint paint = call.positionalArguments[1] as Paint;
707702

708-
expect(tileRects, <Rect>{const Rect.fromLTWH(30.0, 30.0, 200.0, 200.0), const Rect.fromLTWH(230.0, 30.0, 200.0, 200.0)});
703+
expect(paint.shader, isA<ImageShader>());
704+
expect(call.positionalArguments[0], outputRect);
709705
});
710706

711707
test('paintImage with repeatY and fitWidth', () async {
712708
final TestCanvas canvas = TestCanvas();
713709

714710
// Paint a square image into an output rect that is twice as tall as it is
715-
// wide. Two copies of the image should be painted, one above the other.
711+
// wide. One copy of the image should be painted, aligned so that a repeating
712+
// tile mode causes it to appear twice.
716713
const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 200.0, 400.0);
717714
final ui.Image image = await createTestImage(width: 100, height: 100);
718715

@@ -724,28 +721,21 @@ void main() {
724721
fit: BoxFit.fitWidth,
725722
repeat: ImageRepeat.repeatY,
726723
);
724+
final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawRect).toList();
727725

728-
const Size imageSize = Size(100.0, 100.0);
729-
730-
final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawImageRect).toList();
731-
final Set<Rect> tileRects = <Rect>{};
732-
733-
expect(calls, hasLength(2));
734-
for (final Invocation call in calls) {
735-
expect(call.isMethod, isTrue);
736-
expect(call.positionalArguments, hasLength(4));
737-
738-
expect(call.positionalArguments[0], isA<ui.Image>());
739-
740-
// sourceRect should contain all pixels of the source image
741-
expect(call.positionalArguments[1], Offset.zero & imageSize);
726+
expect(calls, hasLength(1));
727+
final Invocation call = calls[0];
728+
expect(call.isMethod, isTrue);
729+
expect(call.positionalArguments, hasLength(2));
742730

743-
tileRects.add(call.positionalArguments[2] as Rect);
731+
// A tiled image is drawn as a rect with a shader.
732+
expect(call.positionalArguments[0], isA<Rect>());
733+
expect(call.positionalArguments[1], isA<Paint>());
744734

745-
expect(call.positionalArguments[3], isA<Paint>());
746-
}
735+
final Paint paint = call.positionalArguments[1] as Paint;
747736

748-
expect(tileRects, <Rect>{const Rect.fromLTWH(30.0, 30.0, 200.0, 200.0), const Rect.fromLTWH(30.0, 230.0, 200.0, 200.0)});
737+
expect(paint.shader, isA<ImageShader>());
738+
expect(call.positionalArguments[0], outputRect);
749739
});
750740

751741
test('DecorationImage scale test', () async {
@@ -794,4 +784,52 @@ void main() {
794784

795785
info.dispose();
796786
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442
787+
788+
test('Compute image tiling', () {
789+
expect(() => createTilingInfo(ImageRepeat.noRepeat, Rect.zero, Rect.zero, Rect.zero), throwsAssertionError);
790+
791+
// These tests draw a 16x9 image into a 100x50 container with a destination
792+
// size of and make assertions based on observed behavior and the original
793+
// rectangles from https://github.com/flutter/flutter/pull/119495/
794+
795+
final ImageTilingInfo repeatX = createTilingInfo(
796+
ImageRepeat.repeatX,
797+
const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0),
798+
const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0),
799+
const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0),
800+
);
801+
802+
expect(repeatX.tmx, TileMode.repeated);
803+
expect(repeatX.tmy, TileMode.decal);
804+
expect(repeatX.transform, matrixMoreOrLessEquals(Matrix4.identity()
805+
..scale(1.0, 1.0)
806+
..setTranslationRaw(-12.0, 0.0, 0.0)
807+
));
808+
809+
final ImageTilingInfo repeatY = createTilingInfo(
810+
ImageRepeat.repeatY,
811+
const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0),
812+
const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0),
813+
const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0),
814+
);
815+
expect(repeatY.tmx, TileMode.decal);
816+
expect(repeatY.tmy, TileMode.repeated);
817+
expect(repeatY.transform, matrixMoreOrLessEquals(Matrix4.identity()
818+
..scale(1.0, 1.0)
819+
..setTranslationRaw(84.0, 0.0, 0.0)
820+
));
821+
822+
final ImageTilingInfo repeat = createTilingInfo(
823+
ImageRepeat.repeat,
824+
const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0),
825+
const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0),
826+
const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0),
827+
);
828+
expect(repeat.tmx, TileMode.repeated);
829+
expect(repeat.tmy, TileMode.repeated);
830+
expect(repeat.transform, matrixMoreOrLessEquals(Matrix4.identity()
831+
..scale(1.0, 1.0)
832+
..setTranslationRaw(-12.0, 0.0, 0.0)
833+
));
834+
});
797835
}

packages/flutter/test/rendering/mock_canvas.dart

+12-4
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ abstract class PaintPattern {
193193
/// painting has completed, not at the time of the call. If the same [Paint]
194194
/// object is reused multiple times, then this may not match the actual
195195
/// arguments as they were seen by the method.
196-
void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
196+
void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style, Matcher? shader });
197197

198198
/// Indicates that a rounded rectangle clip is expected next.
199199
///
@@ -734,8 +734,8 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp
734734
}
735735

736736
@override
737-
void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
738-
_predicates.add(_RectPaintPredicate(rect: rect, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
737+
void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style, Matcher? shader }) {
738+
_predicates.add(_RectPaintPredicate(rect: rect, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style, shader: shader));
739739
}
740740

741741
@override
@@ -891,6 +891,7 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate {
891891
this.strokeWidth,
892892
this.hasMaskFilter,
893893
this.style,
894+
this.shader,
894895
});
895896

896897
final Symbol symbol;
@@ -901,6 +902,7 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate {
901902
final double? strokeWidth;
902903
final bool? hasMaskFilter;
903904
final PaintingStyle? style;
905+
final Matcher? shader;
904906

905907
String get methodName => _symbolName(symbol);
906908

@@ -935,6 +937,9 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate {
935937
if (style != null && paintArgument.style != style) {
936938
throw 'It called $methodName with a paint whose style, ${paintArgument.style}, was not exactly the expected style ($style).';
937939
}
940+
if (shader != null && !shader!.matches(paintArgument.shader, <dynamic, dynamic>{})) {
941+
throw 'It called $methodName with a paint whose shader, ${paintArgument.shader}, was not exactly the expected shader ($shader).';
942+
}
938943
}
939944

940945
@override
@@ -975,6 +980,7 @@ class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate {
975980
required double? strokeWidth,
976981
required bool? hasMaskFilter,
977982
required PaintingStyle? style,
983+
Matcher? shader,
978984
}) : super(
979985
symbol,
980986
name,
@@ -984,6 +990,7 @@ class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate {
984990
strokeWidth: strokeWidth,
985991
hasMaskFilter: hasMaskFilter,
986992
style: style,
993+
shader: shader,
987994
);
988995

989996
final T? expected;
@@ -1069,14 +1076,15 @@ class _TwoParameterPaintPredicate<T1, T2> extends _DrawCommandPaintPredicate {
10691076
}
10701077

10711078
class _RectPaintPredicate extends _OneParameterPaintPredicate<Rect> {
1072-
_RectPaintPredicate({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
1079+
_RectPaintPredicate({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style, Matcher? shader }) : super(
10731080
#drawRect,
10741081
'a rectangle',
10751082
expected: rect,
10761083
color: color,
10771084
strokeWidth: strokeWidth,
10781085
hasMaskFilter: hasMaskFilter,
10791086
style: style,
1087+
shader: shader,
10801088
);
10811089
}
10821090

0 commit comments

Comments
 (0)