martes, 14 de mayo de 2013

Cronómetro en Android

Una de las aplicaciones que podemos encontrar con facilidad en los móviles Android es el cronómetro. Además existen una gran variedad de ellos para descargar e instalar. Sin embargo, yo decidí hacer mi propia versión del mismo. Hace algún tiempo que hice una primera versión empleando la clase Handler para gestionar el hilo secundario. En esta ocasión, decidí utilizar la clase AsyncTask para dicha función, y además aproveché para hacer algunas mejoras.
A continuación podéis ver el código de mi aplicación, así como algunas imágenes de la misma en funcionamiento.

También podéis descargar el fichero para instalar a través de este enlace.

//// Actividad principal


package not.ebacelo.android.crono02;

import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.opengl.Visibility;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.*;

public class Crono02Activity extends Activity {
LinearLayout layTiempos, layTableHeader;
TextView tvCount, tvLaps;
Button btnStart, btnLap, btnStop, btnResume;
TareaCronometro tareaCronometro;

@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();

}

/**
 * Método que se ejecuta al crear la Actividad (se
 * debe consultar ciclo de vida de Actividad en caso
 * de dudas sobre este tema)
 */
@Override
public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.main);

//Definimos la orientación de la pantalla para
//que no se modifique. De otro modo debemos guardar
//el estado de la aplicación y restaurarla cada
//vez que se cambie la orientación.
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);

//Cargamos las referencias a los controles de la UI
initialize();

//Deshabilitamos los botones Stop y Lap hasta que se
//pulse el botón Start.
btnStop.setEnabled(false);
btnLap.setEnabled(false);
//Ocultamos los títulos de la tabla de tiempos por vuelta
layTableHeader.setVisibility(View.INVISIBLE);

//Creamos una nueva instancia de nuestra tarea AsyncTask
tareaCronometro = new TareaCronometro();

//Qué hacemos cuando se pulsa el botón Start
btnStart.setOnClickListener(new View.OnClickListener() {

public void onClick(View v) {
Button btn = (Button) v;

if (btn.getText().toString().compareTo("Start") == 0) {
//Habilitamos los botones Stop y Lap
btnStop.setEnabled(true);
btnLap.setEnabled(true);

if (tareaCronometro.getStatus() != AsyncTask.Status.RUNNING) {
//Arrancamos nuestra tarea asíncrona
tareaCronometro.execute();
}
//Iniciamos el cronómetro
tareaCronometro.iniciar();

} else {
if (btn.getText().toString().compareTo("Resume") == 0) {
//Continuamos el cronómetro cuando ha sido parado
tareaCronometro.continuar();
}
}
}
});

btnStop.setOnClickListener(new View.OnClickListener() {

public void onClick(View v) {
Button btn = (Button) v;

if (btn.getText().toString().compareTo("Stop") == 0) {
//Paramos el cronómetro (la tarea continua en
//ejecución).
tareaCronometro.parar();
} else {
if (btn.getText().toString().compareTo("Restore") == 0) {
//Reiniciamos el cronómetro
tareaCronometro.reiniciar();
//Deshabilitamos los botones Stop y Lap
btnStop.setEnabled(false);
btnLap.setEnabled(false);
//Ocultamos cabecera tabla tiempos
layTableHeader.setVisibility(View.INVISIBLE);
}
}
}
});

//Qué hacemos cuando se pulsa el botón Lap
btnLap.setOnClickListener(new View.OnClickListener() {

public void onClick(View v) {
//Registramos el tiempo actual
tareaCronometro.marcarTiempo();
layTableHeader.setVisibility(View.VISIBLE);
}
});

}

//Inicializamos las referencias a los controles de la
//interfaz de usuario de la Actividad.
private void initialize() {
tvCount = (TextView) findViewById(R.id.tvCount);
btnStart = (Button) findViewById(R.id.btnStart);
btnStop = (Button) findViewById(R.id.btnStop);
btnLap = (Button) findViewById(R.id.btnLap);
btnLap = (Button) findViewById(R.id.btnLap);
layTiempos = (LinearLayout) findViewById(R.id.layTiempos);
layTableHeader = (LinearLayout)findViewById(R.id.layTableHeader);

}

/**
 *
 * @param startTime
 * @param timeNow
 * @param timeStopped
 * @return String
 *
 * Este método nos devuelve en una cadena de texto con el
 * formato adecuado el tiempo transcurrido entre los
 * momentos indicados por los 2 primeros parámetros de
 * entrada. El tercer parámetro indica el tiempo a
 * descontar si el cronómetro estuvo parado durante el
 * intervalo definido por los dos parámetros anteriores.
 */
