[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
diff --git a/packages/flutter/lib/src/painting/decoration_image.dart b/packages/flutter/lib/src/painting/decoration_image.dart
index 92cdecc..2b11996 100644
--- a/packages/flutter/lib/src/painting/decoration_image.dart
+++ b/packages/flutter/lib/src/painting/decoration_image.dart
@@ -8,6 +8,7 @@
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/scheduler.dart';
+import 'package:vector_math/vector_math_64.dart';
 
 import 'alignment.dart';
 import 'basic_types.dart';
@@ -404,6 +405,61 @@
   }());
 }
 
+/// Information that describes how to tile an image for a given [ImageRepeat]
+/// enum.
+///
+/// Used with [createTilingInfo].
+@visibleForTesting
+@immutable
+class ImageTilingInfo {
+  /// Create a new [ImageTilingInfo] object.
+  const ImageTilingInfo({
+    required this.tmx,
+    required this.tmy,
+    required this.transform,
+  });
+
+  /// The tile mode for the x-axis.
+  final TileMode tmx;
+
+  /// The tile mode for the y-axis.
+  final TileMode tmy;
+
+  /// The transform to apply to the image shader.
+  final Matrix4 transform;
+
+  @override
+  String toString() {
+    if (!kDebugMode) {
+      return 'ImageTilingInfo';
+    }
+    return 'ImageTilingInfo($tmx, $tmy, $transform)';
+  }
+}
+
+/// Create the [ImageTilingInfo] for a given [ImageRepeat], canvas [rect],
+/// [destinationRect], and [sourceRect].
+@visibleForTesting
+ImageTilingInfo createTilingInfo(ImageRepeat repeat, Rect rect, Rect destinationRect, Rect sourceRect) {
+  assert(repeat != ImageRepeat.noRepeat);
+  final TileMode tmx = (repeat == ImageRepeat.repeatX || repeat == ImageRepeat.repeat)
+    ? TileMode.repeated
+    : TileMode.decal;
+  final TileMode tmy = (repeat == ImageRepeat.repeatY || repeat == ImageRepeat.repeat)
+    ? TileMode.repeated
+    : TileMode.decal;
+  final Rect data = _generateImageTileRects(rect, destinationRect, repeat).first;
+  final Matrix4 transform = Matrix4.identity()
+    ..scale(data.width / sourceRect.width, data.height / sourceRect.height)
+    ..setTranslationRaw(data.topLeft.dx, data.topLeft.dy, 0);
+
+  return ImageTilingInfo(
+    tmx: tmx,
+    tmy: tmy,
+    transform: transform,
+  );
+}
+
 /// Paints an image into the given rectangle on the canvas.
 ///
 /// The arguments have the following meanings:
@@ -626,7 +682,8 @@
   if (needSave) {
     canvas.save();
   }
-  if (repeat != ImageRepeat.noRepeat) {
+  if (repeat != ImageRepeat.noRepeat && centerSlice != null) {
+    // Don't clip if an image shader is used.
     canvas.clipRect(rect);
   }
   if (flipHorizontally) {
@@ -642,9 +699,12 @@
     if (repeat == ImageRepeat.noRepeat) {
       canvas.drawImageRect(image, sourceRect, destinationRect, paint);
     } else {
-      for (final Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) {
-        canvas.drawImageRect(image, sourceRect, tileRect, paint);
-      }
+      final ImageTilingInfo info = createTilingInfo(repeat, rect, destinationRect, sourceRect);
+      final ImageShader shader = ImageShader(image, info.tmx, info.tmy, info.transform.storage, filterQuality: filterQuality);
+      canvas.drawRect(
+        rect,
+        paint..shader = shader
+      );
     }
   } else {
     canvas.scale(1 / scale);
@@ -665,7 +725,7 @@
   }
 }
 
-Iterable<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) {
+List<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) {
   int startX = 0;
   int startY = 0;
   int stopX = 0;
diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart
index 65026f6..de257e1 100644
--- a/packages/flutter/test/painting/decoration_test.dart
+++ b/packages/flutter/test/painting/decoration_test.dart
@@ -9,6 +9,7 @@
 import 'package:flutter/foundation.dart';
 import 'package:flutter/painting.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:vector_math/vector_math_64.dart';
 
 import '../image_data.dart';
 import '../painting/mocks_for_image_cache.dart';
@@ -672,7 +673,8 @@
     final TestCanvas canvas = TestCanvas();
 
     // Paint a square image into an output rect that is twice as wide as it is
-    // tall.  Two copies of the image should be painted, one next to the other.
+    // tall. One copy of the image should be painted, aligned so that a repeating
+    // tile mode causes it to appear twice.
     const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 400.0, 200.0);
     final ui.Image image = await createTestImage(width: 100, height: 100);
 
