Search code examples
androidtextviewspannable

Thread slows down over time


I am trying to make a rainbow colored title (which changes with time), here is the onCreate:

protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_menu);

    new Thread(TitleColorRunnable).start();

}

And the corrosponding runnable:

Runnable TitleColorRunnable = new Runnable()
{
    TextView title;
    int titleLength;
    Spannable spannable;
    int i;
    float mainHue = 15;
    float hue;
    int color;

    @Override
    public void run()
    {
        title = findViewById(R.id.titleTextView);
        title.setText("TITLE EXAMPLE", TextView.BufferType.SPANNABLE);
        titleLength = title.length();
        spannable = (Spannable) title.getText();

        while (true)
        {
            for (i = 0; i < titleLength - 1; i++)
            {
                hue = (mainHue - i) % 360;
                color = Color.HSVToColor(new float[]{hue, 1, 1});

                title.post(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        spannable.setSpan(new ForegroundColorSpan(color), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                });
            }

            mainHue++;
            if (mainHue == 360)
            {
                mainHue = 0;
            }

            try
            {
                Thread.sleep(10);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
};

This thread slows down over time, and starts to slowly bug down the entire UI until everything stops completely.

Is it possible that the line

spannable.setSpan(new ForegroundColorSpan(color), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

keep saving new ForegroundColorSpan variables to memory?

Please help, thank you!


Solution

  • Ref. the line:

    title.post(new Runnable()...

    There is nothing to guarantee that the Runnable has completed, and been dequeued, by the time you call title.post() the next time. The queue of Runnables inside your app's Looper is probably getting super backlogged. IOW, you're queueing Runnables more quickly than they can be executed, and eventually your UI thread has to spend all its time dealing with those, instead of doing other things you'd like it to do, such as responding to user input and so forth.

    A contributing factor is your 10ms delay: Thread.sleep(10). 100Hz is a little fast for this kind of update; 30Hz ought to be plenty (for human perception), or even slower, since you're just barely changing the color.

    Suggested Fix

    • Get rid of the thread. Take your TitleColorRunnable logic, and move it to onCreate().
    • Declare your Runnable - the one you have as a closure right now - as a final local variable.
    • Inside your myRunnable.run(), execute title.postDelayed( myRunnable, 33 ). This is the trick; this is what keeps the iteration going, and this is what stops the Looper queue from filling up. Since myRunnable re-queues itself, there's never more than one myRunnable in the queue. (It's not recursion; you're simply taking advantage of the Android message queue mechanism to schedule your Runnable.) 33ms is for a 30Hz update rate.
    • To kick the whole thing off, add a title.post( myRunnable ) to onCreate().
    • do mainHue = mainHue + 3; instead, to keep the color changing at roughly the same rate.

    Obviously, with this solution you have to change the state of mainHue inside myRunnable.run().