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:
- create an Android project without a starting activity
- 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>
- 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>
- 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(); } }); } }
Leave a Reply