Skip to content

Commit 997d436

Browse files
Fix applyBoxFit's handling of fitWidth and fitHeight. (#117185)
* Fix applyBoxFit's handling of fitWidth and fitHeight. Previously, in `fitWidth` mode, if the input size had a wider aspect ratio than the output size, `applyBoxFit` would make the source rect taller than the input size in order to match the aspect ratio of the destination rect. Similarly, in `fitHeight` mode, if the input size had a taller aspect ratio than the output size, `applyBoxFit` would make the source rect wider than the input size in to match the aspect ratio of the destination rect. This is in contrast to all the other modes, which never output a source rect that's larger than the input size. Most of the time this worked as intended (since attempting to blit pixels that are outside the source image has no effect), however it meant that if a user attempted to create a `BoxDecoration` that used both `fitWidth` and `repeatY` (e.g. in an attempt to tile a background image), the image would not actually appear to repeat, since the logic in `paintImage` for determining the proper tiling stride is based on the destination image size, meaning that the entire destination rect would be covered in a single tile. This change modifies `applyBoxFit` so that in `fitWidth` mode, if the input size has a wider aspect ratio than the output size, it uses formulas that are equivalent to `contain`, whereas if the input size has a taller aspect ratio than the output size, it uses formulas that are equivalent to `cover`. And vice versa for `fitHeight` mode. This produces source and destination rects that match the behaviour specified in https://api.flutter.dev/flutter/painting/BoxFit.html. * Apply suggestions from code review Co-authored-by: Michael Goderbauer <[email protected]>
1 parent 6277520 commit 997d436

File tree

3 files changed

+106
-4
lines changed

3 files changed

+106
-4
lines changed

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,26 @@ FittedSizes applyBoxFit(BoxFit fit, Size inputSize, Size outputSize) {
166166
destinationSize = outputSize;
167167
break;
168168
case BoxFit.fitWidth:
169-
sourceSize = Size(inputSize.width, inputSize.width * outputSize.height / outputSize.width);
170-
destinationSize = Size(outputSize.width, sourceSize.height * outputSize.width / sourceSize.width);
169+
if (outputSize.width / outputSize.height > inputSize.width / inputSize.height) {
170+
// Like "cover"
171+
sourceSize = Size(inputSize.width, inputSize.width * outputSize.height / outputSize.width);
172+
destinationSize = outputSize;
173+
} else {
174+
// Like "contain"
175+
sourceSize = inputSize;
176+
destinationSize = Size(outputSize.width, sourceSize.height * outputSize.width / sourceSize.width);
177+
}
171178
break;
172179
case BoxFit.fitHeight:
173-
sourceSize = Size(inputSize.height * outputSize.width / outputSize.height, inputSize.height);
174-
destinationSize = Size(sourceSize.width * outputSize.height / sourceSize.height, outputSize.height);
180+
if (outputSize.width / outputSize.height > inputSize.width / inputSize.height) {
181+
// Like "contain"
182+
sourceSize = inputSize;
183+
destinationSize = Size(sourceSize.width * outputSize.height / sourceSize.height, outputSize.height);
184+
} else {
185+
// Like "cover"
186+
sourceSize = Size(inputSize.height * outputSize.width / outputSize.height, inputSize.height);
187+
destinationSize = outputSize;
188+
}
175189
break;
176190
case BoxFit.none:
177191
sourceSize = Size(math.min(inputSize.width, outputSize.width), math.min(inputSize.height, outputSize.height));

packages/flutter/test/painting/box_fit_test.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,18 @@ void main() {
2121
expect(result.source, equals(const Size(2000.0, 200.0)));
2222
expect(result.destination, equals(const Size(1000.0, 100.0)));
2323

24+
result = applyBoxFit(BoxFit.fitWidth, const Size(2000.0, 400.0), const Size(1000.0, 300.0));
25+
expect(result.source, equals(const Size(2000.0, 400.0)));
26+
expect(result.destination, equals(const Size(1000.0, 200.0)));
27+
2428
result = applyBoxFit(BoxFit.fitHeight, const Size(400.0, 2000.0), const Size(100.0, 1000.0));
2529
expect(result.source, equals(const Size(200.0, 2000.0)));
2630
expect(result.destination, equals(const Size(100.0, 1000.0)));
2731

32+
result = applyBoxFit(BoxFit.fitHeight, const Size(400.0, 2000.0), const Size(300.0, 1000.0));
33+
expect(result.source, equals(const Size(400.0, 2000.0)));
34+
expect(result.destination, equals(const Size(200.0, 1000.0)));
35+
2836
_testZeroAndNegativeSizes(BoxFit.fill);
2937
_testZeroAndNegativeSizes(BoxFit.contain);
3038
_testZeroAndNegativeSizes(BoxFit.cover);

packages/flutter/test/painting/decoration_test.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,86 @@ void main() {
669669
}
670670
});
671671