@@ -685,34 +687,29 @@
       repeat: ImageRepeat.repeatX,
     );
 
-    const Size imageSize = Size(100.0, 100.0);
+    final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawRect).toList();
 
-    final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawImageRect).toList();
-    final Set<Rect> tileRects = <Rect>{};
+    expect(calls, hasLength(1));
+    final Invocation call = calls[0];
+    expect(call.isMethod, isTrue);
+    expect(call.positionalArguments, hasLength(2));
 
-    expect(calls, hasLength(2));
-    for (final Invocation call in calls) {
-      expect(call.isMethod, isTrue);
-      expect(call.positionalArguments, hasLength(4));
+    // A tiled image is drawn as a rect with a shader.
+    expect(call.positionalArguments[0], isA<Rect>());
+    expect(call.positionalArguments[1], isA<Paint>());
 
-      expect(call.positionalArguments[0], isA<ui.Image>());
+    final Paint paint = call.positionalArguments[1] as Paint;
 
-      // sourceRect should contain all pixels of the source image
-      expect(call.positionalArguments[1], Offset.zero & imageSize);
-
-      tileRects.add(call.positionalArguments[2] as Rect);
-
-      expect(call.positionalArguments[3], isA<Paint>());
-    }
-
-    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)});
+    expect(paint.shader, isA<ImageShader>());
+    expect(call.positionalArguments[0], outputRect);
   });
 
   test('paintImage with repeatY and fitWidth', () async {
     final TestCanvas canvas = TestCanvas();
 
     // Paint a square image into an output rect that is twice as tall as it is
-    // wide.  Two copies of the image should be painted, one above the other.
+    // wide.  One copy of the image should be painted, aligned so that a repeating
+    // tile mode causes it to appear twice.
     const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 200.0, 400.0);
     final ui.Image image = await createTestImage(width: 100, height: 100);
 
@@ -724,28 +721,21 @@
       fit: BoxFit.fitWidth,
       repeat: ImageRepeat.repeatY,
     );
+    final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawRect).toList();
 
-    const Size imageSize = Size(100.0, 100.0);
+    expect(calls, hasLength(1));
+    final Invocation call = calls[0];
+    expect(call.isMethod, isTrue);
+    expect(call.positionalArguments, hasLength(2));
 
-    final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawImageRect).toList();
-    final Set<Rect> tileRects = <Rect>{};
+    // A tiled image is drawn as a rect with a shader.
+    expect(call.positionalArguments[0], isA<Rect>());
+    expect(call.positionalArguments[1], isA<Paint>());
 
-    expect(calls, hasLength(2));
-    for (final Invocation call in calls) {
-      expect(call.isMethod, isTrue);
-      expect(call.positionalArguments, hasLength(4));
+    final Paint paint = call.positionalArguments[1] as Paint;
 
-      expect(call.positionalArguments[0], isA<ui.Image>());
-
-      // sourceRect should contain all pixels of the source image
-      expect(call.positionalArguments[1], Offset.zero & imageSize);
-
-      tileRects.add(call.positionalArguments[2] as Rect);
-
-      expect(call.positionalArguments[3], isA<Paint>());
-    }
-
-    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)});
+    expect(paint.shader, isA<ImageShader>());
+    expect(call.positionalArguments[0], outputRect);
   });
 
   test('DecorationImage scale test', () async {
@@ -794,4 +784,52 @@
 
     info.dispose();
   }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442
+
+  test('Compute image tiling', () {
+    expect(() => createTilingInfo(ImageRepeat.noRepeat, Rect.zero, Rect.zero, Rect.zero), throwsAssertionError);
+
+    // These tests draw a 16x9 image into a 100x50 container with a destination
+    // size of and make assertions based on observed behavior and the original
+    // rectangles from https://github.com/flutter/flutter/pull/119495/
+
+    final ImageTilingInfo repeatX = createTilingInfo(
+      ImageRepeat.repeatX,
+      const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0),
+      const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0),
+      const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0),
+    );
+
+    expect(repeatX.tmx, TileMode.repeated);
+    expect(repeatX.tmy, TileMode.decal);
+    expect(repeatX.transform, matrixMoreOrLessEquals(Matrix4.identity()
+      ..scale(1.0, 1.0)
+      ..setTranslationRaw(-12.0, 0.0, 0.0)
+    ));
+
+    final ImageTilingInfo repeatY = createTilingInfo(
+      ImageRepeat.repeatY,
+      const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0),
+      const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0),
+      const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0),
+    );
+    expect(repeatY.tmx, TileMode.decal);
+    expect(repeatY.tmy, TileMode.repeated);
+    expect(repeatY.transform, matrixMoreOrLessEquals(Matrix4.identity()
+      ..scale(1.0, 1.0)
+      ..setTranslationRaw(84.0, 0.0, 0.0)
+    ));
+
+    final ImageTilingInfo repeat = createTilingInfo(
+      ImageRepeat.repeat,
+      const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0),
+      const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0),
+      const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0),
+    );
+    expect(repeat.tmx, TileMode.repeated);
+    expect(repeat.tmy, TileMode.repeated);
+    expect(repeat.transform, matrixMoreOrLessEquals(Matrix4.identity()
+      ..scale(1.0, 1.0)
+      ..setTranslationRaw(-12.0, 0.0, 0.0)
+    ));
+  });
 }