private String obtenerTiempoTranscurrido(long startTime, long timeNow,
long timeStopped) {
long diferencia, stopTime;
int hours, minutes, seconds, millis;
String strMillis;

diferencia = timeNow - startTime - timeStopped;
hours = (int) diferencia / 3600000;
diferencia = diferencia % 3600000;
minutes = (int) diferencia / 60000;
diferencia = diferencia % 60000;
seconds = (int) diferencia / 1000;
diferencia = diferencia % 1000;
millis = (int) diferencia;
try {
strMillis = String.format("%03d", millis).substring(0, 2);
millis = Integer.parseInt(strMillis);
} catch (IndexOutOfBoundsException e) {
Log.d("DEPURANDO", "IndexOutOfBoundsException catched");
}

String res = String.format("%d:%02d:%02d.%02d", hours, minutes,
seconds, millis);
return res;
}

/**
 *
 * @author ebacelo
 *
 * Clase interna derivada de AsyncTask que nos permite realizar una
 * tarea en un hilo secundario.
 */
class TareaCronometro extends AsyncTask<Void, String, Void> {
private boolean salir = false;
private boolean parado = true;
private boolean reiniciado = false;
private boolean lap = false;
private boolean iniciar = false;
private long timeStart = 0;
private long resumeTime = 0;
private long stopTime = 0;
private long timeStopped = 0;
private long timeNow = 0;
private long timeLastLap = 0;
private long timeLap = 0;
private int lap_num = 1;

public void iniciar() {
// e0
if (parado != false) {
parado = false;
timeStart = System.currentTimeMillis();
timeLastLap = timeStart;
timeStopped = 0;
}
}

public void parar() {
parado = true;
stopTime = System.currentTimeMillis();
// Ocultamos el botón Lap
// Cambiamos el texto de Stop a Restore, y el de Start a Resume
btnLap.setVisibility(View.INVISIBLE);
btnStart.setText("Resume");
btnStop.setText("Restore");
}

public void continuar() {
btnStart.setText("Start");
btnStop.setText("Stop");
btnLap.setVisibility(View.VISIBLE);
parado = false;
resumeTime = System.currentTimeMillis();
timeStopped += resumeTime - stopTime;
}

public void reiniciar() {
btnStart.setText("Start");
btnStop.setText("Stop");
btnLap.setVisibility(View.VISIBLE);
////
stopTime = System.currentTimeMillis();
timeStopped = 0;
parado = true;
iniciar = true;
reiniciado = true;

//Limpiamos los tiempos de vuelta
layTiempos.removeAllViews();
lap_num = 1;
}

@Override
protected void onCancelled() {
super.onCancelled();

salir = true;
finish();
}

//Tareas a realizar después de comenzar la tarea en el
//hilo secundario.
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
}

//Tareas a realizar antes de comenzar la tarea en el
//hilo secundario.
@Override
protected void onPreExecute() {
super.onPreExecute();
parado = true;
iniciar = false;
}

//Este método se ejecutará en el hilo de la interfaz de usuario
//cada vez que desde el método doInBackground() se invoque el
//método publishProgress(). De este modo podemos transmitir
//información entre los dos hilos de ejecución de forma
//sencilla.
@Override
protected void onProgressUpdate(String... values) {

super.onProgressUpdate(values);
// Si no esta parado actualizamos el tiempo en la pantalla
// (obtenerTiempoTranscurrido)

//Mostramos el tiempo transcurrido en la pantalla del
//cronómetro.
tvCount.setText(values[0]);

if (lap) {
// Añadimos en la lista de vueltas el tiempo que va a ser
// publicado
TextView tvTiempoVuelta = new TextView(layTiempos.getContext());
tvTiempoVuelta.setBackgroundColor(Color.argb(45, 55, 120, 55));

if (lap_num % 2 == 0)
tvTiempoVuelta.setTextColor(Color.argb(100, 33, 230, 230));
else
tvTiempoVuelta.setTextColor(Color.argb(100, 230, 230, 33));

tvTiempoVuelta.setTextSize(20);
tvTiempoVuelta.setPadding(0, 1, 0, 1);

//Calcular el tiempo de la vuelta con los datos recibidos
timeNow = Long.parseLong(values[1]);
timeLap = timeNow - timeLastLap;
tvTiempoVuelta.setText(fijarLongCadena(String.valueOf(lap_num),2) + " - " +
values[0] + "   ---   " + String.valueOf(obtenerTiempoTranscurrido(0, timeLap, 0)));

//Añadimos el nuevo tiempo de vuelta al layout del interfaz
//de usuario para visualizarlo.
layTiempos.addView(tvTiempoVuelta, 0);
timeLastLap = timeNow;
lap_num++;
lap = false;
}
}


