Android Kotlin: How to create a circular bitmap with a border

Introduction

This Android Kotlin tutorial demonstrates how to create a circular bitmap with a border using a custom extension function. The example covers extracting a bitmap image from the assets folder, transforming it into a circular shape, and adding a border around it. This is particularly useful for displaying profile pictures, icons, or thumbnails in a circular form, which is a common design pattern in modern Android apps.

In this example, the image manipulation is done using the Canvas and Paint classes from Android's graphics library, along with a couple of helper functions. The project also makes use of ConstraintLayout to align and display two ImageViews: one for the original image and the other for the circular, bordered version.

MainActivity Breakdown

The MainActivity class extends the Activity class and sets the content view to activity_main.xml, which contains the layout for the app. The layout includes two ImageView components and one TextView to label each image.

Inside onCreate(), the code first references the ImageView and TextView widgets from the XML layout. It then loads a bitmap image named "flower103.jpg" from the assets folder using the assetsToBitmap() function, which is defined as an extension function for the Context class. If the bitmap is successfully retrieved, it is displayed in the first ImageView in its original form.

The next key step is applying the borderedCircularBitmap() extension function to crop the bitmap into a circular shape and add a border. This processed bitmap is displayed in the second ImageView, and the TextView is updated to describe the transformation that has been applied to the image.

assetsToBitmap() Function

The assetsToBitmap() function is a helper that retrieves a bitmap image from the app's assets folder. It takes the filename as an argument and uses a try-catch block to handle potential IOException errors. If successful, it returns a decoded bitmap using BitmapFactory.decodeStream(). If it encounters an error, it logs the exception and returns null.

This function simplifies loading images stored within the app's resources, providing a clean way to access asset images without having to write repetitive code.

borderedCircularBitmap() Function

The borderedCircularBitmap() function is the core of this example. It converts a square bitmap into a circular one with an optional border. This function operates by creating a new bitmap of the same dimensions as the original. A Canvas is then used to draw the circular shape, and the Path class defines the border and the circular crop region.

To draw the border, the function first creates a circular path corresponding to the radius of the bitmap and uses clipPath() to constrain the drawing to this path. The border is drawn using the drawColor() method, which fills the clipped circular area with the specified border color.

The function then creates another circular path, slightly smaller than the first to account for the border width, and clips the bitmap again to ensure the image is drawn inside this smaller circle. The bitmap is then drawn onto the canvas twice: once to clear the drawing area and once to overlay the actual image, preserving transparency.

Finally, the function crops the newly created circular bitmap to the exact size of the circle and returns the result. This method ensures the final bitmap is centered and has a clean circular border.

Layout Overview

The activity_main.xml file defines the UI layout using a ConstraintLayout for flexible positioning. It includes two ImageView elements for displaying the original and processed bitmaps, along with corresponding TextView labels.

The layout ensures that both images are centered horizontally on the screen and have consistent padding. The second ImageView is placed below the first with adequate spacing to provide a clear distinction between the original and circular bordered images.

Summary

This example demonstrates how to create a circular bitmap with a border in Android using Kotlin. By leveraging extension functions, the code is modular and reusable, making it easier to implement similar functionality in other parts of an app. The Canvas and Path classes from Android's graphics API are used to handle the drawing and clipping operations, while the custom assetsToBitmap() and borderedCircularBitmap() functions simplify the process of loading and transforming images.

The final layout is minimalistic, but it clearly illustrates how to present both the original and modified images on the screen, making it a great starting point for developers looking to implement circular image views in their apps.


MainActivity.kt

package com.cfsuman.kotlintutorials

import android.app.Activity
import android.content.Context
import android.graphics.*
import android.os.Bundle
import android.widget.*
import java.io.IOException
import kotlin.math.min

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

        // get the widgets reference from XML layout
        val imageView = findViewById<ImageView>(R.id.imageView)
        val imageView2 = findViewById<ImageView>(R.id.imageView2)
        val textView2 = findViewById<TextView>(R.id.textView2)


        // get the bitmap from assets folder
        val bitmap = assetsToBitmap("flower103.jpg")


        bitmap?.apply {
            // show original bitmap in first image view
            imageView.setImageBitmap(this)

            // crop circular area from bitmap and add border
            imageView2.setImageBitmap(borderedCircularBitmap(
                borderColor = Color.parseColor("#F8F8F8"),
                borderWidth = 12
            ))
            textView2.text = "Circular Bitmap (Border #F8F8F8 12 px)"
        }
    }
}



// extension function to get bitmap from assets
fun Context.assetsToBitmap(fileName:String):Bitmap?{
    return try {
        val stream = assets.open(fileName)
        BitmapFactory.decodeStream(stream)
    } catch (e: IOException) {
        e.printStackTrace()
        null
    }
}



// extension function to create circular bitmap with border
fun Bitmap.borderedCircularBitmap(
    borderColor:Int = Color.LTGRAY,
    borderWidth:Int = 10
):Bitmap?{
    val bitmap = Bitmap.createBitmap(
        width, // width in pixels
        height, // height in pixels
        Bitmap.Config.ARGB_8888
    )

    // canvas to draw circular bitmap
    val canvas = Canvas(bitmap)

    // get the maximum radius
    val radius = min(width/2f,height/2f)

    // create a path to draw circular bitmap border
    val borderPath = Path().apply {
        addCircle(
            width/2f,
            height/2f,
            radius,
            Path.Direction.CCW
        )
    }

    // draw border on circular bitmap
    canvas.clipPath(borderPath)
    canvas.drawColor(borderColor)


    // create a path for circular bitmap
    val bitmapPath = Path().apply {
        addCircle(
            width/2f,
            height/2f,
            radius - borderWidth,
            Path.Direction.CCW
        )
    }

    canvas.clipPath(bitmapPath)
    val paint = Paint().apply {
        xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
        isAntiAlias = true
    }

    // clear the circular bitmap drawing area
    // it will keep bitmap transparency
    canvas.drawBitmap(this,0f,0f,paint)

    // now draw the circular bitmap
    canvas.drawBitmap(this,0f,0f,null)


    val diameter = (radius*2).toInt()
    val x = (width - diameter)/2
    val y = (height - diameter)/2

    // return cropped circular bitmap with border
    return Bitmap.createBitmap(
        bitmap, // source bitmap
        x, // x coordinate of the first pixel in source
        y, // y coordinate of the first pixel in source
        diameter, // width
        diameter // height
    )
}
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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/rootLayout"
    android:background="#DCDCDC"
    android:padding="24dp">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="250dp"
        android:layout_marginTop="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Original Bitmap"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/imageView"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="0dp"
        android:layout_height="250dp"
        android:layout_marginTop="24dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Circular Bitmap With Border"
        app:layout_constraintEnd_toEndOf="@+id/imageView2"
        app:layout_constraintStart_toStartOf="@+id/imageView2"
        app:layout_constraintTop_toBottomOf="@+id/imageView2" />

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