Search code examples
javaandroidprogress-indicator

CircularProgressIndicator shows too late, when time consuming code has finished


I am trying to show a progress indicator in some time consuming code but the screen is not refreshed while the code is running. After the code is finished it animates the indicator.
The solution is probably simple for an experienced android developer which I am not.
I created a minimum code application to show the problem

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"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="@android:color/black">
        <Button
            android:id="@+id/button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:textSize="18sp"
            android:text="Start"
            tools:ignore="HardcodedText" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal">
            <com.google.android.material.progressindicator.CircularProgressIndicator
                android:id="@+id/cpi"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:progress="0"
                android:useLevel="true"
                android:visibility="visible"
                app:indicatorColor="#FF0000"
                app:indicatorSize="95dp"
                app:trackColor="#606060"
                app:trackCornerRadius="5dp"
                app:trackThickness="16dp"
                tools:layout_editor_absoluteX="157dp"
                tools:layout_editor_absoluteY="108dp" />
        </LinearLayout>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java:

package com.hennep.testcircularprogress;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.os.Handler;
import com.google.android.material.progressindicator.CircularProgressIndicator;

public class MainActivity extends AppCompatActivity {
    int iProgress = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final Button button = findViewById(R.id.button);
        final CircularProgressIndicator progress = findViewById(R.id.cpi);

        Handler handler = new Handler();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true ) {
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            progress.setProgress(iProgress,true);
                        }
                    },100 );
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e ) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();


        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v ) {
                iProgress = 0;
                //progress.setVisibility( CircularProgressIndicator.VISIBLE );
                button.setText("Calculating...");
                for( int k = 0; k < 192; k++ ) {
                    Thread.yield();
                    for( int fk = 1; fk <= 1000; fk *= 10 ) {
                        for( int j = 0; j < 192; j++ ) {
                            for( int fj = 1; fj <= 1000; fj *= 10 ) {
                                for( int i = 0; i < 192; i++ ) {
                                    for( int fi = 1; fi <= 1000; fi *= 10 ) {
                                        // Calculate E192 three resistor resistive divider here, not relevant for the test
                                        iProgress = k;
                                    }
                                }
                            }
                        }
                    }
                }
                button.setText("Start");
                //progress.setVisibility( CircularProgressIndicator.GONE );
            }
        });        
    }
}

Also the text on the button does not change, I expect that to be the same problem.


Solution

  • You are running the calculation on the main UI thread which causes the "stalled" UI behavior. Button onClick handlers are executed on the UI thread. Remember there is only one UI thread - so if it is busy doing calculations it can't refresh display.

    Here are some improvements:

    (1) First the UI progress update can be simplified to a self-arming runnable (which runs on the UI thread) to update the progress every 100MS. NOTE: no sleeps. And self-terminates when progress reaches 100. It is started when the button is pressed. It also updates the button with some feedback.

        Button b = findViewById(R.id.button);
    
        // The "new Handler()" is deprecated.
        Handler handler = new Handler(Looper.getMainLooper());
        Runnable r = new Runnable() {
    
            @Override
            public void run() {
                progress.setProgress(iProgress);
                if (iProgress < 100) {
                    handler.postDelayed(this, 100);
                    b.setText(String.format("RUNNING: %d", iProgress));
                }
            }
        };
    
        // The handler progress updater is started when button is pressed (below)
    

    (2) Use button processing to start a new thread (not on the UI thread by definition) to perform the calculation, update the progress and update the button as needed.

        b.setText("Start");
    
        b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                b.setText("Running");
                b.setEnabled(false);
    
                iProgress = 0;
                // start the progress updater
                handler.postDelayed(r, 100);
    
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        long d = System.currentTimeMillis();
                        for (int k = 0; k <= 100; k++) {
                            for (int j = 1; j < 100_000_000; j++) {
                                // some calculation
                                double x = (double) d / j;
                            }
                            iProgress = k;
                        }
    
                        runOnUiThread(new Runnable() {
    
                            @Override
                            public void run() {
                                b.setText("Done - Restart");
                                b.setEnabled(true);
                            }
                        });
                    }
                }).start();
            }
        });
    

    And result:

    enter image description here