672+
test('paintImage with repeatX and fitHeight', () async {
673+
final TestCanvas canvas = TestCanvas();
674+
675+
// Paint a square image into an output rect that is twice as wide as it is
676+
// tall. Two copies of the image should be painted, one next to the other.
677+
const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 400.0, 200.0);
678+
final ui.Image image = await createTestImage(width: 100, height: 100);
679+
680+
paintImage(
681+
canvas: canvas,
682+
rect: outputRect,
683+
image: image,
684+
alignment: Alignment.topLeft,
685+
fit: BoxFit.fitHeight,
686+
repeat: ImageRepeat.repeatX,
687+
);
688+
689+
const Size imageSize = Size(100.0, 100.0);
690+
691+
final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawImageRect).toList();
692+
final Set<Rect> tileRects = <Rect>{};
693+
694+
expect(calls, hasLength(2));
695+
for (final Invocation call in calls) {
696+
expect(call.isMethod, isTrue);
697+
expect(call.positionalArguments, hasLength(4));
698+
699+
expect(call.positionalArguments[0], isA<ui.Image>());
700+
701+
// sourceRect should contain all pixels of the source image
702+
expect(call.positionalArguments[1], Offset.zero & imageSize);
703+
704+
tileRects.add(call.positionalArguments[2] as Rect);
705+
706+
expect(call.positionalArguments[3], isA<Paint>());
707+
}
708+
709+
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)});
710+
});
711+
712+
test('paintImage with repeatY and fitWidth', () async {
713+
final TestCanvas canvas = TestCanvas();
714+
715+
// Paint a square image into an output rect that is twice as tall as it is
716+
// wide. Two copies of the image should be painted, one above the other.
717+
const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 200.0, 400.0);
718+
final ui.Image image = await createTestImage(width: 100, height: 100);
719+
720+
paintImage(
721+
canvas: canvas,
722+
rect: outputRect,
723+
image: image,
724+
alignment: Alignment.topLeft,
725+
fit: BoxFit.fitWidth,
726+
repeat: ImageRepeat.repeatY,
727+
);
728+
729+
const Size imageSize = Size(100.0, 100.0);
730+
731+
final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawImageRect).toList();
732+
final Set<Rect> tileRects = <Rect>{};
733+
734+
expect(calls, hasLength(2));
735+
for (final Invocation call in calls) {
736+
expect(call.isMethod, isTrue);
737+
expect(call.positionalArguments, hasLength(4));
738+
739+
expect(call.positionalArguments[0], isA<ui.Image>());
740+
741+
// sourceRect should contain all pixels of the source image
742+
expect(call.positionalArguments[1], Offset.zero & imageSize);
743+
744+
tileRects.add(call.positionalArguments[2] as Rect);
745+
746+
expect(call.positionalArguments[3], isA<Paint>());
747+
}
748+
749+
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)});
750+
});
751+
672752
test('DecorationImage scale test', () async {
673753
final ui.Image image = await createTestImage(width: 100, height: 100);
674754
final DecorationImage backgroundImage = DecorationImage(

0 commit comments

Comments
 (0)