Circular ImageView in Android: Compose, Coil & ShapeableImageView

Blog / Android · July 2, 2014 · Updated June 10, 2026 · 6 min read
Circular ImageView in Android: Compose, Coil & ShapeableImageView

To show a circular (avatar) image in Android in 2026, you don't hand-roll a BitmapShader custom View any more. You use the toolkit you are already in:

  • Jetpack Compose (the modern default): Image(..., modifier = Modifier.clip(CircleShape)), or Coil's AsyncImage(..., modifier = Modifier.clip(CircleShape)) for network avatars.
  • Views / XML: Material Components' ShapeableImageView with a cornerSize of 50% — the approach Google now recommends instead of third-party circle-image libraries.

The old Canvas + BitmapShader recipe still works and is worth understanding, but it is rarely the right choice for new code. This guide shows the modern paths first, then the legacy approach for context.

Key takeaways

  • In Jetpack Compose, a circle is just Modifier.clip(CircleShape); add Modifier.border(...) for a ring.
  • For network avatars, use CoilAsyncImage in Compose, ImageView.load { } in Views — to handle fetch, caching, placeholder and error states.
  • In XML, prefer Material's ShapeableImageView with cornerSize=50% over third-party libraries like CircleImageView.
  • Always pair the circle with centerCrop / ContentScale.Crop so the image fills the circle without distortion.
  • The legacy BitmapShader / custom-View recipe still works; reach for RoundedBitmapDrawable if you must stay on a plain ImageView.

Circular images in Jetpack Compose

Compose makes a circular image trivial: clip any Image with CircleShape. Use ContentScale.Crop so a non-square source fills the circle instead of squashing, and add Modifier.border for an avatar ring.

import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp

Image(
    painter = painterResource(R.drawable.avatar),
    contentDescription = "User avatar",
    contentScale = ContentScale.Crop,            // fill the circle, no distortion
    modifier = Modifier
        .size(96.dp)
        .clip(CircleShape)                       // the actual circle
        .border(2.dp, Color.LightGray, CircleShape)
)

Network avatars with Coil (Compose)

For images loaded from a URL, Coil is the de-facto Compose loader in 2026 (Coil 3, package coil3.compose). Its AsyncImage takes the same Modifier.clip(CircleShape) and gives you placeholder and error slots so a half-loaded avatar never breaks the layout.

If you are still on Coil 2, the packages are coil.compose.AsyncImage and coil.request.ImageRequest; the API shape is otherwise the same.

// build.gradle.kts
// implementation("io.coil-kt.coil3:coil-compose:3.0.4")
// implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4")

import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade

AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data("https://example.com/u/42.jpg")
        .crossfade(true)
        .build(),
    contentDescription = "User avatar",
    contentScale = ContentScale.Crop,
    placeholder = painterResource(R.drawable.avatar_placeholder),
    error = painterResource(R.drawable.avatar_fallback),
    modifier = Modifier
        .size(96.dp)
        .clip(CircleShape)
        .border(2.dp, MaterialTheme.colorScheme.outline, CircleShape)
)

Circular images with ShapeableImageView (Views / XML)

If you are still on XML layouts, Material Components ships ShapeableImageView. Give it a shapeAppearanceOverlay whose cornerSize is 50% and it renders as a perfect circle — no external library needed. Set scaleType="centerCrop" so the source fills the circle.

<!-- res/layout/profile.xml -->
<com.google.android.material.imageview.ShapeableImageView
    android:id="@+id/avatar"
    android:layout_width="96dp"
    android:layout_height="96dp"
    android:scaleType="centerCrop"
    android:src="@drawable/avatar"
    app:shapeAppearanceOverlay="@style/CircleImageView"
    app:strokeColor="@color/avatar_stroke"
    app:strokeWidth="2dp" />
<!-- res/values/styles.xml -->
<style name="CircleImageView">
    <item name="cornerFamily">rounded</item>
    <item name="cornerSize">50%</item>
</style>

The stroke is drawn at the view's edge, so with a thick strokeWidth add matching android:padding (and keep scaleType="centerCrop") to stop the ring being clipped. To load a remote avatar into a ShapeableImageView, let Coil or Glide fetch the bitmap — the view itself handles the circular clip:

import coil3.load
import coil3.request.crossfade
import coil3.request.error
import coil3.request.placeholder

// avatar is the ShapeableImageView from the layout above
avatar.load("https://example.com/u/42.jpg") {
    crossfade(true)
    placeholder(R.drawable.avatar_placeholder)
    error(R.drawable.avatar_fallback)
}

Prefer Glide? Glide.with(context).load(url).placeholder(R.drawable.avatar_placeholder).into(avatar) works the same way — the ShapeableImageView supplies the shape, so you do not need Glide's own circleCrop() transform.

