Android Kotlin: How to create a custom circular progress bar

Android Kotlin: Custom Design Circular Progress Bar

This code demonstrates how to create a custom circular progress bar in an Android application using Kotlin. It achieves this by defining a custom drawable resource for the progress bar and controlling its progress within the main activity.

Key functionalities:

  • The circular_progress_bar.xml drawable defines the visual appearance of the progress bar. It uses a layer-list to combine a background oval with a colored progress ring. The ring utilizes a sweep gradient for a smooth color transition.
  • The MainActivity.kt class handles user interaction and progress updates. It simulates downloading a random number of files.
  • Upon clicking the "Start Task" button, the progress bar resets and a new download task is initiated on a separate thread.
  • The download progress is simulated using a loop that increments a counter with a slight delay.
  • The Handler ensures updates to the progress bar and text views happen on the main UI thread.
  • The text views display the downloaded file count and progress percentage.
  • Once the download completes, the button is re-enabled for initiating another task.

Summary

This code provides a basic example of building a custom circular progress bar with functionalities like progress updates, text displays, and user interaction. You can further customize the design and functionalities based on your specific needs.


MainActivity.kt

package com.cfsuman.kotlintutorials

import android.app.Activity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import kotlin.random.Random


class MainActivity : Activity() {
    var progressStatus = 0
    var handler = Handler(Looper.getMainLooper())

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

        // Get the widgets reference from XML layout
        val button = findViewById<Button>(R.id.button)
        val textView = findViewById<TextView>(R.id.textView)
        val tvPercentage = findViewById<TextView>(R.id.tvPercentage)
        val progressBar = findViewById<ProgressBar>(R.id.progressBar)


        // button click listener
        button.setOnClickListener {
            button.isEnabled = false

            // set up progress bar on initial stage
            progressBar.progress = 0
            progressStatus = 0

            // generate random number of files to download
            val filesToDownload= Random.nextInt(150,500)

            // set up max value for progress bar
            progressBar.max = filesToDownload

            Thread {
                while (progressStatus < filesToDownload) {
                    // update progress status
                    progressStatus += 1

                    // sleep the thread for 50 milliseconds
                    Thread.sleep(50)

                    // update the progress bar
                    handler.post {
                        progressBar.progress = progressStatus

                        // calculate the percentage
                        val percentage = ((progressStatus.toDouble()
                                / filesToDownload) * 100).toInt()

                        // update the text view
                        textView.text = "Downloaded $progressStatus of " +
                                "$filesToDownload files"
                        tvPercentage.text = "$percentage%"

                        if (progressStatus == filesToDownload) {
                            button.isEnabled = true
                        }
                    }
                }
            }.start()
        }
    }
}
res/drawable/circular_progress_bar.xml

<?xml version="1.0" encoding="UTF-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@android:id/background">
        <shape android:shape="oval">
            <stroke
                android:color="#F4F0EC"
                android:width="10dp"
                android:dashWidth="5dp"
                android:dashGap="5dp"/>
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <rotate
            android:fromDegrees="180"
            android:toDegrees="180"
            android:pivotX="50%"
            android:pivotY="50%">
            <shape
                android:thickness="10dp"
                android:shape="ring"
                android:thicknessRatio="25.0"
                android:innerRadiusRatio="2.2">
                <gradient
                    android:startColor="#FF5722"
                    android:endColor="#00BCD4"
                    android:centerColor="#FFEB3B"
                    android:useLevel="true"
                    android:angle="90"
                    android:type="sweep"/>
            </shape>
        </rotate>
    </item>
</layer-list>
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/rootLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="24dp"
    android:background="#DCDCDC">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:padding="12dp"
        android:fontFamily="sans-serif"
        android:textSize="20sp"
        android:textStyle="bold"
        tools:text="TextView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvPercentage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif"
        android:padding="12dp"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="@+id/progressBar"
        app:layout_constraintEnd_toEndOf="@+id/progressBar"
        app:layout_constraintStart_toStartOf="@+id/progressBar"
        app:layout_constraintTop_toTopOf="@+id/progressBar"
        tools:text="Percentage" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="250dp"
        android:layout_height="250dp"
        android:progressDrawable="@drawable/circular_progress_bar"
        android:layout_marginTop="12dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Start Task"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/progressBar" />

</androidx.constraintlayout.widget.ConstraintLayout>