androidandroid-drawableandroid-progressbarxml-drawable

How does XML modify a horizontal progress bar to a circular one with progressDrawable property?


I have this ProgressBar xml:

<ProgressBar
  android:id="@+id/progressBar"
  style="?android:progressBarStyleHorizontal"
  android:layout_width="80dp"
  android:layout_height="80dp"
  android:layout_marginBottom="32dp"
  android:progress="0"
  android:progressDrawable="@drawable/bg_circular_progress_bar"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent"
  android:elevation="11dp"/>

As can be seen, the style="?android:progressBarStyleHorizontal" specifies that it is a horizontal bar. However, with progressDrawable property, I can somehow change its shape to circular with the following drawable file:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="ring"
    android:innerRadiusRatio="2.5"
    android:thickness="4dp"
    android:useLevel="true">

    <solid android:color="@color/colorPrimary" />
</shape>

Solution

    • How does that drawable file change a horizontal thing into a circular thing and how does it decide how to animate the progress bar from 0degrees to 360degress forming a full ring when the progress is 100%?

    When you specify style="?android:progressBarStyleHorizontal" what you are really saying is that the ProgressBar will be deteminate. From the documentation for ProgressBar:

    To indicate determinate progress, you set the style of the progress bar to R.style.Widget_ProgressBar_Horizontal and set the amount of progress.

    By default, the ProgressBar will be horizontal but, as you noted, it can be changed to circular with your drawable file.

    <shape 
        android:shape="ring"
        android:innerRadiusRatio="2.5"
        android:thickness="4dp"
        android:useLevel="true">
        <solid android:color="@color/colorPrimary" />
    </shape>
    

    Notice the line android:useLevel="true". This specifies that the drawable can accept a level that can range from 0 to 100. The drawable knows how much of itself to draw based upon the level set: 0 is to draw nothing and 100 is to draw 100%. By changing the progress, you are changing the level set for the drawable. Try setting android:useLevel="false" to see what happens.

    • Also, is there a way to manipulate the start of the animation (like from 90degrees from the positive x-axis)?

    In the XML, set android:rotation="90" to have the ring start at the bottom.

    • How do I make another custom shape, like a rectangle being filled from bottom to top with a background color (to signify the rectangle shape before being filled) that doesn't change?

    There are a couple (maybe more) ways to do this. One way is to define a layer-list drawable that defines the background rectangle and the progress rectangle that is a scale drawable. The ids must be as specified since ProgressBar relies upon them.

    <layer-list>
    <item
        android:id="@android:id/background">
        <shape android:shape="rectangle">
            <solid android:color="@android:color/darker_gray" />
        </shape>
    </item>
    <item
        android:id="@android:id/progress">
        <scale
            android:scaleHeight="100%"
            android:scaleGravity="bottom">
            <shape android:shape="rectangle">
                <solid android:color="@android:color/holo_red_light" />
            </shape>
        </scale>
    </item>
    

    You could also use a clip drawable in a layer list:

    <layer-list>
        <item android:id="@android:id/background">
            <shape android:shape="rectangle">
                <solid android:color="@android:color/darker_gray" />
            </shape>
        </item>
        <item android:id="@android:id/progress">
            <clip
                android:clipOrientation="vertical"
                android:gravity="bottom">
                <shape android:shape="rectangle">
                    <solid android:color="@android:color/holo_red_light" />
                </shape>
            </clip>
        </item>
    </layer-list>
    

    Here is a sample layout that puts this all together:

    activity_main.xml

    <androidx.constraintlayout.widget.ConstraintLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:progressBarStyleHorizontal"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:layout_marginTop="16dp"
            android:progress="0"
            android:progressDrawable="@drawable/bg_circular_progress_bar"
            android:rotation="90"
            app:layout_constraintVertical_chainStyle="packed"
            app:layout_constraintBottom_toTopOf="@+id/progressBar2"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <ProgressBar
            android:id="@+id/progressBar2"
            style="?android:progressBarStyleHorizontal"
            android:layout_width="20dp"
            android:layout_height="80dp"
            android:layout_marginTop="16dp"
            android:progress="0"
            android:progressDrawable="@drawable/rectangular_progress_with_scale_drawable"
            app:layout_constraintBottom_toTopOf="@+id/progressBar3"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/progressBar" />
    
        <ProgressBar
            android:id="@+id/progressBar3"
            style="?android:progressBarStyleHorizontal"
            android:layout_width="20dp"
            android:layout_height="80dp"
            android:layout_marginTop="16dp"
            android:progress="0"
            android:progressDrawable="@drawable/rectangular_progress_with_clip_drawable"
            app:layout_constraintBottom_toTopOf="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/progressBar2" />
    
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="0/ 100"
            app:layout_constraintBottom_toTopOf="@+id/button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/progressBar3" />
    
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:onClick="onClick"
            android:text="Start"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    MainActivity.kt

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }
    
        fun onClick(view: View) {
            view.isEnabled = false
            var progress = 0
            val handler = Handler()
    
            Thread(Runnable {
                while (progress < 100) {
                    progress += 5
                    handler.post {
                        progressBar.progress = progress
                        progressBar2.progress = progress
                        progressBar3.progress = progress
                        textView.text = "$progress/ ${progressBar.max}"
                    }
                    try {
                        Thread.sleep(100)
                    } catch (e: InterruptedException) {
                        e.printStackTrace()
                    }
                }
                runOnUiThread { view.isEnabled = true }
            }).start()
        }
    }
    

    enter image description here