0

I got a fragment which got a control called RingProgress which is simply a ring that fills itself according to a percentage value given. For example, if I do:

ringProgress.setProgress(20);

It means that 20% of the ring will now be filled.

What I'm trying to do is to animate the ring being filled over a few seconds. So what I've tried to do is this:

@Override
public void onResume()
{
    super.onResume();
    HandlerThread handlerThread = new HandlerThread("countdown");
    handlerThread.start();
    Handler handler = new Handler(handlerThread.getLooper());
    handler.post(new Runnable()
    {
        @Override
        public void run()
        {
            final Timer timer = new Timer();
            timer.scheduleAtFixedRate(new TimerTask()
            {
                int totalSeconds = secondsToStart + minutesToStart * 60;
                int secondsPassed = 0;

                @Override
                public void run()
                {
                    if(secondsPassed == totalSeconds)
                    {
                        timer.cancel();
                    }
                    final int currentProgress = (secondsPassed / totalSeconds) * 100;
                    secondsPassed++;
                    getActivity().runOnUiThread(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            mRingProgressBar.setProgress(currentProgress);
                        }
                    });

                }
            }, 0, 1000);

        }
    });
}

The problem is that the update of the ring is not shown until the time is up. For example, if I set it for 5 seconds then when the fragment loads the ring is set to 0, then nothing happens for 5 seconds and then the ring is full with 100% all at once..

How can I start this animation properly?

CodeMonkey
  • 11,196
  • 30
  • 112
  • 203

4 Answers4

3

I guess the problem is with

final int currentProgress = (secondsPassed / totalSeconds) * 100;

secondsPassed / totalSeconds return int value so it will be 0 or 1 only. And you multiply it to 100.

You have to use float or double instead

something like

final int currentProgress = Math.round(((float) secondsPassed)/((float) totalSeconds)*100f);
Andrey Danilov
  • 6,194
  • 5
  • 32
  • 56
  • This would explain why the progress is only updated at the end – dumazy Mar 02 '17 at 09:02
  • 1
    Also possible to do `secondsPassed * 100 / totalSeconds` which will result in the same thing without the need to cast all numbers. Also, if one of the values is a `float` you don't need to cast the others, which means that `(float) totalSeconds` and `100f` is not necessary if you already have `(float) secondsPassed`. – Edson Menegatti Mar 02 '17 at 09:02
  • @Yonatan Nir, although this seems like is the exact issue, but your code is not reliable, because of incorrect usage of handlers. See my answer. – azizbekian Mar 02 '17 at 09:05
  • That was indeed the problem with the current implementation. I gave it only a thumb up and not accepted since the answer Edson gave me makes everything much less complicated – CodeMonkey Mar 02 '17 at 09:08
2

On this line:

Handler handler = new Handler(handlerThread.getLooper());

You are trying to get the looper from a handlerThread. But how sure you are the looper has already been initialized?

From the documentation of getLooper()

This method returns the Looper associated with this thread. If this thread not been started or for any reason is isAlive() returns false, this method will return null. If this thread has been started, this method will block until the looper has been initialized.

onLooperPrepared() is the callback, where you can be sure, that the Looper has been initialized, and therefore you can construct logics on that.

Thus, what you have to do, is to subclass HandlerThread and create appropriate logics from onLooperPrepared() callback.

Here's a nice post which will help you out. See implementation of MyWorkerThread class there.

azizbekian
  • 60,783
  • 13
  • 169
  • 249
1

Instead of using a handler, you could use a property animator as follows:

ObjectAnimator.ofInt(mRingProgressBar, "progress", 0, 100)
    .setDuration(totalSeconds * 1000)   //time is in miliseconds
    .start();

This will find a method setProgress() in your mRingProgressBarand set the value according to the limits given. In the example above, 0 to 100. You can read more about it here

CodeMonkey
  • 11,196
  • 30
  • 112
  • 203
Edson Menegatti
  • 4,006
  • 2
  • 25
  • 40
0

Since you want to run on a different thread, you can use this handler in the top of the class:

     private int progress = 0;
     Handler timerHandler = new Handler();
     Runnable timerRunnable = new Runnable() {
            @Override
            public void run() {

            ringProgress.setProgress(progress);
            progress += 20;
            if (progress == 100) { //clear??
            }
            timerHandler.postDelayed(this, 1000);
        }
    };

In inCreate set the max:

ringProgress.setMax(100);

This will complete the animation within 5 seconds, then you can clear the animation. If you want smaller increments, change the line below (update every tenth of a second), and change the steps

timerHandler.postDelayed(this, 100);
Idan
  • 5,405
  • 7
  • 35
  • 52