sexta-feira, 28 de dezembro de 2012

Time's Up

De volta à plataforma Android desta vez para fazer um pequeno programa que vai permitir cronometrar e guardar tempos.

A interface é simples:



O botão Guardar vai permitir adicionar o tempo atual aos tempos intermédios. O botão Iniciar vair intercalar a interface entre a hora atual e o cronometro. O botão Recomeçar vai recolocar o temporizador a zero.

Sempre que carregar no botão Guardar o tempo do cronometro é adicionado aos tempos intermédios o que  implica ter uma área que se pode deslizar para cima e para baixo uma vez que não existe limite no número de tempos que podemos adicionar.


Os desafios deste pequeno programa são dois:
- primeiro a atualização da interface implica criar uma thread separada, de outro modo o sistema não atualiza a informação mostrada ao utilizador.
- segundo temos os cálculos com tempo, neste aspeto encontrei um problema inesperado! O programa apresenta resultados diferentes quando executado no emulador de quando é executado no dispositivo real. Ainda não percebi porquê mas testei com duas versões diferentes do emulador e os resultados foram os mesmos, mas quando executei num Huawei com a versão 4.0.3 do Android os resultados não coincidiam com  os apresentados pelo emulador.

A interface

Vamos começar pela interface. O código é:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >

    <Button
        android:id="@+id/bt_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:onClick="bt_guardar_click"
        android:text="@string/guardar" />

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@+id/bt_start"
        android:layout_marginTop="16dp"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@+id/textView1"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <Button
        android:id="@+id/bt_iniciar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/bt_start"
        android:onClick="bt_iniciar_click"
        android:text="@string/inicio" />

    <Button
        android:id="@+id/bt_continuar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/bt_iniciar"
        android:onClick="bt_continuar_click"
        android:text="@string/reiniciar" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/textView2"
        android:layout_alignBottom="@+id/textView2"
        android:layout_toRightOf="@+id/bt_start"
        android:text="@string/intermedios"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</RelativeLayout>
</ScrollView>


Resumindo temos:
- 1 scrollview para podermos deslizar o conteúdo
- 3 botões
- 3 Textviews: 1 para mostrar a hora ou o tempo cronometrado, 1 para o texto "Intermédios" e por fim para os tempos intermédios guardados.

O código

Em relação ao código vamos apresentar a função on create

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_times_up);
        tv1=(TextView) findViewById(R.id.textView1);
        tv2=(TextView) findViewById(R.id.textView2);
        iniciado=false;

        //thread que vai atualizar a UI
        Thread th = new Thread(){
        @Override
        public void run(){
        while(true){
        try {
Thread.sleep(250);
} catch (InterruptedException e) {
e.printStackTrace();
}
        runOnUiThread(new Runnable(){
        public void run(){
        String t;
        if(!iniciado){
                 cal = Calendar.getInstance();
               t=String.format("%02d:%02d:%02d",cal.get(Calendar.HOUR_OF_DAY),cal.get(Calendar.MINUTE),cal.get(Calendar.SECOND));
        tv1.setText(t);
        }else{
                 int m=0,h=0,s=0;
               
                 cal = Calendar.getInstance();
                 cal.setTimeInMillis(cal.getTimeInMillis()-inicio.getTimeInMillis());
                 //h=cal.get(Calendar.HOUR_OF_DAY);
                 h=(int)cal.getTimeInMillis()/(1000*60*60);
        m=cal.get(Calendar.MINUTE);
        s=cal.get(Calendar.SECOND);
        t=String.format("%02d:%02d:%02d",h,m,s);
        tv1.setText(t);// + "-" + Long.toString(mil));
    } //if else
        }//run
        });//runOnUiThread
        }//whiler
        }//run
        };//thread
        th.start();
    }//oncreate

Este código cria um ciclo infinito que vai atualizar e apresentar a hora ou o tempo cronometrado de acordo com a variável "iniciado" que indica se o cronometro foi ou não iniciado.

A linha de código que está comentada é a que apresenta um valor diferente quando executada no emulador de quando executada no dispositivo real. Esta linha devia calcular quantas horas correspondem ao valor de milissegundos calculados como diferença entre o tempo inicial do cronometro e o atual. No emulador o valor é calculado corretamente como sendo zero inicialmente enquanto que no dispositivo começa em um!! Vai-se lá saber porquê.
Esta é a imagem do programa a correr no dispositivo real.
 E esta é a imagem do programa a correr no emulador.
Para resolver este problema decidi calcular a hora de outro modo utilizando a linha

                 h=(int)cal.getTimeInMillis()/(1000*60*60);


Por fim criei um menu com duas opções:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/item1" android:title="Sobre"></item>
    <item android:id="@+id/item2" android:title="Sair"></item>
</menu>

O código que cria o menu e responde às opções é apresentado a seguir:
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_times_up, menu);
        return true;
    }
    //opções do menu
    public boolean onOptionsItemSelected(MenuItem item) {
if(item.getTitle().toString().equals("Sobre"))
showMessage("Paulo Ferreira");
if(item.getTitle().toString().equals("Sair")){
showMessage("That's all folks!");
finish();
}
return true;
  }

A primeira função é criada pelo Eclipse automaticamente enquanto que a segunda é a que reage ao clique nas opções dos menus.
A função showMessage é foi apresentada noutros projetos e somente cria uma Toast com o texto que lhe foi passado:
    /* mostra o texto passado numa pequena mensagem */
    private void showMessage(CharSequence text) {
    Context context = getApplicationContext();
int duration = Toast.LENGTH_SHORT;
//faz aparecer uma mensagem pequena durante um certo tempo
Toast toast = Toast.makeText(context, text, duration);
toast.show();    
    }

Por fim temos as funções que implementam a lógica dos três botões:
    //botão guardar
    public void bt_guardar_click(View v){
    String t;
   
    t=(String) tv2.getText();
    t = t + "\n" + tv1.getText();
    tv2.setText(t);
    }
    //botão iniciar
    public void bt_iniciar_click(View v){
    if(inicio==null)
    inicio = Calendar.getInstance();
    iniciado=!iniciado;
    }
    //botão recomeçar
    public void bt_continuar_click(View v){
    inicio = Calendar.getInstance();
    }

Um dos problemas que ainda temos para resolver é o que acontece quando mudamos a orientação do dispositivo, uma vez que o programa é reiniciado o tempo do temporizador perdido. Assim podemos fixar a orientação em vertical (portait) com a seguinte linha:
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

Basta adicionar no inicio da função onCreate.

Uma funcionalidade que fica para depois, espero que não muito depois, é adicionar a possibilidade de guardar os tempos para mais tarde poder recarregar e comparar.

Como sidekick temos uma caraterística interessante que permite que o programa mesmo não estando aberto em primeiro plano continua a calcular o tempo no cronometro graças ao modo como o Android executa os programas, assim podemos ter o programa aberto a executar em modo cronometro e atender chamadas ou executar outro programa que quando voltamos o tempo continua.


Sem comentários:

Enviar um comentário