How can i optimize my stopwatch in my app to use less cpu?
I am running a stopwatch using an asynctask class in the mainActivity. The doInBackground() method increments the values for the hours, minutes, seconds and centiseconds(10th of a second). The onProgressUpdate() method is responsible for updating 4 imageViews that display the hrs, mins, sec, centisec.
The problem i have is that the stopwatch uses about on average 50%+ cpu usage according to android studio(50% user and 30% kernel usage) and a cpu monitoring app that i installed on the device (2013 HTC one m7). The default android operating system stopwatch uses only about 10% cpu usage. If i use textViews instead of image views the cpu usage drop to half (less than 25%). But it is still more than 10% and i also i want to keep the style of digits im using.
Would caching the images help in anyway? source
I have also considered using XML drawables for the digits instead of bitmaps, but i don't know how effective this will be or if its even possible to create xml drawables of the digits
Lend me your knowledge stackoverflow
main XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:background="#763768"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context="com.example.aroboius.stopwatch.MainActivity"
tools:showIn="#layout/activity_main">
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:id="#+id/hoursImage"
android:layout_marginRight="20dp"
android:src="#drawable/digit00" />
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:id="#+id/minutesImage"
android:layout_marginRight="20dp"
android:src="#drawable/digit00" />
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:id="#+id/secondsImage"
android:layout_marginRight="20dp"
android:src="#drawable/digit00" />
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:id="#+id/centiSecondsImage"
android:src="#drawable/digit00" />
</LinearLayout>
MainActivity
public class MainActivity extends AppCompatActivity {
ImageView hoursIMG, minutesIMG, secondsIMG, centiSecondsIMG;
TextView hoursText, minutesText, secondsText, centicsecondsText;
int centiseconds, seconds, minutes, hours ;
long startMS , endMS , elapsed ;
boolean timerRunning;
String [] digit = {"digit00","digit01","digit02","digit03","digit04","digit05","digit06","digit07","digit08","digit09", "digit10", "digit11","digit12","digit13","digit14","digit15","digit16","digit17","digit18","digit19","digit20", "digit21","digit22","digit23","digit24","digit25","digit26","digit27","digit28","digit29","digit30","digit31",
"digit32","digit33","digit34","digit35","digit36","digit37","digit38","digit39","digit40","digit41","digit42","digit43","digit44","digit45","digit46","digit47","digit48","digit49","digit50","digit51","digit52","digit53",
"digit54","digit55","digit56","digit57","digit58","digit59","digit60","digit61","digit62","digit63","digit64","digit65","digit66","digit67","digit68","digit69","digit70","digit71","digit72","digit73","digit74","digit75",
"digit76","digit77","digit78","digit79","digit80","digit81","digit82","digit83","digit84","digit85","digit86","digit87","digit88","digit89", "digit90","digit91","digit92","digit93","digit94","digit95","digit96","digit97","digit98","digit99"} ;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
//initializing values
centiseconds = 0; seconds = 0; minutes = 0; hours = 0;
startMS = 0; endMS = 0; elapsed = 0;
hoursIMG = (ImageView) findViewById(R.id.hoursImage);
minutesIMG = (ImageView) findViewById(R.id.minutesImage);
secondsIMG = (ImageView) findViewById(R.id.secondsImage);
centiSecondsIMG = (ImageView) findViewById(R.id.centiSecondsImage);
//start asynctask/stopwatch
timerRunning = true; new asyncTask().execute();
}
class asyncTask extends AsyncTask<Void, Void, Void> {
//initialize a variable to the current system time
#Override
protected void onPreExecute() {
startMS = System.currentTimeMillis();
}
#Override
protected Void doInBackground(Void... params) {
//timerRunning a varible to stop/start the timer
while (timerRunning) {
//initialize a 2nd variable to the current system time
endMS = System.currentTimeMillis();
//get the difference between the 2 time variables
elapsed = endMS - startMS;
//once it is greater than or equal to 100ms increment the centis, mins, secs, hrs
if (elapsed >= 100) {
//reset the starting variable to repeat the process. it also compensating if elapses is greater than 100ms
startMS = endMS - (elapsed - 100);
centiseconds++;
if (centiseconds > 9) {
centiseconds = 0;
seconds++;
if (seconds > 59) {
seconds = 0;
minutes++;
if (minutes > 59) {
minutes = 0;
hours++;
}
}
}
//call method to update the images
publishProgress();
}
}
return null;
}
#Override
protected void onProgressUpdate(Void... values) {
//get resource IDs for images that represent the values of hrs, mins, secs using the string array created earlier
int hourResID = getResources().getIdentifier(digit[hours], "drawable", getPackageName());
int minResID= getResources().getIdentifier(digit[minutes], "drawable", getPackageName());
int secResID= getResources().getIdentifier(digit [seconds], "drawable", getPackageName());
int csecResID= getResources().getIdentifier(digit[centiseconds], "drawable", getPackageName());
//set images of imageViews
centiSecondsIMG.setImageResource(csecResID);
secondsIMG.setImageResource(secResID);
minutesIMG.setImageResource(minResID);
hoursIMG.setImageResource(hourResID);
}
}
}
1:
Instead of burning your CPU and battery with a loop, you should use a CountDownTimer
new CountDownTimer(30000, 1000) { // 30sec, tick each second
public void onTick(long millisUntilFinished) {
publishProgress();
// mTextField.setText("seconds remaining: " + millisUntilFinished / 1000);
}
public void onFinish() {
mTextField.setText("done!");
}
}.start();
Or you can use a Runnable that you delay every time by the desired amount of time, let's say 200ms:
final static long REFRESH_RATE = 200L;
Handler mHandler = new Handler();
private final Runnable mRunnable = new Runnable() {
#Override
public void run() {
if (mStarted) {
long seconds = (System.currentTimeMillis() - t) / 1000;
statusBar.setText(String.format("%02d:%02d", seconds / 60, seconds % 60));
// cancel previous messages if they exist
handler.removeCallbacks(mRunnable);
handler.postDelayed(runnable, REFRESH_RATE);
}
}
};
start it:
mHandler.postDealyed(runnable, 0);
You can use a Timer with fixed rate:
new Timer().scheduleAtFixedRate(new TimerTask(){
#Override
public void run(){
publishProgress();
}
},0,1000);
Or you can use a ScheduledExecutorService, that will fix most of the problems you mentioned. See here and here.
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(1);
long lastSecondDisplayed = 0;
ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception {
long now = System.currentTimeMillis() / 1000;
// add this optimisation, so you don't calculate and
// for sure don't refresh your UI (even slower)
// if it's not needed:
if (lastSecondDisplayed != now) {
lastSecondDisplayed = now;
// calculate whatever you want
publishProgress();
}
return "Called!";
}
}, 1, TimeUnit.SECONDS);
Optimisations:
move the 4 getResources().getIdentifier(... lines out of onProgressUpdate and prepare the 10 digits only once in onCreate.
It is always good to reuse resources in java, because when you're creating and disposing them frequently, like here, you'll finish your memory quite fast and the GC will have to free some memory for you. Both creating the objects, and especially garbage-collecting them takes a fair amount of time. By creating them only once and reusing them you keep yourself far from all this trouble.
It seems it was a problem with continuously resetting the imageViews to different drawables. The getResources().getIdentifier() function calls also somewhat contributed to extra cpu usage and GarbageCleaner(GC) problems.
Instead of creating an image resource array I created a drawable array that I can continually reference. I created it in onCreate().
final Drawable[] drawable = {ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit00),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit01),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit02),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit03),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit04),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit05),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit06),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit07),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit08),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit09),
ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit10)}
Then I set the images on the imageViews using the Drawable
MainActivity.this.runOnUiThread(new Runnable() {
#Override
public void run() {
centiSecondsIMG.setImageDrawable(drawable[centiseconds]);
secondsIMG.setImageDrawable(drawable[seconds]);
minutesIMG.setImageDrawable(drawable[minutes]);
hoursIMG.setImageDrawable(drawable[hours]);
}
});
Memory and cpu are now all perfectly fine and working normally.
i sitll dont know why changing the imageViews images rapidly using setImageResource() caused problems with cpu,memory and GC.
Related
I want to show a circular progress bar with countdown timer.And the timer starts from 10 mins to 0.In the Textview i am showing timer and that is working fine.
But it is not reflecting in the progressbar.Progress bar is not at all changing.
Below is the code which i have tried.
public class MainActivity extends AppCompatActivity {
ProgressBar barTimer;
CountDownTimer countDownTimer;
TextView textTimer;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
barTimer = findViewById(R.id.barTimer);
textTimer = findViewById(R.id.textTimer);
barTimer.setProgress(100);
startTimer(10);
}
private void startTimer(final int minuti) {
countDownTimer = new CountDownTimer(60 * minuti * 1000, 500) {
#Override
public void onTick(long leftTimeInMilliseconds) {
long seconds = leftTimeInMilliseconds / 1000;
barTimer.setProgress((int)seconds);
textTimer.setText(String.format("%02d", seconds/60) + ":" + String.format("%02d", seconds%60));
}
#Override
public void onFinish() {
if(textTimer.getText().equals("00:00")){
textTimer.setText("STOP");
}
else{
textTimer.setText("2:00");
barTimer.setProgress(60*minuti);
}
}
}.start();
}
}
xml code
<TextView
android:id="#+id/textTimer"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ProgressBar
android:id="#+id/barTimer"
android:layout_below="#+id/textTimer"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:indeterminate="false"
android:progressDrawable="#drawable/circular_progress"
/>
Can anyone please tell me where it is wrong and how to solve this?
Your scaling factor is not correct and hence your maxProgess and setProgess values are also incorrect.There are two ways to fix this.
1.Set seconds = leftTimeInMilliseconds / 600 and not 1000.
Let's say X% = 1 second then 100% = 60 seconds,then X% = 100/60 seconds but we are calculating in milliseconds
hence X% = 100/(60*1000) = 1/600 milliseconds.
Also we have set 60 seconds for 100% hence multiply your setProgess and setMax by a factor 10.Since 600 seconds is 10 minutes.
barTimer.setProgress(1000);
barTimer.setMax(1000);
long seconds = leftTimeInMilliseconds / 600;
2.Similar to above rather than dividing by 600 divide 6000.
seconds = leftTimeInMilliseconds / 6000;
Let's say X% = 1 second then 100% = 600 seconds(10 mins),then X% = 100/600 seconds but we are calculating in milliseconds
hence X% = 100/(60*1000) = 1/6000 milliseconds.
Now since we are directly calculating for 10 mins no need to multiply your setProgess and setMax by any factor.
barTimer.setProgress(100);
barTimer.setMax(100);
long seconds = leftTimeInMilliseconds / 6000;
Also I would suggest setting countDownInterval to 1 sec rather 0.5 sec.
Hope this helps.
Please mark the answer as accepted if it helps.
So i run your code and found it was the time that you are giving is causing the issue , in reality progressbar is doing its work , but because you are giving the time of 10(minutes) initially the process is too slow that you cannot see it doing work , try changing it to 1 for the testing sake and you will see progress bar moving.
startTimer(10);
To
startTimer(1);
What I've built is a timer with multiple timer styles, using a spinner so the user can switch quickly between the timer they want to use.
My problem is with the switch statement - each timer works fine on its own but when going (example) from the Basic Timer to the Countdown Timer via the spinner, the first timer (Basic Timer) continues being run alongside the newly selected timer. In this example, the bug displays as 1 second down 1 second up 1 second down 1 second up etc...
My questions is: Is there a command that can be used to "kill" a function that I no longer want to continue running? If not, is there a better way I could have organized the code so the switch statements will keep each timer process separate? I have tried a Boolean check with no luck and breaking up the timer running-specific code differently has not worked for me so far.
Please see the relevant code below:
package com.mtag.app.muaythaiathletesguide;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Locale;
public class TimerActivity extends Activity implements AdapterView.OnItemSelectedListener {
private int seconds = 0; // Number of seconds passed
private boolean running; // Check whether timer is running
private boolean wasRunning;
private int timeCap = 0; // Custom max time, stop timer when reached and reset here for countdown
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_timer);
// Timer Selection Spinner
Spinner spinner = (Spinner) findViewById(R.id.timer_spinner);
// Create an ArrayAdapter using the string array and a default spinner layout
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.timer_spinner, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// Apply the adapter to the spinner
spinner.setAdapter(adapter);
// Spinner click listener
spinner.setOnItemSelectedListener(this);
// Restore activity's state by getting values from Bundle
if (savedInstanceState != null && running) {
seconds = savedInstanceState.getInt("seconds");
running = savedInstanceState.getBoolean("running");
wasRunning = savedInstanceState.getBoolean("wasRunning");
}
}
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id){
String selection = parent.getItemAtPosition(pos).toString();
// TODO: Remove Toast outputs after testing
// Call Timer types when corresponding position is chosen
switch(pos) {
case 0: // Basic Stopwatch: Count from 0:00:00 to 99:59:59 (or cap)
onDestroy();
running = false; // Stop clock
seconds = 0; // Reset seconds to zero
timeCap = seconds; // Set time cap to match seconds on the clock, for reset point
runBasicTimer();
break;
case 1: // Countdown: Count from 99:59:59 (or cap) to 0:00:00
Toast.makeText(parent.getContext(), "Selected: " + selection, Toast.LENGTH_LONG).show();
onDestroy();
running = false;
seconds = 1200; // Default cap 20:00:00
timeCap = seconds;
runCountdownTimer();
break;
case 2: // Tabata: Beep every 20th and 30th second. Reset to 0:00:00 on each 30th second
Toast.makeText(parent.getContext(), "Selected: " + selection, Toast.LENGTH_LONG).show();
running = false;
seconds = 0;
runTabataTimer();
break;
case 3: // Fight Gone Bad: 17min cap, beep on each minute
Toast.makeText(parent.getContext(), "Selected: " + selection, Toast.LENGTH_LONG).show();
running = false;
seconds = 0;
runFGBTimer();
break;
case 4: // "3 On 1 Off": Beep every 3rd and 4th minute
Toast.makeText(parent.getContext(), "Selected: " + selection, Toast.LENGTH_LONG).show();
running = false;
seconds = 0;
runThreeOneTimer();
break;
case 5: // "5 On 1 Off": Beep every 5th and 6th minute
Toast.makeText(parent.getContext(), "Selected: " + selection, Toast.LENGTH_LONG).show();
running = false;
seconds = 0;
runFiveOneTimer();
break;
default:
running = false;
seconds = 0;
Toast.makeText(parent.getContext(), "Error", Toast.LENGTH_LONG).show();
break;
}
}
public void onNothingSelected(AdapterView<?> parent){
// Another interface callback
}
#Override
// Save the state of variables
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt("seconds", seconds);
savedInstanceState.putBoolean("running", running);
savedInstanceState.putBoolean("wasRunning", wasRunning);
}
#Override
protected void onResume() {
super.onResume();
// If the stopwatch was running at stop, set it running again
if (wasRunning)
running = true;
}
#Override
protected void onPause() {
super.onPause();
// Record state of stopwatch, running or not running
wasRunning = running;
running = false;
}
public void onClickStart(View view) {
running = true; // Start stopwatch
}
public void onClickStop(View view) {
running = false; // Stop stopwatch
}
public void onClickReset(View view) {
seconds = timeCap; // Reset seconds to zero
}
private void runBasicTimer() {
final TextView timeView = (TextView)findViewById(R.id.time_view);
final Handler handler = new Handler();
handler.post(new Runnable() {
#Override
public void run() {
int hours = seconds / 3600;
int minutes = (seconds % 3600) / 60;
int secs = seconds % 60;
// Format time to hours, minutes, and seconds
String time = String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, secs);
timeView.setText(time);
if (running) {
seconds++;
}
// Don't allow timer to go over 99:59:59
if (seconds >= 359999) {
running = false;
Toast.makeText(getApplicationContext(), "Maximum time reached", Toast.LENGTH_LONG).show();
}
// Post code again with delay of one second
handler.postDelayed(this, 1000);
}
});
}
private void runCountdownTimer() {
final TextView timeView = (TextView)findViewById(R.id.time_view);
final Handler handler = new Handler();
handler.post(new Runnable() {
#Override
public void run() {
int hours = seconds / 3600;
int minutes = (seconds % 3600) / 60;
int secs = seconds % 60;
// Format time to hours, minutes, and seconds
String time = String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, secs);
timeView.setText(time);
if (running) {
seconds--;
}
// Don't allow timer to go under 0:00:00
if (seconds <= 1) {
running = false;
Toast.makeText(getApplicationContext(), "Maximum time reached", Toast.LENGTH_LONG).show();
}
// Post code again with delay of one second
handler.postDelayed(this, 1000);
}
});
}
private void runTabataTimer() {
}
private void runFGBTimer() {
}
private void runThreeOneTimer() {
}
private void runFiveOneTimer() {
}
}
And here's the .xml file:
<?xml version="1.0" encoding="utf-8"?>
<GridLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="#color/colorWhite"
tools:context="com.mtag.app.muaythaiathletesguide.TimerActivity">
<TextView
android:id="#+id/timer_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="#string/timer_style"/>
<Spinner
android:id="#+id/timer_spinner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="20dp"
android:background="#color/colorLightGrey"
android:minHeight="40dp" />
<TextView
android:id="#+id/time_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textAppearance="#android:style/TextAppearance.Large"
android:textSize="90sp" />
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="horizontal"
android:padding="16dp">
<Button
android:id="#+id/start_button"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="#color/colorTimerGreen"
android:onClick="onClickStart"
android:text="#string/start" />
<Button
android:id="#+id/stop_button"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="#color/colorTimerRed"
android:onClick="onClickStop"
android:text="#string/stop" />
<Button
android:id="#+id/reset_button"
android:layout_width="90dp"
android:layout_height="50dp"
android:layout_marginTop="8dp"
android:onClick="onClickReset"
android:text="#string/reset" />
</LinearLayout>
</GridLayout>
Your timer/countdown is running on another thread. That is okay by itself but comes with provisions that you have missed. For example, you are trying to set running and seconds when a new item is selected. Those two variables exist in the context of your UI thread. When they are used in the Runnable they are simply put, copied as final variables and used by the new thread. This happens when you reference a variable inside an anonymous class. This causes the Runnable to not see the variables you are changing because they are not the same anymore.
Now how to fix this?
Instead of passing an anonymous Runnable, you should hold a reference to it. Right now, you are creating a timer and just letting it go. That makes it very hard to control it as you can see.
Runnable activeTimer;
private void runBasicTimer() {
//... first remove any other activeTimer if exists and make sure it is cancelled properly (see below)
activeTimer = new Runnable() {//Your code}
handler.post(activeTimer);
}
This way you can always remove the activeTimer from the Handler like this.
This is still not complete. Because you also repost the runnable regularly. We also want to cancel that as well so that it is not reposted to the handler. To do this you should extend the Runnable class so that you can change running in it.
public class TimerRunnable extends Runnable {
public boolean mRunning = true;
#Override
public void run() {
//your code
if (mRunning) {
handler.postDelayed(this, 1000);
}
}
}
Now when you want to create a new timer:
activeTimer = new TimerRunnable(); //instead of just new Runnable()
Perfect! Now you can easily cancel it because you have access to its thread and you have access to the thread's mRunning variable. In your onItemSelected instead of just changing running you can do this:
if (activeTimer != null) {
activeTimer.mRunning = false;
}
I'm trying to port a PC Java program to the Android platform. The PC application uses a Swing.Timer to trigger an update every second. The associated listener, upon being called, gets new data from a database, then updates/redraws the screen using Graphics2D. I've learned how to use Android's Canvas to draw the same things that I do with the PC application. Now I'm trying to learn how to use the equivalent Timer in Android. Unfortunately things don't seem as straightforward on the Android platform. There are Timers, Handlers, AlarmManagers, and AsyncTasks. It would seem that AsyncTasks and AlarmManagers are more appropriate for one time (heavy duty?) tasks (right? wrong?) With regard to Timers and Handlers, I've seen many posts that say don't use Timer, use Handlers instead. I found the approach used in the code below somewhere out there on the web and tried it. It seems like it should do what I want but it hangs the GUI whenever I click the stop button. Does anyone know why it does that?
Thanks times a million
Bill
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
dateFormat = new SimpleDateFormat(dateFormatString);
mHandler = new Handler();
mUpdateTimeTask = new MyRunnable();
Button button = (Button) findViewById(R.id.start_button);
button.setOnClickListener(new MyStartListener());
button = (Button) findViewById(R.id.stop_button);
button.setOnClickListener(new MyStopListener());
}
class MyStartListener implements View.OnClickListener {
public void onClick(View v) {
if (startUptimeMillis == 0L) {
startUptimeMillis = SystemClock.uptimeMillis();
mHandler.removeCallbacks(mUpdateTimeTask);
mHandler.postDelayed(mUpdateTimeTask, 100);
}
}
};
class MyStopListener implements View.OnClickListener {
public void onClick(View v) {
mHandler.removeCallbacks(mUpdateTimeTask);
}
};
class MyRunnable implements Runnable {
public void run() {
final long start = startUptimeMillis;
long millis = SystemClock.uptimeMillis() - start;
int seconds = (int) (millis / 1000);
int minutes = seconds / 60;
seconds = seconds % 60;
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
TextView tv = (TextView) findViewById(R.id.time_textView);
tv.setText(dateFormat.format(calendar.getTime()));
mHandler.postAtTime(this, (((minutes * 60) + seconds + 1) * 1000));
}
};
EDIT:
The problem is that postAtTime needs an absolute time at which to start, not a delay which is what my example is using. (See postAtTime here)
So I replaced all of the timing code above with the below and it does what I want!!:
long millis = SystemClock.uptimeMillis();
mHandler.postAtTime(this, millis+1000);
I don't see how this could hang your app, unless you mean the start button doesn't work any more... Perhaps you want to add this to your stop listener:
public void onClick(View v) {
startUptimeMillis = 0l; // Reset startUptimeMillis
mHandler.removeCallbacks(mUpdateTimeTask);
}
As far as Timers, AsyncsTask, etc... You are correct, the best way to program an event in the near future in Android is with a Handler and Runnable. AlarmManagers are not intended for fast callbacks like in animations and AsyncTasks are better for heavy duty computation.
I would like a to offer a simpler update Runnable:
class MyRunnable implements Runnable {
public void run() {
// You should make this a class variable and initialize it in onCreate(),
// there is no need to search for the same View every second.
TextView tv = (TextView) findViewById(R.id.time_textView);
final long now = System.currentTimeMillis();
tv.setText(dateFormat.format(now));
mHandler.postAtTime(this, 1000 - (now - start) % 1000); // Accounts for millisecond offsets over time
// mHandler.postDelayed(this, 1000); // Effected by minute offsets
}
};
I think there is something that I'm just not yet getting about how these Views are to be coded. This is the third time I have had to come and post a question rearding them in the last 3 days! :S
Anyway, my problem is as follows.
edit:
This is the code inside a method that gets executed on a button press:
(Points of importance are noted)
>> setContentView(R.layout.stop);
>> timer = (TextView) findViewById(R.id.timer2);
GPSMain.button = (Button) findViewById(R.id.stopbutton);
startService(new Intent(context, Timer.class));
}
This is the Timer class that is executed in the "startService" call:
(Again points of importance are noted)
public class Timer extends Service {
static int totalSeconds = 0;
private int hour = 0;
private int min = 0;
private int sec = 0;
String mTimeFormat = "%02d:%02d:%02d";
final private Handler mHandler = new Handler();
static String timeTaken;
Context context = this;
Runnable mUpdateTime = new Runnable() {
public void run() { updateTimeView(); }
};
#Override
public void onCreate() {
Toast.makeText(context, "Timer Started", Toast.LENGTH_LONG).show();
mHandler.postDelayed(mUpdateTime, 1000);
}
public void updateTimeView() {
totalSeconds += 1;
sec += 1;
if(sec >= 60) {
sec = 0;
min += 1;
if (min >= 60) {
min = 0;
hour += 1;
}
}
>> timeTaken = String.format(mTimeFormat, hour, min, sec);
>> GPSMain.timer.setText("Time Taken: "+timeTaken);
mHandler.postDelayed(mUpdateTime, 1000);
}
#Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return null;
}
}
The logcat returns a NullPointerException on the following line of code:
GPSMain.timer.setText("Time Taken: "+timeTaken);
If I remove that line of code the code executes properly*, well I say properly because it executes all the way to the end of the application code, but the reason that I want to print the timer to the screen as it is counting is because I need to make sure that it is functioning correctly.
*not only does it run properly but it displays the timer text view with it's default defined string from the xml. It's only when I try to update it from the java that it crashes.
Here is the full xml file that the code is referencing at this point:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<Button
android:id="#+id/stopbutton"
android:layout_width="100px"
android:layout_height="100px"
android:text="Stop"
android:layout_centerInParent="true"
/>
<TextView
android:id="#+id/timer2"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Time Taken: unknown"
android:layout_below="#+id/timer"
/>
</RelativeLayout>
UPDATE!
The MyTimer Class:
#SuppressWarnings("unchecked")
public class MyTimer extends AsyncTask {
Timer _timerTask = new Timer();
static int totalSeconds = 0, hour = 0, min = 0, sec = 0;
static String mTimeFormat = "%02d:%02d:%02d";
static String timeTakenString;
#Override
protected Object doInBackground(Object... params) {
TimerTask timer = new TimerTask() {
#Override
public void run() {
totalSeconds += 1;
sec += 1;
if(sec >= 60) {
sec = 0;
min += 1;
if (min >= 60) {
min = 0;
hour += 1;
}
}
timeTakenString = String.format(mTimeFormat, hour, min, sec);
GPSMain.timer.setText("Time Taken: "+GPSMain.timeTaken);
}
};
(_timerTask).scheduleAtFixedRate(timer,1000,1000);
return null;
}
}
The method to start the timer thread:
void startService(){
setContentView(R.layout.stop);
timer = (TextView) findViewById(R.id.timer2);
GPSMain.button = (Button) findViewById(R.id.stopbutton);
new MyTimer().execute();
}
Why do you need a Service here? Why don't you just use AsyncTask? A Service is usually used when you need to run something in background, with no layout shown to user. Your application shows data to user, so the best way for you to solve your problem is to run a separate thread, not a service. Hope this helps.
You can use TimerTask instead of Service . Here is the code ..
TimerTask timer = new TimerTask() {
#Override
public void run() {
// Here you can update your UI
}
}
Timer _timerTask = new Timer();
_timerTask.scheduleAtFixedRate(timer,60000,60000);
Well, I seriously don't know much about services, but in all cases, you should be accessing UI only from the UI thread.
I your case, I feel the Activity that has the button and the Service are two different classes, with the service not possessing any object of the activity. I think the way you're setting the text view from a service may be an issue. but again, i don't know much about services.
On the other note, I agree with Egor, unless you have a lot of activities and each keeps doing a lot of network activity, AsyncTask is the way to go.
I've made a simple Android music player. I want to have a TextView that shows the current time in the song in minutes:seconds format. So the first thing I tried was to make the activity Runnable and put this in run():
int position = 0;
while (MPService.getMP() != null && position<MPService.duration) {
try {
Thread.sleep(1000);
position = MPService.getSongPosition();
} catch (InterruptedException e) {
return;
}
// ... convert position to formatted minutes:seconds string ...
currentTime.setText(time); // currentTime = (TextView) findViewById(R.id.current_time);
But that fails because I can only touch a TextView in the thread where it was created. So then I tried using runOnUiThread(), but that doesn't work because then Thread.sleep(1000) is called repeatedly on the main thread, so the activity just hangs at a blank screen. So any ideas how I can solve this?
new code:
private int startTime = 0;
private Handler timeHandler = new Handler();
private Runnable updateTime = new Runnable() {
public void run() {
final int start = startTime;
int millis = appService.getSongPosition() - start;
int seconds = (int) ((millis / 1000) % 60);
int minutes = (int) ((millis / 1000) / 60);
Log.d("seconds",Integer.toString(seconds)); // no problem here
if (seconds < 10) {
// this is hit, yet the text never changes from the original value of 0:00
currentTime.setText(String.format("%d:0%d",minutes,seconds));
} else {
currentTime.setText(String.format("%d:%d",minutes,seconds));
}
timeHandler.postAtTime(this,(((minutes*60)+seconds+1)*1000));
}
};
private ServiceConnection onService = new ServiceConnection() {
public void onServiceConnected(ComponentName className,
IBinder rawBinder) {
appService = ((MPService.LocalBinder)rawBinder).getService();
// start playing the song, etc.
if (startTime == 0) {
startTime = appService.getSongPosition();
timeHandler.removeCallbacks(updateTime);
timeHandler.postDelayed(updateTime,1000);
}
}
what about this:
int delay = 5000; // delay for 5 sec.
int period = 1000; // repeat every sec.
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask()
{
public void run()
{
//your code
}
}, delay, period);
Use a Timer for this (instead of a while loop with a Thread.Sleep in it). See this article for an example of how to use a timer to update a UI element periodically:
Updating the UI from a timer
Edit: updated way-back link, thanks to Arialdo: http://web.archive.org/web/20100126090836/http://developer.android.com/intl/zh-TW/resources/articles/timed-ui-updates.html
Edit 2: non way-back link, thanks to gatoatigrado: http://android-developers.blogspot.com/2007/11/stitch-in-time.html
You have to use a handler to handle the interaction with the GUI. Specifically a thread cannot touch ANYTHING on the main thread. You do something in a thread and if you NEED something to be changed in your main thread, then you call a handler and do it there.
Specifically it would look something like this:
Thread t = new Thread(new Runnable(){
... do stuff here
Handler.postMessage();
}
Then somewhere else in your code, you do
Handler h = new Handler(){
something something...
modify ui element here
}
Idea its like this, thread does something, notifies the handler, the handler then takes this message and does something like update a textview on the UI thread.
This is one more Timer example and I'm using this code in my project.
https://stackoverflow.com/a/18028882/1265456
I think the below blog article clearly gives a very nice solution. Especially, if you are a background service and want to regularly update your UI from this service using a timer-like functionality.
It really helped me, much more than the 2007 blog link posted by MusiGenesis above.
https://www.websmithing.com/2011/02/01/how-to-update-the-ui-in-an-android-activity-using-data-from-a-background-service/