//El contenido de este método son las tareas que se
//realizarán en el hilo secundario (sólo estas, todos
//los demás métodos se ejecutan en el hilo de la
//interfaz de usuario).
@Override
protected Void doInBackground(Void... arg0) {

while (!salir) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!parado) {
timeNow = System.currentTimeMillis();
String values[] = new String[2];
if (reiniciado) {
timeStart = timeNow;
reiniciado = false;
}
values[0] = obtenerTiempoTranscurrido(timeStart,
timeNow, timeStopped);
values[1] = String.valueOf(timeNow);

publishProgress(values);
}

if (iniciar) {
timeStopped = 0;
timeLastLap = 0;
timeNow = timeStart;
String values[] = new String[2];
values[0] = obtenerTiempoTranscurrido(timeStart,
timeNow, timeStopped);
values[1] = String.valueOf(timeNow);
publishProgress(values);
iniciar = false;
parado = true;
}

}
return null;
}

//Permite indicar al hilo secundario que hay que marcar un tiempo
public void marcarTiempo() {
lap = true;

return;
}

public String fijarLongCadena(String cadena, int longitud) {
String resultado = "";
String relleno = "";

if (cadena.length() < longitud) {
for (int i=0; i< longitud - cadena.length(); i++) {
relleno = relleno.concat("  ");
}
resultado = relleno.concat(cadena);
Log.d("DEPURANDO", "Cadena1: |" + resultado + "|");
return resultado;
}

Log.d("DEPURANDO", "Cadena2: |" + cadena + "|");
return cadena;
}
}

}

//// Layout de la actividad



<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:id="@+id/linearLayout1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dip"
        android:gravity="center" >

        <TextView
            android:id="@+id/tvCount"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dip"
            android:layout_marginTop="5dip"
            android:text="0:00:00.00"
            android:textColor="#ff22ee22"
            android:textSize="44sp" />
    </LinearLayout>


    <LinearLayout
        android:id="@+id/linearLayout2"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/btnStart"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="0.5"
            android:padding="10dip"
            android:text="Start"
            android:textSize="24sp" />

        <Button
            android:id="@+id/btnStop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dip"
            android:layout_marginTop="5dip"
            android:layout_weight="0.5"
            android:padding="10dip"
            android:text="Stop"
            android:textSize="24sp" />

        <Button
            android:id="@+id/btnLap"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dip"
            android:layout_marginLeft="5dip"
            android:layout_marginTop="5dip"
            android:layout_weight="0.5"
            android:padding="10dip"
            android:text="Lap"
            android:textSize="24sp" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/layTableHeader"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="#304930" >

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="2dp"
            android:text="Lap"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#eeeeee" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="14dp"
            android:text="Absolute time"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#eeeeee" />

        <TextView
            android:id="@+id/textView3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="36dp"
            android:text="Lap time"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#eeeeee" />
     
    </LinearLayout>
 
    <ScrollView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >

        <LinearLayout
            android:id="@+id/layTiempos"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:orientation="vertical"
            android:padding="4dip" >
        </LinearLayout>
    </ScrollView>

</LinearLayout>

//// AndroidManifest.xml



<manifest android:versioncode="1" android:versionname="1.0" package="not.ebacelo.android.crono02" xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-sdk android:minsdkversion="3">

    <application android:icon="@drawable/chrono2" android:label="@string/app_name">
        <activity android:configchanges="orientation|keyboardHidden|keyboard" android:label="@string/app_name" android:name=".Crono02Activity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN">

                <category android:name="android.intent.category.LAUNCHER">
            </category></action></intent-filter>
        </activity>
    </application>

</uses-sdk></manifest>

6 comentarios:

  1. Estaría bien que colgases el apk.

    Me apetece probarlo sin tener que reescribirlo.

    Un saludo.

    ResponderEliminar
  2. Ya tienes un enlace para descargar el apk. Esta no es muy original que digamos. Espero publicar aquí algunas más en las que he estado trabajando y ya me dirás qué te parecen.

    ResponderEliminar
  3. amigo tengo una duda con el programa por que al cerrar la app y volverlo abrir la aplicación no empieza a contar de nuevo el cronometro, me parece que el problema es el AsyncTask que queda ejecutandose y no se cierra me podrias ayudar?

    ResponderEliminar
    Respuestas
    1. en el apk funciona bien , pero al hacerlo segun la web sucede eso "al cerrar la app y volverlo abrir la aplicación no empieza a contar de nuevo el cronometro" ....tienes la solucion??
      gracias

      Eliminar
  4. O si podrias pasarme el proyecto te lo agradeceria .)

    ResponderEliminar
  5. Hola, gracias por compartir el código. Te hago una consulta, si quisiera que cuando le de pause en determinado segundo haga algo, como hago para definir el numero del segundo, es decir por ej si quiero poner if(timeStopped==1){btnStart.setText("Un segundo")} . Es decir cómo se qué número equivale a un segundo?

    ResponderEliminar