aspectfit

Android scaleType and Compose ContentScale

An ImageView uses fitCenter unless you say otherwise: aspect fit, centered. centerCrop is aspect fill, fitXY stretches. In Jetpack Compose the same choices are ContentScale.Fit, Crop and FillBounds.

The eight ImageView.ScaleType values

All measurements are inside the view's padding. The drawable's intrinsic size is in pixels after Android has applied its density scaling.

XML valueEnumKeeps ratioWhat it does
fitCenterFIT_CENTERyesAspect fit, centered. The default.
fitStartFIT_STARTyesAspect fit, aligned to the left and top edges.
fitEndFIT_ENDyesAspect fit, aligned to the right and bottom edges.
centerCropCENTER_CROPyesAspect fill: both sides equal to or larger than the view, centered, overflow cropped.
centerInsideCENTER_INSIDEyesAspect fit, but never enlarges: a small image stays at its own size.
centerCENTERyesNo scaling, centered; cropped if larger than the view.
fitXYFIT_XYnoStretches to exactly the view's size.
matrixMATRIXyou decideDraws with the matrix from setImageMatrix(). With the identity matrix: natural size, top-left.
fit / contain
3003003002005050
fill / cover
300300450300
scale-down
3003003002005050
none
3003001200800−450−450−250−250
stretch / fill
300300300300
fitCenter, centerCrop, centerInside, center and fitXY for a 1200 × 800 drawable in a 300 × 300 view.

The formulas behind them: fitCenter scales by min(viewW / imgW, viewH / imgH), centerCrop by max(…), and centerInside by min(1, viewW / imgW, viewH / imgH). For center, centerCrop and centerInside, ImageView rounds the offset to whole pixels, so when the leftover space is an odd number of pixels, one side gets the extra pixel.

XML
<ImageView
    android:id="@+id/cover"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:src="@drawable/harbor"
    android:contentDescription="@string/harbor_description"
    android:scaleType="centerCrop" />

In code: imageView.scaleType = ImageView.ScaleType.CENTER_CROP.

adjustViewBounds, and the trap in it

android:adjustViewBounds="true" lets the ImageView resize itself to the drawable's aspect ratio. It is the usual way to show an image full-width at its natural shape: fixed width, wrap_content height, and the view becomes exactly as tall as the image needs, with no empty bands.

XML
<ImageView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:adjustViewBounds="true"
    android:src="@drawable/harbor"
    android:contentDescription="@string/harbor_description" />

Three details are worth knowing:

  • It changes the scale type when set from code. setAdjustViewBounds(true) also calls setScaleType(ScaleType.FIT_CENTER) (see the AOSP source of ImageView). If you set a scale type first and then turn on adjustViewBounds in code, your scale type is gone. In XML the scaleType attribute is applied after adjustViewBounds, so it wins there.
  • android:maxWidth and android:maxHeight only take effect together with adjustViewBounds.
  • For apps targeting API level 17 or lower, adjustViewBounds can shrink the view to the drawable but does not always grow it to fill the available space.

Padding and cropToPadding

The scale type works on the area inside the padding. With centerCrop the overflow can still be drawn into the padding area, because by default the drawing is only clipped at the view's outer edge. Set android:cropToPadding="true" to clip at the padding instead.

A custom crop with MATRIX

centerCrop always crops equally from both sides. For a header where the top of a portrait must stay visible, compute the same scale yourself and choose where the leftover goes:

Kotlin
fun ImageView.cropAnchored(anchorX: Float = 0.5f, anchorY: Float = 0f) {
    scaleType = ImageView.ScaleType.MATRIX
    doOnLayout {
        val d = drawable ?: return@doOnLayout
        val vw = (width - paddingLeft - paddingRight).toFloat()
        val vh = (height - paddingTop - paddingBottom).toFloat()
        val s = maxOf(vw / d.intrinsicWidth, vh / d.intrinsicHeight)   // aspect fill
        imageMatrix = Matrix().apply {
            setScale(s, s)
            postTranslate((vw - d.intrinsicWidth * s) * anchorX,
                          (vh - d.intrinsicHeight * s) * anchorY)
        }
    }
}

// keep the top, crop only from the bottom
headerImage.cropAnchored(anchorY = 0f)

doOnLayout is in androidx.core (core-ktx). Use minOf instead of maxOf for an anchored aspect fit. Call it again whenever the drawable changes.

Jetpack Compose: ContentScale

In Compose, Image takes a contentScale parameter whose default is ContentScale.Fit, and an alignment whose default is Alignment.Center.

ContentScaleBehaviorClosest ImageView value
FitAspect fit (the default)fitCenter
CropAspect fill, overflow croppedcenterCrop
FillBoundsStretch, ratio ignoredfitXY
InsideFit if larger, natural size if smallercenterInside
NoneNatural sizecenter
FillWidthScale so the width matches; height may crop or leave spaceneeds a matrix
FillHeightScale so the height matches; width may crop or leave spaceneeds a matrix
Kotlin
Image(
    painter = painterResource(R.drawable.harbor),
    contentDescription = stringResource(R.string.harbor_description),
    modifier = Modifier
        .fillMaxWidth()
        .aspectRatio(16f / 9f),
    contentScale = ContentScale.Crop,
    alignment = Alignment.TopCenter,      // anchor the crop, no matrix needed
)

Two differences from ImageView make Compose easier: Image clips to its bounds itself, and alignment anchors any scale mode, including Crop. Modifier.aspectRatio(ratio) gives the layout box a ratio, much like CSS aspect-ratio. Image loaders such as Coil's AsyncImage take the same contentScale parameter.

A note on density and "natural size"

center, centerInside, ContentScale.None and Inside depend on the drawable's intrinsic size, which Android scales by density bucket: a 300-pixel bitmap in drawable-mdpi is drawn 900 pixels wide on an xxhdpi screen. Put images in drawable-nodpi if they must not be scaled, or use the fitting modes, which do not care about the intrinsic size beyond its ratio.

The visualizer shows each mode with your own numbers and writes the XML or Compose code; for the same modes in iOS terms see aspect fit vs aspect fill.

Questions

What is the default scaleType of an Android ImageView?

fitCenter (ScaleType.FIT_CENTER): the image is scaled to fit inside the view with its aspect ratio kept, and centered.

What is the difference between centerCrop and fitCenter?

fitCenter scales by the smaller ratio so the whole image is visible, possibly with empty space. centerCrop scales by the larger ratio so the view is covered and the overflow is cropped equally from both sides.

Why did my scaleType change to fitCenter?

Calling setAdjustViewBounds(true) in code also sets the scale type to FIT_CENTER. Set the scale type after enabling adjustViewBounds, or set both in XML, where scaleType is applied last.