Mostrando postagens com marcador Activity. Mostrar todas as postagens
Mostrando postagens com marcador Activity. Mostrar todas as postagens

quarta-feira, 26 de outubro de 2011

Android: Tratando mudança de orientação

Olá povo,

Chegaram 4 novos estagiários aqui no projeto e coube "a minha pessoa" orientá-los. Durante a aula, o assunto enveredou para tratamento de mudança de orientação do aparelho (portrait e landscape | retrato e paisagem).
Por padrão, ao girar o telefone o método onCreate é chamado novamente e com isso, dados que foram carregados dinamicamente (como itens de um ArrayList que estão preenchendo um ListView) são perdidos.

Vejamos o exemplo abaixo:

Arquivo de layout.
<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:orientation="vertical"
 android:background="#0000FF">

 <TextView
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:text="Digite o nome" />

 <EditText
   android:id="@+id/editText1"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"/>

 <Button
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:onClick="meuBotaoClick"
   android:text="Adicionar" />

 <ListView
   android:id="@+id/listView1"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent" />
</LinearLayout>

Classe da Activity
public class TelaPrincipalActivity
 extends Activity {

 EditText edt;
 ArrayList<String> nomes;
 ArrayAdapter<String> adapter;

 @Override
 public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.main);

   edt = (EditText) findViewById(R.id.editText1);

   ListView listView = (ListView) 
     findViewById(R.id.listView1);
   adapter = new ArrayAdapter<String>(this,
     android.R.layout.simple_list_item_1, nomes);

   listView.setAdapter(adapter);
 }

 public void meuBotaoClick(View v) {
   nomes.add(edt.getText().toString());
   edt.setText("");
   adapter.notifyDataSetChanged();
 }
}

No código acima, ao clicar no botão, o texto digitado no EditText é inserido na lista, e em seguida o adapter é atualizado através do método notifyDataSetChanged.
O exemplo é bem simples, mas se você girarmos o aparelho os dados que estão sendo exibidos são perdidos, uma vez que a lista inicia vazia e (como já falei) o onCreate é chamado novamente.

Para isso não acontecer você tem 3 alternativas:

1)Forçar uma orientação
Na declaração da sua Activity, no AndroidManifest.xml, você pode usar a propriedade android:screenOrientation para manter sua aplicação em uma orientação específica (portrait/landscape).
<activity
 android:name="TelaPrincipalActivity"
 android:screenOrientation="portrait" />


2)Avisar o Android para não chamar o onCreate
Essa é a melhor opção quando você quer utilizar a tela nas duas orientações e impedir que o Android chame o onCreate cada vez que você girar o aparelho. Para isso basta dizer que ao mudar a configuração (configChanges) de orientação, ele não recrie a Activity.
<activity
 android:name="TelaPrincipalActivity"
 android:configChanges="orientation|keyboardHidden|screenSize"/>


3) Salvar o estado da Activity
A opção número dois é perfeita quando você quer usar o mesmo layout nas duas orientações. Mas, e se eu quiser usar layouts diferentes? Neste caso você dever permitir que a Activity seja recriada. Entretanto, devemos salvar seu estado com o método onSaveInstanceState e no onCreate recuperar esse estado.

No exemplo que fiz os "estags", criei a pasta "layout-land" com o mesmo layout definido acima, só mudando o background (apenas para notarmos os diferentes layouts). Depois implementamos o método onSaveInstanceState para salvar os itens da lista.
@Override
protected void onSaveInstanceState(Bundle outState) {
 super.onSaveInstanceState(outState);
 outState.putStringArrayList("nomes", nomes);
}

Se vocês notaram, o método onSaveInstanceState usa um objeto da classe Bundle para salvar o estado. Esse estado é passado para o método onCreate da Activity como parâmetro, que utilizaremos para restaurar o seu estado.
@Override
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 // Código já apresentado
 if (savedInstanceState != null) {
   nomes = savedInstanceState.
    getStringArrayList("nomes");

 } else {
   nomes = new ArrayList<String>();
 }
}


Com isso podemos ter, para a mesma tela, um layout diferente para cada orientação, desde que tenhamos os mesmos componentes (e com os mesmos ids).

Ficou com dúvida? Deixe seu comentário.

4br4ç05,
nglauber

sexta-feira, 26 de agosto de 2011

Fazendo uma tela de splash

Olá povo,

É muito comum vermos nas aplicações a famosa "tela de splash", ou tela de loading. É nela que a aplicação dá um feedback pro usuário informando que os recursos necessários para execução da aplicação estão sendo carregados. Após verificar uma dessas telas elaborada por uma equipe do projeto em que trabalho, resolvi dar minha sugestão de como implementá-la.

[EDITADO 08/02/2016]
O código a seguir é basicamente o que está sendo descrito nesse post do Ian Lake.
https://plus.google.com/+AndroidDevelopers/posts/Z1Wwainpjhd

Basicamente você precisa definir um tema para sua Activity.
<style name="AppTheme.Launcher">
  <item name="android:windowBackground">@drawable/launch_screen</item>
</style>
Em seguida defina esse tema na sua activity MAIN/LAUNCHER (no AndroidManifest.xml). Perceba que "launch_screen" não precisa ser necessariamente uma imagem PNG, pode ser um XML como a seguir.
<layer-list 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:opacity="opaque">
  <!-- Uma cor de background, preferencialmente a mesma do tema -->
  <item android:drawable="@android:color/white"/>
  <!-- Logo do app. Imagem de 144dp -->
  <item>
    <bitmap
      android:src="@drawable/product_logo_144dp"
      android:gravity="center"/>
  </item>