diff --git a/packages/flutter/test/rendering/mock_canvas.dart b/packages/flutter/test/rendering/mock_canvas.dart
index 3ae1388..2f53a8b 100644
--- a/packages/flutter/test/rendering/mock_canvas.dart
+++ b/packages/flutter/test/rendering/mock_canvas.dart
@@ -193,7 +193,7 @@
   /// painting has completed, not at the time of the call. If the same [Paint]
   /// object is reused multiple times, then this may not match the actual
   /// arguments as they were seen by the method.
-  void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style });
+  void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style, Matcher? shader });
 
   /// Indicates that a rounded rectangle clip is expected next.
   ///
@@ -734,8 +734,8 @@
   }
 
   @override
-  void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) {
-    _predicates.add(_RectPaintPredicate(rect: rect, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
+  void rect({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style, Matcher? shader }) {
+    _predicates.add(_RectPaintPredicate(rect: rect, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style, shader: shader));
   }
 
   @override
@@ -891,6 +891,7 @@
     this.strokeWidth,
     this.hasMaskFilter,
     this.style,
+    this.shader,
   });
 
   final Symbol symbol;
@@ -901,6 +902,7 @@
   final double? strokeWidth;
   final bool? hasMaskFilter;
   final PaintingStyle? style;
+  final Matcher? shader;
 
   String get methodName => _symbolName(symbol);
 
@@ -935,6 +937,9 @@
     if (style != null && paintArgument.style != style) {
       throw 'It called $methodName with a paint whose style, ${paintArgument.style}, was not exactly the expected style ($style).';
     }
+    if (shader != null && !shader!.matches(paintArgument.shader, <dynamic, dynamic>{})) {
+      throw 'It called $methodName with a paint whose shader, ${paintArgument.shader}, was not exactly the expected shader ($shader).';
+    }
   }
 
   @override
@@ -975,6 +980,7 @@
     required double? strokeWidth,
     required bool? hasMaskFilter,
     required PaintingStyle? style,
+    Matcher? shader,
   })  : super(
           symbol,
           name,
@@ -984,6 +990,7 @@
           strokeWidth: strokeWidth,
           hasMaskFilter: hasMaskFilter,
           style: style,
+          shader: shader,
         );
 
   final T? expected;
