Flutter BoxFit: contain, cover, fill and the rest
BoxFit.contain is aspect fit, BoxFit.cover is aspect fill and BoxFit.fill stretches. If you leave fit out, an Image uses scaleDown, which shrinks large images and never enlarges small ones.
The seven values
| BoxFit | Scale | Result |
|---|---|---|
fill | width and height separately | Fills the box, distorts the ratio. |
contain | min(boxW / imgW, boxH / imgH) | As large as possible while showing the whole image. |
cover | max(boxW / imgW, boxH / imgH) | As small as possible while covering the whole box. |
fitWidth | boxW / imgW | Full width shown; vertically cropped or with space. |
fitHeight | boxH / imgH | Full height shown; horizontally cropped or with space. |
none | 1 | Natural size, aligned, the rest discarded. |
scaleDown | min(1, contain scale) | Like contain if that shrinks the image, otherwise like none. |
SizedBox(
width: double.infinity,
height: 220,
child: Image.asset('assets/harbor.jpg', fit: BoxFit.cover),
)Why the default is scaleDown
Image.fit is nullable, and its documentation points to paintImage, which applies fit ??= centerSlice == null ? BoxFit.scaleDown : BoxFit.fill. So an Image without fit shrinks a large picture to fit but leaves a small one at its natural size, centered. That is why a small asset inside a large box looks "not scaled": pass fit: BoxFit.contain to enlarge it. DecorationImage follows the same rule. FittedBox is different: its fit defaults to BoxFit.contain.
BoxFit needs a box
An Image sizes itself to the image when its parent allows any size, and then every fit looks the same. Give it tight constraints: a SizedBox, width and height on the Image itself, an Expanded, or an AspectRatio:
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(photoUrl, fit: BoxFit.cover),
)cover in Image vs FittedBox
For an Image, Flutter works out which part of the source is visible (the "source size" from applyBoxFit) and paints only that part into the box, so cover never spills out. FittedBox scales a child widget instead, and its clipBehavior defaults to Clip.none; with BoxFit.cover the child is drawn beyond the box unless you clip:
FittedBox(
fit: BoxFit.cover,
clipBehavior: Clip.hardEdge, // without this the overflow is painted
child: SizedBox(width: 1200, height: 800, child: MapPreview()),
)For rounded corners on an image, wrap it in ClipRRect(borderRadius: BorderRadius.circular(12), child: …).
Alignment, and how it maps to an anchor
alignment defaults to Alignment.center. Flutter's Alignment(x, y) runs from −1 to 1, so the offset of the drawn image is:
offsetX = (boxW - drawnW) * (x + 1) / 2
offsetY = (boxH - drawnH) * (y + 1) / 2Alignment.topCenter is (0, −1): with cover it keeps the top of the image and crops from the bottom. This is the same as an anchor of 0.5, 0 in the visualizer, or object-position: center top in CSS.
Image.asset(
'assets/portrait.jpg',
fit: BoxFit.cover,
alignment: Alignment.topCenter,
width: double.infinity,
height: 200,
)Background images with DecorationImage
A BoxDecoration can paint an image behind a container's child, with the same fit and alignment:
Container(
height: 180,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/harbor.jpg'),
fit: BoxFit.cover,
),
),
child: const Align(
alignment: Alignment.bottomLeft,
child: Padding(padding: EdgeInsets.all(16), child: Text('Harbor')),
),
)applyBoxFit for custom painting
In a CustomPainter, applyBoxFit gives you the two sizes Flutter uses internally: the part of the source to read and the size to draw it at. Alignment.inscribe places a size inside a rectangle:
void paintFitted(Canvas canvas, Rect box, ui.Image image) {
final imageSize = Size(image.width.toDouble(), image.height.toDouble());
final fitted = applyBoxFit(BoxFit.cover, imageSize, box.size);
final src = Alignment.center.inscribe(fitted.source, Offset.zero & imageSize);
final dst = Alignment.center.inscribe(fitted.destination, box);
canvas.drawImageRect(image, src, dst, Paint());
}For cover the destination is the whole box and the source is the visible part of the image, which is the "visible source" figure the visualizer reports.
Logical pixels and asset variants
none and scaleDown depend on the image's natural size in logical pixels. With resolution-aware assets (2.0x/, 3.0x/ folders) Flutter divides the pixel size by the variant's scale, so a 1200-pixel-wide file in 3.0x/ has a natural width of 400. The other fits only use the ratio.
The same seven behaviors in other toolkits are compared on the home page; for Android's version see scaleType and ContentScale.
Questions
What is the difference between BoxFit.contain and BoxFit.cover?
contain scales by the smaller of the two ratios so the whole image is visible inside the box. cover scales by the larger ratio so the box is completely covered and the image's overflow is not shown.
What BoxFit does Image use by default?
If fit is null, Image paints with BoxFit.scaleDown (or BoxFit.fill when centerSlice is set). FittedBox defaults to BoxFit.contain.
Why does BoxFit.cover overflow in a FittedBox?
FittedBox does not clip by default (clipBehavior is Clip.none). Set clipBehavior: Clip.hardEdge. An Image with fit: BoxFit.cover does not need this because it only paints the visible part of the source.
Want your own numbers? The visualizer draws any box and image, shows each step of the math and writes the code for six toolkits.