Android Kotlin: How to create a bordered circular ImageView

Introduction

Creating custom UI components in Android can significantly enhance the user experience and visual appeal of your app. One such common UI requirement is displaying circular images with borders. By default, Android’s ImageView does not provide an easy way to apply circular cropping or add borders. However, by using Kotlin extensions and custom drawing techniques, we can create a reusable, efficient solution to display circular images with borders.

In this tutorial, we will break down how to create a bordered circular ImageView in an Android application using Kotlin. We will leverage Kotlin extension functions to simplify the process of converting a regular bitmap into a circular image with a border, which can be easily applied across different parts of the app.

Main Code Walkthrough

The MainActivity.kt file is the core of this implementation. The onCreate method initializes the UI and loads an image from the app's assets folder. The image is then displayed in two different ImageView widgets. The first ImageView shows the raw image, while the second ImageView applies the circular cropping and border effect using an extension function.

The extension function circularWithBorder is the key to transforming a regular Bitmap into a circular one with a border. This function calculates the diameter and radius of the desired circular image by taking into account the original image dimensions and the specified border width. A Canvas is used to draw the image and apply the circular border. The result is encapsulated in a RoundedBitmapDrawable, which is then set as the drawable for the ImageView.

Circular Image with Border Implementation

To create a circular bitmap, the function first calculates the diameter and radius based on the image dimensions. It ensures that the circular crop fits within the bounds of the smallest dimension of the original image. A Canvas object is then used to draw the bitmap at the center of the canvas. The circular border is drawn using a Paint object configured with the desired border width, style, and color. This ensures that the image has a neatly drawn circular frame.

The RoundedBitmapDrawableFactory.create method is used to return a RoundedBitmapDrawable, which supports setting the image as circular. The result of this operation is a circular image with a smooth, anti-aliased border, which can then be easily assigned to any ImageView.

Supporting Utility Functions

Two additional Kotlin extension functions are used in this implementation. The dpToPixels function converts density-independent pixels (dp) to screen pixels, ensuring that the border width scales appropriately across different screen densities. This is essential for creating a visually consistent UI across devices.

The assetsToBitmap function handles loading images from the app's assets folder. It safely reads an image file, decodes it into a Bitmap, and returns it for further processing. This function is robust against I/O exceptions, ensuring that the app doesn't crash if the image file is missing or unreadable.

XML Layout

The activity_main.xml layout defines a simple UI with two ImageView widgets inside a ConstraintLayout. The first ImageView displays the original bitmap image, while the second ImageView shows the circular version with a border. Both views are constrained within the layout to maintain responsiveness and adapt to different screen sizes.

Summary

In this guide, we demonstrated how to create a circular ImageView with a customizable border using Kotlin in Android. By leveraging extension functions and custom drawing techniques, we simplified the process of transforming a regular bitmap into a circular image with a border. The approach we used ensures that this functionality can be reused throughout the app.

With these techniques, you can enhance the visual design of your Android applications by displaying circular images in a simple, efficient, and reusable way. Whether you need to show profile pictures, logos, or any other type of image, this method provides a clean, professional solution.

MainActivity.kt


package com.example.jetpack

import android.content.Context
import android.graphics.*
import android.os.Bundle
import android.util.TypedValue
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.drawable.RoundedBitmapDrawable
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import kotlinx.android.synthetic.main.activity_main.*
import java.io.IOException
import kotlin.math.min


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // get bitmap from assets folder
        val bitmap: Bitmap? = assetsToBitmap("flower10.jpg")

        // show the bitmap on first image view
        bitmap?.apply { imageView.setImageBitmap(this) }

        // make the bitmap circular with specified border
        bitmap?.circularWithBorder(
            context = applicationContext,
            borderWidth = 7.dpToPixels(applicationContext),
            borderColor = Color.parseColor("#004225")
        )?.apply {
            imageView2.setImageDrawable(this)
        }
    }
}


// extension function to make a bitmap circular with border
fun Bitmap.circularWithBorder(
    context: Context,
    borderWidth: Float = 25F,
    borderColor: Int = Color.BLACK
): RoundedBitmapDrawable? {
    // calculate the bitmap diameter and radius
    val diameter = (min(width, height) + borderWidth * 2).toInt()
    val radius = diameter / 2F

    val bitmap = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    canvas.drawColor(Color.WHITE)

    // draw bitmap at canvas center
    canvas.drawBitmap(
        this, // bitmap
        (diameter - this.width)/2F, // left
        (diameter - this.height)/2F, // top
        null // paint
    )

    // pa to draw circular border
    Paint().apply {
        style = Paint.Style.STROKE
        // stroke is always centered so double it
        strokeWidth = borderWidth * 2F
        color = borderColor
        isAntiAlias = true

        // draw circular border on canvas
        canvas.drawCircle(canvas.width / 2F,canvas.width / 2F,radius,this)
    }

    // return circular bitmap drawable with border
    return RoundedBitmapDrawableFactory.create(context.resources, bitmap).apply {
        isCircular = true
        setAntiAlias(true)
    }
}


// extension function to convert dp to equivalent pixels
fun Int.dpToPixels(context: Context):Float = TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), context.resources.displayMetrics
)


// extension function to get bitmap from assets
fun Context.assetsToBitmap(fileName: String): Bitmap?{
    return try {
        with(assets.open(fileName)){
            BitmapFactory.decodeStream(this)
        }
    } catch (e: IOException) { null }
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#EDEAE0"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="280dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:srcCompat="@tools:sample/avatars" />

    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="0dp"
        android:layout_height="280dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView"
        tools:srcCompat="@tools:sample/avatars" />

</androidx.constraintlayout.widget.ConstraintLayout>
More android kotlin tutorials