segunda-feira, 29 de agosto de 2011

Armazenando configurações no Android

Olá povo,

É muito comum armazenar configurações simples das nossas aplicações para que o usuário não tenha que setá-las toda vez que abrir o programa. No Android, podemos fazer isso através da interface SharedPreferences, com ela podemos salvar grupos de configurações para recuperá-las posteriormente.

// Obtém a instância da SharedPreference

SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(this);

// Salva dois valores nas preferências
SharedPreferences.Editor editor = prefs.edit();
editor.putString("operadora", "Claro");
editor.putBoolean("som", false);
editor.commit();

// Recupera os valores
String op = prefs.getString("operadora", null);
boolean som = prefs.getBoolean("som", false);


Ao salvar esses valores eles serão armazenados na pasta /data/data/pacote.da.sua.app/shared_prefs. O valores "Claro" e false serão armazenados para as configurações "operadora" e "som" respectivamente, e estarão lá mesmo que o usuário feche a aplicação. Eles só são apagados através do menu Configurações> Aplicações > Gerencias Aplicações > [Sua app] > Limpar dados.

Mas se eu quiser criar uma tela de configurações? O Android já tem uma API para facilitar (e padronizar) a criação de telas para esse propósito. Podemos defini-la como abaixo:

<PreferenceScreen

xmlns:android="http://schemas.android.com/apk/res/android"
<CheckBoxPreference
android:summaryOff="Som desabilitado"
android:key="som"
android:summaryOn="Som habilitado"
android:title="Som" />
<EditTextPreference
android:key="operadora"
android:dialogTitle="Operadora"
android:dialogMessage="Digite o nome da operadora"
android:title="Operadora" />
</PreferenceScreen>

Eu nomeei esse arquivo como tela_config.xml e salvei em res/xml. Notem que os atibutos android:key são iguais aos que foram especificados no código Java anterior. Para carregar esse arquivo, devemos ter uma classe que herda de PreferenceActivity como a seguir.

public class PreferenciasActivity

extends PreferenceActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.tela_config);
}
}

Você pode chamar essa tela como uma Activity comum, e quando os valores dessa tela forem alterados, automaticamente os valores já são salvos na SharedPreference, e consequentemente podem ser obtidos com o código mostrado mais acima. Dessa forma, não precisamos utilizar o SharedPreference.Editor para salvar as configurações ;)





Bem pessoal, esse foi um post bem rápido. Qualquer dúvida deixem seus comentários.

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 ;)

domingo, 21 de agosto de 2011

Galeria de mídia do Android

Olá povo,

Neste post vou mostrar como tirar uma foto com a aplicação de câmera, salvar a imagem em um diretório temporário e em seguida adiciona-la à galeria de mídia do aparelho. E por último, vou mostrar como carregar uma imagem da galeria.
Vamos iniciar mostrando como chamar a aplicação de câmera.
public void tirarFotoClick(View v) {
  String nomeFoto = DateFormat.format(
    "yyyy-MM-dd_hhmmss", new Date()).toString();

  caminhoFoto = new File(
    Environment.getExternalStoragePublicDirectory(
      Environment.DIRECTORY_PICTURES),
    nomeFoto);

  Intent it = new Intent(
    MediaStore.ACTION_IMAGE_CAPTURE);  
  it.putExtra(MediaStore.EXTRA_OUTPUT, 
    Uri.fromFile(caminhoFoto));
  startActivityForResult(it, 0);
}
A aplicação de câmera é chamada através da ação ACTION_IMAGE_CAPTURE passado no construtor da Intent. Um detalhe interessante aqui é o parâmetro EXTRA_OUTPUT, ele serve para indicar um caminho para a foto ser salva. Sem ele, a imagem será salva com tamanho e qualidade inferior a que a câmera realmente tirou.