@@ -1069,7 +1076,7 @@
 }
 
 class _RectPaintPredicate extends _OneParameterPaintPredicate<Rect> {
-  _RectPaintPredicate({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super(
+  _RectPaintPredicate({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style, Matcher? shader }) : super(
     #drawRect,
     'a rectangle',
     expected: rect,
@@ -1077,6 +1084,7 @@
     strokeWidth: strokeWidth,
     hasMaskFilter: hasMaskFilter,
     style: style,
+    shader: shader,
   );
 }
 
diff --git a/packages/flutter/test/widgets/image_rtl_test.dart b/packages/flutter/test/widgets/image_rtl_test.dart
index 6d574d5..3b2572b 100644
--- a/packages/flutter/test/widgets/image_rtl_test.dart
+++ b/packages/flutter/test/widgets/image_rtl_test.dart
@@ -56,17 +56,10 @@
       EnginePhase.layout, // so that we don't try to paint the fake images
     );
     expect(find.byType(Container), paints
-      ..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0))
       ..translate(x: 50.0, y: 0.0)
       ..scale(x: -1.0, y: 1.0)
       ..translate(x: -50.0, y: 0.0)
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(4.0, 0.0, 20.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(20.0, 0.0, 36.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(36.0, 0.0, 52.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(52.0, 0.0, 68.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(68.0, 0.0, 84.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0))
+      ..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0), shader: isA<ImageShader>())
       ..restore(),
     );
     expect(find.byType(Container), isNot(paints..scale()..scale()));
@@ -95,14 +88,7 @@
       EnginePhase.layout, // so that we don't try to paint the fake images
     );
     expect(find.byType(Container), paints
-      ..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(4.0, 0.0, 20.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(20.0, 0.0, 36.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(36.0, 0.0, 52.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(52.0, 0.0, 68.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(68.0, 0.0, 84.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0))
+      ..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0), shader: isA<ImageShader>())
       ..restore(),
     );
     expect(find.byType(Container), isNot(paints..scale()));
@@ -130,14 +116,7 @@
       EnginePhase.layout, // so that we don't try to paint the fake images
     );
     expect(find.byType(Container), paints
-      ..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(16.0, 0.0, 32.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(32.0, 0.0, 48.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(48.0, 0.0, 64.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(64.0, 0.0, 80.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(80.0, 0.0, 96.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(96.0, 0.0, 112.0, 9.0))
+      ..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0), shader: isA<ImageShader>())
       ..restore(),
     );
     expect(find.byType(Container), isNot(paints..scale()));
@@ -165,14 +144,7 @@
       EnginePhase.layout, // so that we don't try to paint the fake images
     );
     expect(find.byType(Container), paints
-      ..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(4.0, 0.0, 20.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(20.0, 0.0, 36.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(36.0, 0.0, 52.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(52.0, 0.0, 68.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(68.0, 0.0, 84.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0))
+      ..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0), shader: isA<ImageShader>())
       ..restore(),
     );
     expect(find.byType(Container), isNot(paints..scale()));
@@ -314,17 +286,10 @@
       EnginePhase.layout, // so that we don't try to paint the fake images
     );
     expect(find.byType(SizedBox), paints
-      ..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0))
       ..translate(x: 50.0, y: 0.0)
       ..scale(x: -1.0, y: 1.0)
       ..translate(x: -50.0, y: 0.0)
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(4.0, 0.0, 20.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(20.0, 0.0, 36.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(36.0, 0.0, 52.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(52.0, 0.0, 68.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(68.0, 0.0, 84.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0))
+      ..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0), shader: isA<ImageShader>())
       ..restore(),
     );
     expect(find.byType(SizedBox), isNot(paints..scale()..scale()));
@@ -351,14 +316,7 @@
       EnginePhase.layout, // so that we don't try to paint the fake images
     );
     expect(find.byType(SizedBox), paints
-      ..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(4.0, 0.0, 20.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(20.0, 0.0, 36.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(36.0, 0.0, 52.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(52.0, 0.0, 68.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(68.0, 0.0, 84.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0))
+      ..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0), shader: isA<ImageShader>())
       ..restore(),
     );
     expect(find.byType(SizedBox), isNot(paints..scale()));
@@ -384,14 +342,7 @@
       EnginePhase.layout, // so that we don't try to paint the fake images
     );
     expect(find.byType(SizedBox), paints
-      ..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(16.0, 0.0, 32.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(32.0, 0.0, 48.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(48.0, 0.0, 64.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(64.0, 0.0, 80.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(80.0, 0.0, 96.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(96.0, 0.0, 112.0, 9.0))
+      ..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0), shader: isA<ImageShader>())
       ..restore(),
     );
     expect(find.byType(SizedBox), isNot(paints..scale()));
@@ -417,14 +368,7 @@
       EnginePhase.layout, // so that we don't try to paint the fake images
     );
     expect(find.byType(SizedBox), paints
-      ..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(-12.0, 0.0, 4.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(4.0, 0.0, 20.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(20.0, 0.0, 36.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(36.0, 0.0, 52.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(52.0, 0.0, 68.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(68.0, 0.0, 84.0, 9.0))
-      ..drawImageRect(source: const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0), destination: const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0))
+      ..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0), shader: isA<ImageShader>())
       ..restore(),
     );
     expect(find.byType(SizedBox), isNot(paints..scale()));