Categories
Android

Updating frequently a TextView inside a loop

The problem
How to update a TextView inside a loop or, more generally, how to update frequently an UI element of an Activity.
See the following example:

  1. create an Android project without a starting activity
  2. edit the file AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="eu.lucazanini.updateui"
        android:versionCode="1"
        android:versionName="1.0" >
    
        <uses-sdk
            android:minSdkVersion="8"
            android:targetSdkVersion="17" />
    
        <application
            android:allowBackup="true"
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:theme="@style/AppTheme" >
            <activity
                android:name=".MainActivity"
                android:label="@string/app_name" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    
    </manifest>
    
  3. edit the the file res/layout/activity_main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Update UI" />
    
        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Large Text"
            android:textAppearance="?android:attr/textAppearanceLarge" />
    
    </LinearLayout>
    
  4. create the file eu/lucazanini/updateui/MainActivity.java
    package eu.lucazanini.updateui;
    
    import java.text.DateFormat;
    import java.util.Calendar;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.Menu;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    public class MainActivity extends Activity {
    
        private static final String TAG = "UpdateUI";
        private TextView tv;
    
        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
    	// Inflate the menu; this adds items to the action bar if it is present.
    	getMenuInflater().inflate(R.menu.main, menu);
    	return true;
        }
    
        private String getTime() {
    	Calendar calendar = Calendar.getInstance();
    	DateFormat formatTime = DateFormat.getTimeInstance();
    	return formatTime.format(calendar.getTime());
        }
    
        private void updateUI() {
    	long endTime = System.currentTimeMillis() + 5 * 1000;
    	while (System.currentTimeMillis() < endTime) {
    	    String time = getTime();
    	    tv.setText(time);
    	    Log.d(TAG, "time " + time);
    
    	}
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
    	setContentView(R.layout.activity_main);
    
    	tv = (TextView) findViewById(R.id.textView1);
    
    	Button btn = (Button) findViewById(R.id.button1);
    	btn.setOnClickListener(new View.OnClickListener() {
    	    @Override
    	    public void onClick(View v) {
    		updateUI();
    	    }
    	});
        }
    
    }
    

The expected result is that to update frequently the TextView for 5 seconds displaying the device time, instead the observed result is that of a single update at the end.

The cause
In the LogCat you can see the message “Skipped […] frames! The application may be doing too much work on its main thread.”; the message is clear: too many operation on the main thread also known as UI thread.
A first attempt to solve the problem may be to call the updateUI method from another thread as in the following example:

package eu.lucazanini.updateui;

import java.text.DateFormat;
import java.util.Calendar;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {

    private static final String TAG = "UpdateUI";

    private Runnable separateThread = new Runnable() {
	@Override
	public void run() {
	    updateUI();
	}

    };

    private TextView tv;

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
	// Inflate the menu; this adds items to the action bar if it is present.
	getMenuInflater().inflate(R.menu.main, menu);
	return true;
    }

    private String getTime() {
	Calendar calendar = Calendar.getInstance();
	DateFormat formatTime = DateFormat.getTimeInstance();
	return formatTime.format(calendar.getTime());
    }

    private void updateUI() {
	long endTime = System.currentTimeMillis() + 5 * 1000;
	while (System.currentTimeMillis() < endTime) {
	    String time = getTime();
	    tv.setText(time);
	    Log.d(TAG, "time " + time);

	}
    }

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

	tv = (TextView) findViewById(R.id.textView1);

	Button btn = (Button) findViewById(R.id.button1);
	btn.setOnClickListener(new View.OnClickListener() {
	    @Override
	    public void onClick(View v) {
		Thread t = new Thread(separateThread);
		t.start();
	    }
	});

    }

}

but the application crashes at the line 45 launching the exception “android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.”.
In fact, you can’t access a View from threads other than the main one.
So the question is how to access a View without taking up too many resources from the UI thread?