What about the CircleImageView library?

For years the go-to was Henning Dodenhof's de.hdodenhof:circleimageview. It still compiles and is genuinely small, but its last release (3.1.0) shipped back in 2019, and it only draws a circle — you still need Coil or Glide for network images. Because ShapeableImageView is part of Material Components and is actively maintained, it is the better default for new code.

// build.gradle.kts (legacy)
implementation("de.hdodenhof:circleimageview:3.1.0")
<de.hdodenhof.circleimageview.CircleImageView
    android:layout_width="96dp"
    android:layout_height="96dp"
    android:src="@drawable/avatar"
    app:civ_border_width="2dp"
    app:civ_border_color="#FFFFFFFF" />

The classic BitmapShader / custom-View approach

The original technique — the one this article first covered in 2014 — draws the source bitmap through a BitmapShader onto a circular Canvas. It is useful to understand, and occasionally handy when you need full control over the pixels, but for everyday avatars it is far more code than the options above. Here it is in modern Kotlin:

import android.graphics.Bitmap
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Shader

fun toCircularBitmap(source: Bitmap): Bitmap {
    val size = minOf(source.width, source.height)
    val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(output)
    val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    }
    val radius = size / 2f
    canvas.drawCircle(radius, radius, radius, paint)
    return output
}

// imageView.setImageBitmap(toCircularBitmap(sourceBitmap))

If you must stay on a plain ImageView but want to avoid all of that, AndroidX ships a one-liner: RoundedBitmapDrawable with isCircular = true.

import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory

val rounded = RoundedBitmapDrawableFactory.create(resources, bitmap)
rounded.isCircular = true
imageView.setImageDrawable(rounded)

Which approach should you use?

For new screens, reach for Compose + Coil. For existing XML, use ShapeableImageView. Keep the library and custom-View routes only for legacy code you are not ready to migrate.

Approach Toolkit Network images Borders Maintenance Effort
Modifier.clip(CircleShape) + Coil AsyncImage Jetpack Compose First-class (Coil) Modifier.border() Active (Google + Coil) Lowest
ShapeableImageView (cornerSize=50%) + Coil/Glide Views / XML Via Coil or Glide strokeWidth / strokeColor Active (Material) Low
de.hdodenhof:CircleImageView Views / XML Needs Coil or Glide Built-in civ_border_* Stale (last release 2019) Low
Custom BitmapShader View Views / Canvas Manual decode + cache Manual Paint stroke You own it High

Where this fits in a real app

A circular avatar is a small detail, but across contact lists, chat threads and profile screens it adds up — the wrong choice quietly hurts scroll performance and memory. Picking the right tool (Compose + Coil for new UI, ShapeableImageView for existing XML) keeps avatars crisp with almost no code to maintain. At MicroPyramid we build production Android and cross-platform apps as part of larger custom software projects, and these are the same defaults we start from. For wider pitfalls to avoid, see 7 mistakes to avoid while building a mobile application.

Frequently Asked Questions

What is the easiest way to make a circular ImageView in Android in 2026?

In Jetpack Compose, wrap your image with Modifier.clip(CircleShape) and use ContentScale.Crop. In XML layouts, use Material's ShapeableImageView with a shapeAppearanceOverlay whose cornerSize is 50%. Both produce a clean circle without any third-party library.

Should I still use the CircleImageView library?

Not for new code. de.hdodenhof:circleimageview still works, but its last release was in 2019 and it only draws the circle. ShapeableImageView ships with Material Components, is actively maintained, and does the same job, so it is the better default.

How do I load a circular network image (avatar) efficiently?

Use Coil. In Compose, AsyncImage with Modifier.clip(CircleShape) handles fetching, caching, crossfade, placeholder and error. In Views, call imageView.load(url) { ... } on a ShapeableImageView so the loader fetches the bitmap and the view supplies the circular clip.

How do I add a border or stroke around a circular image?

In Compose, add Modifier.border(2.dp, color, CircleShape) after the clip. With ShapeableImageView, set app:strokeWidth and app:strokeColor, and add matching android:padding so a thick ring is not clipped at the edge.

Why does my circular image look stretched or cropped wrong?

Because the source is not square and no crop scale is set. Add ContentScale.Crop in Compose or android:scaleType="centerCrop" in XML so the image fills the circle by cropping the overflow instead of distorting the aspect ratio.

Do I still need a custom View or BitmapShader?

Almost never. The BitmapShader + Canvas technique is worth understanding and still useful for full pixel-level control, but for normal avatars Compose clip, ShapeableImageView, or RoundedBitmapDrawable (with isCircular = true) are far less code to write and maintain.

Share this article