caminhoFoto é um atributo do tipo File.
A chamada do método startActivityForResult indica que queremos tratar o resultado da activity que estamos chamando. Esse tratamento é feito no método onActivityResult.
protected void onActivityResult(
  int requestCode, int resultCode, Intent data) {
  super.onActivityResult(
    requestCode, resultCode, data);

  if (resultCode == RESULT_OK && requestCode == 0) {
    ImageView img = (ImageView)
      findViewById(R.id.imageView1);
   
    // Obtém o tamanho da ImageView
    int targetW = img.getWidth();
    int targetH = img.getHeight();
    
    // Obtém a largura e altura da foto
    BitmapFactory.Options bmOptions =
      new BitmapFactory.Options();
    bmOptions.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(
      caminhoFoto.getAbsolutePath(), bmOptions);

    int photoW = bmOptions.outWidth;
    int photoH = bmOptions.outHeight;
    
    // Determina o fator de redimensionamento
    int scaleFactor = Math.min(
      photoW/targetW, photoH/targetH);
    
    // Decodifica o arquivo de imagem em 
    // um Bitmap que preencherá a ImageView
    bmOptions.inJustDecodeBounds = false;
    bmOptions.inSampleSize = scaleFactor;
    bmOptions.inPurgeable = true;
    
    Bitmap bitmap = BitmapFactory.decodeFile(
      caminhoFoto.getAbsolutePath(), bmOptions);
    img.setImageBitmap(bitmap);
  }
}
Quando chamamos a câmera, no startActivityForResult, passamos o valor 1 como segundo parâmetro, esse valor representa o código da requisição (requestCode). Esse valor é retornado no método onActivityResult para saber se o resultado que estamos tratando é daquela requisição. No if que fizemos, além do requestCode, checamos o resultCode. O resultCode descreve o resultado da operação, que neste caso é tirar a foto. Se o resultado for RESULT_OK, é porque uma foto foi tirada.
[EDITADO em 12/05/2013] 
A foto que é tirada com as câmeras atuais requer muita memória para ser alocada. Sendo assim, verificamos o tamanho da ImageView para carregar a imagem deste tamanho.
O atributo caminhoFoto que foi atribuído no método tirarFotoClick é utilizado para carregar a imagem e atribuir em um ImageView. Agora vamos ver como salvar essa imagem na galeria de mídia.
public void salvarFotoClick(View v) {
  if (caminhoFoto != null && caminhoFoto.exists()) {
    MediaStore.Images.Media.insertImage(
      getContentResolver(), 
      caminhoFoto.getAbsolutePath(), 
      caminhoFoto.getName(), "");

    Toast.makeText(this, 
      "Imagem adicionada a galeria.",
      Toast.LENGTH_SHORT).show();
  }
}
A classe MediaStore.Images.Media tem um "método mágico" que permite que possamos inserir um Bitmap na galeria. Essa operação é feita graças a um ContentProvider disponibilizado pela aplicação de galeria. O próximo passo será como abrir a galeria de mídia para selecionarmos uma imagem.
public void galleryButtonClick(View v) {

  Intent intent = new Intent(
    Intent.ACTION_GET_CONTENT);
  intent.setType("image/*");
  startActivityForResult(intent, 2);
}
O código acima abrirá a aplicação de galeria. Notem que agora passamos o valor 2 para o startActivityForResult. Da mesma forma que fizemos para tirar foto, usaremos o método onActivityResult para tratar a imagem selecionada.
// adicione esse código no onActivityResult
if (resultCode == RESULT_OK && requestCode == 2) {
  Uri selectedImage = data.getData();
  String[] filePathColumn = { 
    MediaStore.Images.Media.DATA };

  Cursor cursor = getContentResolver().query(
    selectedImage, filePathColumn, null, null, null);
  cursor.moveToFirst();

  int columnIndex = cursor.getColumnIndex(
    filePathColumn[0]);
  String filePath = cursor.getString(columnIndex); 
  cursor.close();

  Bitmap yourSelectedImage = 
    BitmapFactory.decodeFile(filePath);

  imageView1.setImageBitmap(yourSelectedImage);
}
A galeria retorna o endereço da imagem selecionada através de um objeto Uri que é obtido em data.getData(). Com essa informação podemos acessar o ContentProvider da galeria para obter o caminho da imagem. Com esse caminho passamos para a classe BitmapFactory que retornará um objeto Bitmap para preencher a imageView.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 15 de agosto de 2011

Google compra Motorola Mobility


Olá povo,

