Android Kotlin: How to create a custom horizontal progress bar

Introduction

This code demonstrates creating a custom horizontal progress bar in an Android application written in Kotlin. The progress bar simulates downloading a random number of files. The code includes three parts:

  1. The MainActivity.kt file handles the activity logic, user interaction, and progress updates.
  2. The progressbar_states.xml file defines the custom drawable used for the progress bar.
  3. The activity_main.xml file defines the user interface layout for the activity.

Breakdown of MainActivity.kt

The MainActivity.kt file manages the core functionality of the application. Here's a breakdown of the code:

  1. Variable Initialization: It declares variables for progressStatus to track download progress, and a handler to update the UI thread.
  2. onCreate(): This function sets up the activity when it's first created. Here, it:
    • Inflates the layout defined in activity_main.xml.
    • Finds references to the button, text view, and progress bar using their IDs.
  3. Button Click Listener: This code defines what happens when the user clicks the button:
    • Disables the button to prevent multiple clicks.
    • Resets progress variables and the progress bar.
    • Generates a random number of files to download (between 10 and 200).
    • Sets the maximum value of the progress bar based on the number of files.
    • Creates a new thread to simulate the download process.

Functionality within the Thread

The code within the new thread continuously loops until all files are downloaded:

  1. It increments the progressStatus variable to simulate file download.
  2. The thread sleeps for 200 milliseconds to simulate download time.
  3. It uses the handler.post method to update the UI thread safely. Inside the handler:
    • The progress bar value is updated based on the current progress.
    • The percentage downloaded is calculated and displayed in the text view.
    • The button is re-enabled once all files are downloaded.

Summary

This code showcases a custom horizontal progress bar implementation in Kotlin. It demonstrates how to use threads, handlers, and UI updates to simulate a download process and provide visual feedback to the user. The custom drawable defined in progressbar_states.xml creates the unique visual style of the progress bar.


MainActivity.kt

package com.example.jetpack

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 androidx.appcompat.app.AppCompatActivity
import kotlin.random.Random


class MainActivity : AppCompatActivity() {
    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 progressBar = findViewById<ProgressBar>(R.id.progressBar)


        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(10,200)

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

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

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

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

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

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

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

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/background">
            <shape android:shape="line">
                <stroke
                    android:width="3dp"
                    android:dashWidth="20dp"
                    android:dashGap="7dp"
                    android:color="#FFEB3B"
                    />
            </shape>
    </item>

    <item android:id="@+id/progress">
        <clip android:clipOrientation="horizontal" android:gravity="start">
            <shape android:shape="line">
                <stroke
                    android:width="3dp"
                    android:dashWidth="20dp"
                    android:dashGap="7dp"
                    android:color="#2196F3"
                    />
            </shape>
        </clip>
    </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/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.textview.MaterialTextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:padding="24dp"
        android:text="Stats"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Start Task"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/progressBar" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="12dp"
        android:layout_marginEnd="8dp"
        android:progressDrawable="@drawable/progressbar_states"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

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