</layer-list>
Depois é só mudar o tema em tempo de execução para o tema que você preferir.
public class MyMainActivity extends AppCompatActivity {
 @Override
  protected void onCreate(Bundle savedInstanceState) {
    // Essa linha tem que ser chamada antes do super.onCreate
    setTheme(R.style.Theme_MyApp);
    super.onCreate(savedInstanceState);
    // ...
  }
}

[/EDITADO]
[EDITADO 20/08/2016]
Ótima apresentação do Cyril Mottier sobre splash screens.
http://cyrilmottier.com/2016/06/20/launch-screens-from-a-tap-to-your-app/
[/EDITADO]

Essa implementação não é recomendada (ao contrário da mostrada acima), mas já precisei fazer por exigência de cliente.
Vamos começar pela classe que realiza esse trabalho.
public class SplashActivity extends Activity
 implements Runnable {
 private Handler handler;

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

   handler = new Handler();
   handler.postDelayed(this, 2000);
 }

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

   handler.removeCallbacks(this);
 }

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

   run(); 
 }

 @Override
 public void run() {
   // Faça o carregamento necessário aqui...
   
   // Depois abre a atividade principal e fecha esta
   Intent it = new Intent(
     this, PrincipalActivity.class);
   startActivity(it);

   finish();

   overridePendingTransition(
     android.R.anim.fade_in, android.R.anim.fade_out);
 }
}

A Activity está implementando Runnable para carregaremos os recursos da aplicação em uma Thread separada, uma vez que não queremos dar a impressão de que ela está travada. Em seguida, temos um atributo do tipo Handler, ele é utilizado para atualizar os componentes da tela (informando o progresso por exemplo) através de outra Thread, uma vez que é uma restrição do Android permitir que apenas a Thread da UI (ou seja, a Thread principal) faça isso.
Ao criar a atividade (onCreate), inicializamos o Handler e agendamos a execução da Thread de carregamento dos recursos para ser executada daqui a 2 segundos (2000 milissegundos). Como não estou carregando nada nesse exemplo, estou usando esse valor pra dar tempo de ver a splash. Na prática, esse valor deve ir para um valor mais baixo (0,5 segundo por exemplo). No método onPause, eu estou tratando o caso de o usuário clicar na tecla "Back" durante o carregamento da splash. Caso isso aconteça, o carregamento é cancelado.
No método run, que será executado em background, devemos fazer o carregamento dos recursos da aplicação. Em seguida, criamos a Intent para a tela principal da aplicação e a chamamos com o startActivity. Por fim, finalizamos a tela de splash com o método finish.
Um recurso interessante que coloquei nesse exemplo foi a mudança da transição padrão entre as telas. Isso é feito através do método overridePendingTransition, que recebe uma animação para a tela que será exibida e outra para a tela q está sendo fechada. Nesse exemplo, usei um efeito de transparência (Fade In e Fade Out) da classe R do próprio Android. Se quiser fazer a sua própria, eu coloquei um post sobre isso aqui no blog.
De código é só. O que não podemos esquecer, é de declarar a Activity no AndroidManifest.xml, e as telas de Splash e Principal têm uma configuração interessante.
<activity
 android:name=".SplashActivity">
 <intent-filter>
   <action 
      android:name="android.intent.action.MAIN" />
   <category 
      android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
</activity>

<activity
 android:name=".PrincipalActivity">
 <intent-filter>
   <action 
      android:name="android.intent.action.MAIN" />
   <category 
      android:name="android.intent.category.DEFAULT" />
 </intent-filter>
</activity>
Notem que as duas atividades têm a ação android.intent.action.MAIN que informa ao Android que elas são pontos de partida da aplicação. Entretanto, a tela de Splash tem a categoria android.intent.category.LAUNCHER, enquanto que a principal tem a categoria android.intent.category.DEFAULT. O que isso significa?
Ao clicar no ícone no menu principal de aplicações do Android (Launcher) a SplashActivity é executada (isso graças a categoria LAUNCHER), e como vimos, em seguida ela chama a PrincipalActivity e se finaliza (com o finish).
Quando clicamos na tecla "Back" na tela Principal, a aplicação "morreu", e se a executarmos novamente a splash será aberta novamente. Porém, se saírmos da aplicação com a tecla "Home", a PrincipalActivity deve ficar lá suspensa, para que se o usuário clique no ícone da aplicação novamente, a tela principal (que foi aberta anteriormente) seja exibida. Isso funciona graças a categoria DEFAULT que foi passada para a PrincipalActivity.
Por hoje é só pessoal. Qualque dúvida, deixem seus comentários.
[EDITADO 15/03/2016]
Recentemente tive que fazer a tela de splash da maneira não recomendada, mas ela precisava ocultar também a barra de status e os botões de navegação. Sendo assim, precisei colocar o código a seguir no onCreate.
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
        | View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);

setContentView(R.layout.splash_layout);
[/EDITADO]
4br4ç05,
nglauber
P.S.: O exemplo acima pode ser feito com AsyncTask também. Fica o desafio pra quem quiser ;)