Você chega na segunda de manhã e se depara com uma notícia que balança o mundo mobile: a compra da Motorola Mobility pela Google.
Não tô afim de escrever muito, mas vou colocar minha opinião pessoal. Achei essa aquisição bastante oportuna e ao meu ver até esperada. A Apple tem seu próprio SO (iOS) e fabrica seu hardware; a Nokia fecha parceria com a Microsoft para o Windows Phone; nada mais justo do que a Google se juntar com alguém, e com alguém que apostou muito no Android como a Motorola.

Aí pode vir a pergunta: e porque não a HTC ou a Samsung? Na minha opinião, pode ser porque esses dois fabricantes em especial "dão tiros pra todo lado". Digo isso porque podemos encontrar celulares Samsung com Bada, e da HTC com Windows, além do próprio sistema da Google. Então eu creio que eles continuarão assim, diversificando seus produtos.

Outro ponto importante é que o Google/Android/Fabricantes estavam sendo processados por diversos concorrentes por questões de patentes, e em muitos casos, perdendo essas disputas na justiça. Com a compra da Motorola, o Google amplia seu leque de patentes e protege a si e aos parceiros (fabricantes) que adotam o Android, uma vez que a Google reiterou sua posição de deixa-lo Open Source.

Sendo assim, acho que isso fortalecerá o Android e aquecerá ainda mais o mercado mobile. E nós, como desenvolvedores, devemos estar preparados para onde quer que o vento leve este barco.
E vocês, o que acham? Deixem seus comentários.

4br4ç05,
nglauber

*imagem Gizmodo Brasil

sábado, 6 de agosto de 2011

Artigo "Sensores no Android"

Olá povo,

A revista Java Magazine edição 94, trás uma matéria sobre utilização de sensores na plataforma Android. Ela foi escrita por mim e pelos meus colegas Edilson Mendes(Ronaldo!) e Artur Botelho (o doido :)

Este artigo demonstra como obter informações dos sensores dos aparelhos Android através da Sensor API. Com ela podemos capturar informações como aceleração e orientação do aparelho. Estes e outros sensores podem servir tanto como mecanismo de iteração com o usuário, quanto de canal de comunicação para obter informações sobre o dispositivo e o ambiente onde ele se encontra. O sensor de aceleração, também conhecido como acelerômetro, por exemplo, é amplamente utilizado em jogos disponíveis no Android Market.

Espero que vocês gostem.

4br4ç05,
nglauber

sexta-feira, 5 de agosto de 2011

Apple Fail

Olá povo,

Como vocês sabem, aqui no blog coloco coisas da minha vida profissional, então a maioria dos exemplos de código que eu coloco aqui, vem de necessidades minhas, ou de alunos/colegas durante o desenvolvimento de seus projetos. Entretanto, dessa vez não serão exemplos de código que colocarei aqui, e sim um registro, para que eu lembre e quem sabe até me retrate depois em relação a Apple.

Não estou nada satisfeito com os produtos da companhia do Steve Jobs. Comprei meu Macbook (branco) em Fevereiro deste ano, e em julho, após pagar os U$30 para atualizar o sistema operacional para a versão Lion, o computador já travou 3 vezes apresentando e tela de "kernel panic". Tudo bem que o Lion está na primeira versão, mas devemos combinar que isso é inadmissível.

Também não tive uma boa experiência com o tablet "da maçã". Em junho, comprei um iPad 2 (16GB + Wi-Fi + 3G) pois estava bastante motivado para desenvolver aplicações para iOS. Porém, após 32 dias de uso o mesmo apresentou um defeito: uma linha de pixels queimados de cima a baixo. Isso mesmo, uma linha de "dead pixels".

Acabei de solicitar a devolução do mesmo junto a empresa onde adquiri, e felizmente acho que vou conseguir meu dinheiro de volta. Se isso acontecer, comprarei um outro tablet, mas com certeza, não será um iPad, pois duas experiências ruins de uma vez não são lá muito motivadoras.

Por ser um professor, e ter uma pequena parcela de participação na formação de pessoas, acho legal ter um registro como esse para que elas não fiquem com a imagem de que a Apple é perfeita. Não é, e pelo que eu vi depois dessas experiências, está longe disso.

4br4ç05,
nglauber