The first solution: the Handler class
An instance of a Handler is associated with the thread in which it was created, in this case the main thread, and a second thread (separateThread) uses the Handler to send a Message to the main thread.

package eu.lucazanini.updateui;

import java.text.DateFormat;
import java.util.Calendar;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {

    private static final String TAG = "UpdateUI";

    private Handler handler = new Handler() {
	@Override
	public void handleMessage(Message msg) {
	    Bundle b = msg.getData();
	    String key = b.getString("timeKey");
	    tv.setText(key);
	}
    };

    private Runnable separateThread = new Runnable() {
	@Override
	public void run() {
	    updateUI();
	}

    };

    private TextView tv;

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
	// Inflate the menu; this adds items to the action bar if it is present.
	getMenuInflater().inflate(R.menu.main, menu);
	return true;
    }

    private String getTime() {
	Calendar calendar = Calendar.getInstance();
	DateFormat formatTime = DateFormat.getTimeInstance();
	return formatTime.format(calendar.getTime());
    }

    private void updateUI() {
	long endTime = System.currentTimeMillis() + 5 * 1000;
	while (System.currentTimeMillis() < endTime) {
	    String time = getTime();
	    Message msg = new Message();
	    Bundle b = new Bundle();
	    b.putString("timeKey", time);
	    msg.setData(b);
	    handler.sendMessage(msg);
	    Log.d(TAG, "time " + time);

	}
    }

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

	tv = (TextView) findViewById(R.id.textView1);

	Button btn = (Button) findViewById(R.id.button1);
	btn.setOnClickListener(new View.OnClickListener() {
	    @Override
	    public void onClick(View v) {

		Thread t = new Thread(separateThread);
		t.start();

	    }
	});

    }

}

The second solution: the AsyncTask class
In this example, the inner class MyAsyncTask extends the abstract class AsyncTask, and overwrites the doInBackground method that performs the calculation and with the line publishProgress calls the onProgressUpdate method that performs the updating of the TextView.
Often the update of the TextView is required after it has been completed the doInBackground method, in this case, you can insert the code to update the TextView in the OnPostExecute method.

package eu.lucazanini.updateui;

import java.text.DateFormat;
import java.util.Calendar;

import android.os.AsyncTask;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {

    private class MyAsyncTask extends AsyncTask<Void, String, Void> {

	@Override
	protected Void doInBackground(Void... params) {

	    long endTime = System.currentTimeMillis() + 5 * 1000;
	    while (System.currentTimeMillis() < endTime) {
		String time = getTime();
		publishProgress(time);
		Log.d(TAG, "time " + time);
	    }

	    return null;
	}

	@Override
	protected void onPostExecute(Void result) {
	    super.onPostExecute(result);
	}

	@Override
	protected void onProgressUpdate(String... values) {

	    tv.setText(values[0]);

	}

    }

    private static final String TAG = "UpdateUI";
    private TextView tv;

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
	// Inflate the menu; this adds items to the action bar if it is present.
	getMenuInflater().inflate(R.menu.main, menu);
	return true;
    }

    private String getTime() {
	Calendar calendar = Calendar.getInstance();
	DateFormat formatTime = DateFormat.getTimeInstance();
	return formatTime.format(calendar.getTime());
    }

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

	tv = (TextView) findViewById(R.id.textView1);

	Button btn = (Button) findViewById(R.id.button1);
	btn.setOnClickListener(new View.OnClickListener() {
	    @Override
	    public void onClick(View v) {

		MyAsyncTask myAsyncTask = new MyAsyncTask();
		myAsyncTask.execute();

	    }
	});
    }

}

2 replies on “Updating frequently a TextView inside a loop”

I am curios to know if this code works. I am running something very similar but I am getting the error “Only the original thread that created a view hierarchy can touch its views”. After reading many posts I have seen people usually set views on the UI thread instead of on the async task and use async tasks to run other time consuming code.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.