segunda-feira, 28 de novembro de 2011

Lendo RSS no Android

Olá povo,

A tecnologia RSS tornou-se uma forma padrão que os sites encontraram para compartilhar suas informações. Com ele, os usuários podem visualizar seu conteúdo de forma padronizada, uma vez que utiliza o formato XML. Sendo assim, vou mostrar nesse post como ler um RSS com Android. Mas o código utilizado aqui pode ser utilizado para qualquer XML.

Vou começar criando a classe que vai representar uma notícia do RSS. Ela tem basicamente um título, um resumo da notícia e um link para notícia completa.

class Noticia {
  String titulo;
  String descricao;
  String link;
 
  public Noticia(String t, String d, String l) {
    titulo = t;
    descricao = d;
    link = l;
  }
 
  @Override
  public String toString() {
    return titulo;
  }
}

Simplifiquei ao máximo a classe aqui, não coloquei nem os GETs nem os SETs. Implementei apenas o método toString() pois o mesmo é utilizado pelo Adapter (que veremos em seguida) para exibir a notícia.

Agora vamos ver o código da Activity.
public class LeitorRSSActivity extends ListActivity {

  List<Noticia> news;
  ArrayAdapter<Noticia> adapter;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    news = new ArrayList<Noticia>();
    adapter = new ArrayAdapter<Noticia>(
    this, android.R.layout.simple_list_item_1, news);
    setListAdapter(adapter);

    // Criando AsyncTask que buscará o RSS da globo
    new RssAsyncTask().execute(
      "http://g1.globo.com/dynamo/rss2.xml");
  }

  // Método que lê o XML do RSS
  private List<Noticia> readXML(InputStream is){
    List<Noticia> noticias =
      new ArrayList<Noticia>();

    try {
      // Criando os objetos que representam o XML
      DocumentBuilderFactory factory =
        DocumentBuilderFactory.newInstance();

      DocumentBuilder builder = 
        factory.newDocumentBuilder();
      Document xmlDocument = builder.parse(is);
   
      // Cada notícia é representada pela tag <item>
      // Aqui obtemos a lista de nós com essa tag
      NodeList posts = 
        xmlDocument.getElementsByTagName("item");

      // Vamos iterar sobre a lista de itens
      String titulo = null, descricao = null, 
        link = null;

      for (int i = 0; i < posts.getLength(); i++) {
        Node post = posts.item(i);

        // Cada nó ITEM tem os filhos:
        // TITLE, DESCRIPTION e LINK
        NodeList postInfo = post.getChildNodes();
        for (int j = 0; j < postInfo.getLength(); j++){
          Node info = postInfo.item(j);

          if ("title".equals(info.getNodeName())){
            titulo = info.getTextContent();

          } else if ("link".equals(
            info.getNodeName())){
            link = info.getTextContent();
      
          } else if ("description".equals(
            info.getNodeName())){
            descricao = info.getTextContent();
          }
        }
        // Com as informações das tags, criamos o
        // objeto notícia e adicionamos na lista
        noticias.add(
          new Noticia(titulo, descricao, link));
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
    return noticias;
  }

  // A AsyncTask realiza a comunicação em background
  class RssAsyncTask extends
    AsyncTask<String, Void, List<Noticia>>{

    ProgressDialog dialog;
 
    @Override
    protected void onPreExecute() {
      super.onPreExecute();
      // Antes de baixaro XML, mostra o dialog
      dialog = ProgressDialog.show(
        LeitorRSSActivity.this, 
        "Aguarde", "Baixando RSS");
    }
 
    @Override
    protected List<Noticia> doInBackground(
      String... params) {

      List<Noticia> lista = null;
      HttpURLConnection conexao = null;
      InputStream is = null;
   
      try {
        URL url = new URL(params[0]);
        conexao = (HttpURLConnection)
          url.openConnection();
        conexao.connect();
    
        is = conexao.getInputStream();
        lista = readXML(is);

      } catch (Throwable t){
        t.printStackTrace();
      } finally {
        try {
          if (is != null) is.close();
          if (conexao != null) conexao.disconnect();
        } catch (Throwable t){
        }
      }
      return lista;
    }
  
    @Override
    protected void onPostExecute(List<Noticia> result){
      super.onPostExecute(result);
      dialog.dismiss();
      news.addAll(result);
      adapter.notifyDataSetChanged();
    }
  }
}


Quem tiver dúvida sobre AsyncTask, dá uma olhada aqui, ou deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 21 de novembro de 2011

Google Maps e GPS

Olá povo,

Nesse post de 2009 mostrei como fazer um "Hello World" com a API do Google Maps. Agora vamos incrementar um pouco esse exemplo colocando uma imagem para indicar o seu local baseado na posição GPS.

Pré-requisitos
A configuração inicial para obter a chave e exibir o mapa na tela é idêntica a apresentada no post de dois anos atrás, mas caso não consiga gerar a chave, dê uma olhada nesse post aqui (Dica 5). Outra ressalva é que, caso você esteja usando o emulador para testar sua aplicação, certifique-se de que ele esteja usando a "Google APIs" no "Android AVD Manager" conforme a figura abaixo:



O código do Overlay
Uma coordenada geográfica é representada pela classe GeoPoint. Ela tem a latitude e longitude de um ponto no mapa. Entretanto essa classe não é visual, ou seja, ela não serve para enxergarmos o ponto no mapa. Quando queremos adicionar no mapa alguma indicação para uma coordenada geográfica, devemos utilizar a classe Overlay, que é uma subclasse de View. O código abaixo cria um Overlay que desenhará uma imagem em um dado ponto no mapa (representado por um GeoPoint).
public class MeuOverlay extends Overlay {

private Bitmap imagem;
private GeoPoint geopoint;

public MeuOverlay(Bitmap img ) {
imagem = img;
}

public void setGeopoint(GeoPoint geopoint) {
this.geopoint = geopoint;
}

@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);

if (geopoint != null){
Point pontoNaTela = mapView.getProjection()
.toPixels(geopoint, null);

canvas.drawBitmap(imagem,
pontoNaTela.x - (imagem.getWidth() / 2),
pontoNaTela.y - (imagem.getHeight()), null);
}
}
}

Essa classe tem dois atributos: um Bitmap e um GeoPoint. O Bitmap será a imagem que desenharemos para representar a o ponto no mapa; já o GeoPoint, como o próprio nome diz, é um ponto geográfico, que obteremos a partir da posição do GPS.
No método draw() é desenhado o ponto no mapa. Inicialmente checamos se o GeoPoint foi setado, em caso positivo, convertemos essa coordenada geográfica em um ponto na tela, que é representado pela classe Point. Quem faz essa conversão é a classe Projection obtida através do método getProjection() do MapView recebido como parâmetro do método. Depois, é só desenhar a imagem na tela. O cálculo feito na chamada do método é para que a imagem fique imediatamente acima do ponto geográfico.

O arquivo de layout
O arquivo de layout não tem nada de mais, apenas a declaração da MapView com a API key (veja no post de 2009 como obter a sua API key).
<?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">

<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Exemplo GPS e MapActivity" />

<com.google.android.maps.MapView
android:id="@+id/mapa"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey="SUA_API_KEY" />
</LinearLayout>

A Activity
O código abaixo representa a Activity da aplicação e está todo comentado e só vou fazer alguns comentários no final. A nossa classe herda de MapActivity e implementa a interface LocationListener, que é utilizada para receber as notificações do GPS.
public class Aula09Activity extends MapActivity
// Interface que tem os métodos para tratar
// os eventos do GPS
implements LocationListener {

private MapView mapa;
private MapController controller;
private MeuOverlay overlay;
private LocationManager locationManager;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// O LocationManager vai registrar e desregistrar
// a classe para ouvir eventos do GPS
locationManager = (LocationManager)
getSystemService(LOCATION_SERVICE);

// Configurando a MapView
mapa = (MapView)findViewById(R.id.mapa);
// Habilita os botões de Zoom
mapa.setBuiltInZoomControls(true);
// Mostra em modo satélite
mapa.setSatellite(true);

// O MapController 'controla' o mapa :)
controller = mapa.getController();
controller.setZoom(17);

// Cria a imagem que vai representar o Overlay
Bitmap marcador = BitmapFactory.decodeResource(
getResources(), R.drawable.ponto);

// Cria o Overlay e adiciona ao mapView
overlay = new MeuOverlay(marcador);
mapa.getOverlays().add(overlay);

// Determinando um ponto inicial
int latitude = (int)(-8.058698 * 1E6);
int longitude = (int)(-34.872129 * 1E6);

GeoPoint geopoint = new GeoPoint(latitude, longitude);
controller.setCenter(geopoint);
overlay.setGeopoint(geopoint);
}

@Override
protected void onResume() {
super.onResume();
// Registrando a Activity para receber notificações
// de mudança na posição GPS
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 0, 0, this);

locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER, 0, 0, this);
}

@Override
protected void onPause() {
super.onPause();
// Desregistrando a Activity para receber
// notificações de mudança na posição GPS
locationManager.removeUpdates(this);
}

@Override
protected boolean isRouteDisplayed() {
return false;
}

@Override
public void onLocationChanged(Location location) {
// Método chamado quando a posição GPS muda
int latitude = (int)(location.getLatitude() * 1E6);
int longitude = (int)(location.getLongitude() * 1E6);

GeoPoint geopoint = new GeoPoint(latitude, longitude);
overlay.setGeopoint(geopoint);
controller.animateTo(geopoint);
}

@Override
public void onProviderDisabled(String provider) {
// Método chamado quando o GPS é desabilitado
}

@Override
public void onProviderEnabled(String provider) {
// Método chamado quando o GPS é habilitado
}

@Override
public void onStatusChanged(String provider,
int status, Bundle extras) {
// Método chamado quando o status do GPS muda.
// Pode ser: OUT_OF_SERVICE,
// TEMPORARILY_UNAVAILABLE e AVAILABLE
}
}

No código acima fazemos algumas inicializações no método onCreate. A classe LocationManager é responsável por registrar uma classe para "ouvir" os eventos de GPS (definidos na interface LocationListener que nossa Activity está implementando). Para criar um GeoPoint devemos passar a latitude e longitude em microdegrees, para tal devemos multiplicar os valores por 1.000.000 ou em notação científica 1E6 (10 elevado a 6).
No método onResume registramos nossa classe para ouvir os eventos do GPS. O método requestLocationUpdates recebe como parâmetros:
- o provedor de informações de posição GPS: aqui podemos utilizar o GPS do telefone, que demora mais a obter os dados, porém eles são mais precisos; e o da rede de dados, que obtém a posição baseado no esquema de triangulação de antenas da operadora de telefonia;
- a distância mínima em metros na posição GPS;
- o intervalo de tempo em milisegundos para receber a atualização;
- o objeto de uma classe que implemente a interface LocationListener (no nosso caso, a própria Activity).
No método onPause, desregistramos nossa classe para não ouvir mais os eventos de GPS. Isso evitará que ao sair da aplicação o Android tente ficar enviando as coordenadas mesmo com a aplicação inativa.
O método onLocationChanged é o mais importante, pois a cada vez que a posição GPS muda, esse método é chamado pelo Android. A partir do objeto Location que vem como parâmetro, obtemos a latitude e longitude para atualizarmos a posição do Overlay.
AndroidManifest.xml
No manifest, adicionamos as permissões INTERNET e ACCESS_FINE_LOCATION para podermos utilizar o Google Maps e obter a posição GPS respectivamente. Outra informação relevante é a tag <uses-library> que adicionamos para usar a API do Google Maps, uma vez que ela não é "padrão" da plataforma Android.
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="br.edu.cesar.aula09"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk android:minSdkVersion="10" />

<uses-permission
android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION" />

<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:label="@string/app_name"
android:name=".Aula09Activity" >
<intent-filter >
<action
android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<uses-library
android:name="com.google.android.maps"/>
</application>
</manifest>

Para testar o código da aplicação, você deve habilitar as opções de GPS no dispositivo através da opção Settings > Location & Security > My Location. Notem que temos duas opções nessa seção: GPS satélites e redes móveis. A primeira é mais precisa, porém demora mais; já a segunda obtém a posição baseada triangulação entre as distâncias das redes de telefonia ou wireless.

Mas se você estiver usando o emulador, use a aba "Emulator control" da perspectiva DDMS do Eclipse (para exibir, acesse Window > Open perspective > DDMS).

Digite uma coordena válida e clique em "Send". Para ober uma posição, vá no Google Maps, clique com o botão direito e selecione "O que há aqui? | What's here?". Na barra de busca, ficará a latitude e a longitude, aí é só copiar e testar.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

terça-feira, 8 de novembro de 2011

AutoCompleteTextView personalizado

Olá povo,

Estou com um monte de posts legais pra publicar, mas infelizmente (pra variar) estou sem tempo. São coisas que precisei fazer nos projetos que trabalhei, e que gosto de compartilhar com quem lê o blog. Além do mais, me serve como fonte de consulta para uma uso posterior. Afinal de contas, é pra isso que serve o blog (pelo menos pra mim).

Hoje vou mostrar como criar uma consulta personalizada para o componente AutoCompleteTextView. Esse componente é utilizado quando você tem uma lista de valores, e ao invés de selecioná-lo em uma lista, você digita parte do texto e os resultados vão sendo exibidos em uma lista (estilo drop-down) abaixo do componente. Este componente já conta com um mecanismo de busca padrão, entretanto a necessidade de personalizá-lo surgiu quando precisei realizar a busca por nomes de cidades. O cliente solicitou que a acentuação fosse ignorada, ou seja, quando eu digitasse "sao" era para aparecer "São Paulo" nos resultados.

Vamos à implementação! Primeiro vou mostrar o método que substitui os caracteres acentuados pelos mesmos não acentuados.

public static String[] REPLACES = 
{ "a", "e", "i", "o", "u", "c" };

public static Pattern[] PATTERNS = null;

public static void compilePatterns() {
PATTERNS = new Pattern[REPLACES.length];
PATTERNS[0] = Pattern.compile(
"[âãáàä]", Pattern.CASE_INSENSITIVE);
PATTERNS[1] = Pattern.compile(
"[éèêë]", Pattern.CASE_INSENSITIVE);
PATTERNS[2] = Pattern.compile(
"[íìîï]", Pattern.CASE_INSENSITIVE);
PATTERNS[3] = Pattern.compile(
"[óòôõö]", Pattern.CASE_INSENSITIVE);
PATTERNS[4] = Pattern.compile(
"[úùûü]", Pattern.CASE_INSENSITIVE);
PATTERNS[5] = Pattern.compile(
"[ç]", Pattern.CASE_INSENSITIVE);
}

public static String removeAcentos(String text) {
if (PATTERNS == null) {
compilePatterns();
}

String result = text;
for (int i = 0; i < PATTERNS.length; i++) {
Matcher matcher = PATTERNS[i].matcher(result);
result = matcher.replaceAll(REPLACES[i]);
}
return result.toUpperCase();
}

O código acima, utiliza as classes Pattern e Matcher para checar a presença de caracteres acentuados e substitui-los pelos caracteres correspondentes sem acentuação. Observe que só são sendo tratadas as vogais e o cedilha, para textos em outros idiomas você deve fazer os ajustes necessários (como o 'ñ' do espanhol).

Para que um AutoCompleteTextView faça a busca, é necessário definir um Adapter com os dados que ele irá filtrar para exibir. Para fazer nossa busca personalizada (ignorando acentuação) devemos criar nosso Adapter e sobrescrever o método getFilter(). Esse método deve retornar um objeto da classe android.widget.Filter que representa o resultado da busca.

public class MeuAutoCompleteAdapter
extends ArrayAdapter<String>
implements Filterable {

private List<String> listaCompleta;
private List<String> resultados;
private Filter meuFiltro;

public MeuAutoCompleteAdapter(
Context ctx, int layout,
List<String> textos) {

super(ctx, layout, textos);
this.listaCompleta = textos;
this.resultados = listaCompleta;
this.meuFiltro = new MeuFiltro();
}

@Override
public int getCount() {
return resultados.size();
}

@Override
public String getItem(int position) {
if (resultados != null
&& resultados.size() > 0
&& position < resultados.size()){
return resultados.get(position);
} else {
return null;
}
}

@Override
public Filter getFilter() {
return meuFiltro;
}

private class MeuFiltro extends Filter {
@Override
protected FilterResults performFiltering(
CharSequence constraint) {

FilterResults filterResults =
new FilterResults();

ArrayList<String> temp =
new ArrayList<String>();

if (constraint != null) {
String term = removeAcentos(
constraint.toString().trim().toLowerCase());

String placeStr;
for (String p : listaCompleta) {
placeStr = removeAcentos(p.toLowerCase());

if ( placeStr.indexOf(term) > -1){
temp.add(p);
}
}
}
filterResults.values = temp;
filterResults.count = temp.size();
return filterResults;
}

@SuppressWarnings("unchecked")
@Override
protected void publishResults(
CharSequence contraint,
FilterResults filterResults) {

resultados = (ArrayList<String>)
filterResults.values;

notifyDataSetChanged();
}
}
}

A classe acima tem duas listas de strings: a original e a que serve como resultado da busca. Note que os métodos getCount e getItem trabalha em cima da lista dos resultados. O outro atributo da classe é da classe MeuFiltro, e esse atributo é retornado no método getFilter.
A classe interna MeuFiltro faz a nossa busca personalizada. O filtro é realizado no método performFiltering, que retorna um objeto da classe android.widget.Filter.FilterResults que contém o resultado da busca e o total de registros encontrados. Esse resultado é passado para o método publishResults que seta o resultado da busca no atributo resultados, e logo após atualiza o adapter através do método notifyDatasetChanged.

Agora vamos ver como usar esse nosso adapter.


List<String> cidade = new ArrayList<String>();
cidade.add("Recife");
cidade.add("São Paulo");
cidade.add("Santos");
cidade.add("Santa Cruz");

MeuAutoCompleteAdapter adapter =
new MeuAutoCompleteAdapter(
contexto,
android.R.layout.simple_dropdown_item_1line,
cidade);

AutoCompleteTextView actv = (AutoCompleteTextView)
findViewById(R.id.autoCompleteTextView1);
actv.setAdapter(adapter);


O resultado é exibido na figura abaixo:


Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

sexta-feira, 4 de novembro de 2011

Baixe arquivos com o DownloadManager

Olá povo,

Frequentemente alunos e colegas me perguntam qual é um bom livro sobre Android. E normalmente eu costumo indicar o livro do Ricardo Lecheta, Google Android (Editora Novatec), ou o Professional Android 2, do Reto Meyer. Vou adicionar nessa lista o Pro Android 2 da Editora Apress. Estava folheando esse livro quando achei um exemplo bem bacana: utilizar a classe DownloadManager para baixar arquivos. A utilização dela é bem simples, como podemos observar no exemplo abaixo:

public class ExDownldMngrActivity
extends Activity {

private DownloadManager dm;
private MeuReceiver receiver;
private ImageView imgView;
private long downloadId;

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

imgView = (ImageView)findViewById(
R.id.imageView1);

dm = (DownloadManager)
getSystemService(DOWNLOAD_SERVICE);

receiver = new MeuReceiver();
}

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

IntentFilter filter = new IntentFilter(
DownloadManager.
ACTION_DOWNLOAD_COMPLETE);

registerReceiver(receiver, filter);
}

@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
}

public void baixarArquivoClick(View v){
String url = "http://developer.android.com/assets/"+
"images/home/honeycomb-android.png";

DownloadManager.Request request =
new DownloadManager.Request(Uri.parse(url));
request.setTitle("Meu download");
request.setDescription("Logo do Android");
request.setAllowedNetworkTypes(Request.NETWORK_MOBILE);

downloadId = dm.enqueue(request);
}

private class MeuReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(
DownloadManager.EXTRA_DOWNLOAD_ID, -1);

if (id == downloadId) {
try {
FileDescriptor fd = dm.openDownloadedFile(
downloadId).getFileDescriptor();

Bitmap image =
BitmapFactory.decodeFileDescriptor(fd);

imgView.setImageBitmap(image);

} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
}
}

Na classe acima, temos quatro atributos. O DownloadManager que realizará a solicitação de download. O atributo MeuReceiver receberá a notificação quando o download for concluído. No nosso exemplo, baixaremos uma imagem, e ela será exibida no ImageView declarado em seguida. O último atributo é o ID do download que a nossa aplicação iniciará. Esse atributo é importante para obtermos o arquivo que foi baixado.
No onCreate fazemos as inicializações necessárias, inclusive, a do DownloadManager, através do método getSystemService. No método onResume, registramos o nosso BroadcastReceiver para ser chamado quando a ação ACTION_DOWNLOAD_COMPLETE for disparada. Essa ação é disparada quando o download do arquivo é concluído. E no onDestroy, desregistamos o nosso receiver.
O método baixarArquivoClick é chamado quando o botão (que declarei no arquivo de layout) é clicado. Nesse método criamos um objeto da classe DownloadManager.Request passando a URL do arquivo. Em seguida, setamos o título e a descrição do download. Além disso, podemos restringir o tipo de rede usada para download, no nosso caso, informamos que podemos fazer com a rede de dados da operadora, mas poderíamos informar que o download só poderia ser feito via Wi-Fi.
Finalmente, enviamos a requisição de download e armazenamos o id do download no atributo downloadId.
Para tratar o fim do download, temos a classe MeuReceiver. Quando o download é concluído o método onReceive é chamado. Nosso broadcast será chamado mesmo se não for o nosso download que for concluído, então temos que checar se o id que foi concluído é igual ao atributo downloadId. Se for, obtemos o FileDescriptor do arquivo baixado, criamos a imagem e setamos no ImageView.

Abaixo, temos a imagem da nossa aplicação em execução.


4br4ços,
nglauber

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

segunda-feira, 24 de outubro de 2011

Android: utilizando layout_weight

Olá povo,

Em todas as minhas turmas de Android, noto uma certa dificuldade em entender como funciona a propriedade layout_weight utilizada nos componentes inseridos dentro do LinearLayout. Por padrão, essa propriedade serve para definir como o espaço restante do layout deve ser distribuído pelos componentes. Mas esse comportamento pode variar de acordo com o valor definido na propriedade layout_width e layout_height. Observemos a figura abaixo:




O layout acima é um LinearLayout vertical (orientation=vertical) com vários LinearLayout horizontal iguais, com 3 botões cada, e com os respectivos textos: "Um", "exemplo" e "de peso".

Vamos ver como da linha está configurada:

Linha1 - Nessa linha não fizemos nenhuma modificação, ou seja, está sem peso e com a largura e altura (layout_width e layout_height) definidos como wrap_content.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:text="Exemplo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:text="de peso"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>

Linha 2 - Nesse linha, definimos pesos iguais para todos os botões. Notem que apesar dos pesos estarem iguais, os componentes não têm tamanhos iguais. Essa é a aplicação clássica do layout_weight. Como vimos na linha 1, sobrou um espaço entre o terceiro botão e a margem direita. Com os pesos iguais, o espaço restante é distribuído igualmente entre os componentes. Uma vez que o texto de um botão é maior que o outro, os botões ficam com larguras diferentes.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Exemplo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>

Linha 3 - Essa linha é similar a anterior, a diferença fica por conta da propriedade layout_width definida como 0dp. Como assim? Se setarmos a largura para zero, o componente não deveria aparecer certo? A resposta é sim, mas como estamos usando o peso, ele vai distribuir o espaço restante da tela igualmente para os 3 botões. Como os 3 botões estão com tamanho zero, o espaço restante (e que será dividido entre os componentes) é toda a tela.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Exemplo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>

Linha 4 - Igual a anterior, mas modificamos a proporção dos componentes. Os dois primeiros ocupam metade da tela, e o terceiro ocupa a outra metade.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Exemplo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"/>
</LinearLayout>

Linha 5 - Essa linha usa uma outra utilidade do layout_weight, a de prioridade. No código abaixo os dois primeiros botões estão com a propriedade layout_width setada para match_parent (que é o mesmo que fill_parent), ou seja, devem ocupar toda a largura do seu pai. Se esses componentes não tivessem o peso setado, o primeiro botão ocuparia toda a tela, e os demais não apareceriam. Uma vez que definimos o peso, cada um deles divide a tela, mas e o terceiro? Como não foi definido peso para ele, ele tem peso zero, então, o componente com MENOR peso, tem MAIOR prioridade, então ele passa a aparecer. Note que o layout_width do terceiro botão está como wrap_content, se estivesse fill_parent, ele ocuparia toda a tela, uma vez que ele tem peso menor (zero).
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Exemplo"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>

Linha 6 - Conforme explicado acima, todos os botões estão com a propriedade layout_width definidos como fill_parent, entretanto, o primeiro está com peso 2, enquanto os demais tem peso 1. Logo os com peso menor tem prioridade.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="2"/>
<Button
android:text="Exemplo"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>


Espero que agora fique mais claro a utilização do peso. Na dúvida, deixem seus comentários.

4br4ç05,
nglauber

quinta-feira, 20 de outubro de 2011

Android: Dicas 5

Olá povo,

Esse é mais um post da série de dicas de Android. Aproveitem e deixem seus comentários :)

Dica 1 - Alterando a fonte dos componentes
O Android, por padrão, tem apenas 3 fontes que podem ser utilizadas nos componentes visuais: sans, serif e monospace. Para utilizar cada uma delas, basta atribuir o valor desejado à propriedade android:typeface. Mas se quiser utilizar uma nova fonte, basta adicionar o arquivo *.ttf na pasta assets e carregá-la utilizando o código abaixo.
Typeface typeface =
Typeface.createFromAsset(getAssets(), "Aliens.ttf");

TextView txt = (TextView)findViewById(R.id.textView1);
txt.setTypeface(typeface);

Infelizmente, não achei uma forma de setar essa fonte no XML. Se alguém souber, deixe um comentário. Ah! A fonte que usei nesse exemplo foi baixada do site http://www.webpagepublicity.com/free-fonts.html.

Dica 2 - Converter DIP (Density Independent Pixel) para Pixel
Após ministrar duas aulas seguidas (em turmas diferentes) sobre Views personalizadas no Android, uma dúvida frequente foi como converter DIP para PX (pixel). Isso é especialmente útil para que a View rode corretamente em diferentes densidades de tela (LDPI, MDPI, HDPI e agora XHDPI). Para tal, pode-se utilizar o método applyDimension da classe TypeValue.
Resources r = getResources();
float valorEmDp = 14;
float valorEmPixels = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, valorEmDp,
r.getDisplayMetrics());


Dica 3 - Evitando acumular Toasts
Um "bug" clássico que os engenheiros de teste do projeto em que eu trabalho levantam é sobre a utilização do Toast. Se você chamar o método show() dessa classe várias vezes seguidas, essas mensagens são acumuladas e ficam sendo exibidas sequencialmente. Um recurso que utilizo é criar um método separado, que verificará se já existe um Toast aberto, em caso positivo, ele o cancela para exibir um novo. Para tal, você deve criar um atributo da classe Toast (que abaixo chamo de 'toast') e utilizar o seguinte código:
// Declare o atributo
private static Toast toast;

// Método
public static void showToast(Context ctx, int res){
if (toast != null){
toast.cancel();
toast.setText(res);
} else {
toast = Toast.makeText(ctx, res, Toast.LENGTH_LONG);
}
toast.show();
}


Dica 4 - Definindo onde instalar sua aplicação
A partir da versão 2.2 (Froyo) é possível especificar onde sua aplicação pode ser instalada: cartão de memória ou memória interna. Por padrão, a aplicação será instalada na memória interna, mas você pode utilizar a propriedade android:installLocation da tag <manifest> do AndroidManifest.xml para um dos valores abaixo:
internalOnly - A aplicação só poderá ser instalada na memória interna (valor padrão).
auto - A aplicação é instalada na memória interna, mas poderá ser movida para o cartão de memória posteriormente ou quando a memória interna estiver cheia.
preferExternal - A aplicação deve ser instalada no cartão de memória preferencialmente, mas isso não é garantido.

Exemplo:
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="preferExternal"
... >


Dica 5: Ativar/Acender a tela do aparelho
Neste post eu mostrei como criar notificações no Android. Um recurso que pode ser utilizado juntamente com as notificações é acender/ligar a tela do aparelho. Para fazer isso, deve-se utilizar a classe PowerManager.

// Obtém a instância do PowerManager
PowerManager pm = (PowerManager)c.getSystemService(
Context.POWER_SERVICE);

// Liga o display do aparelho
WakeLock wakeLock = pm.newWakeLock(
// Liga a tela
PowerManager.SCREEN_DIM_WAKE_LOCK |
// Após liberar a tela para apagar,
// mantém a tela ligada por um pouco
// mais de tempo
PowerManager.ON_AFTER_RELEASE,
// Tag para debug
"tag_para_debug");

// Liga a tela por 10 segundos
wakeLock.acquire(10000);


Para usar o código acima, deve-se adicionar a permissão WAKE_LOCK no AndroidManifest.xml.
<uses-permission name="android.permission.WAKE_LOCK"/>


Dica 6: debug.keystore expirou

Essa dica foi enviada pelo meu colega Ricardo Gilson.
Toda aplicação Android é assinada para ser instalada no dispositivo, por padrão, ela é assinada com uma assinatura de debug através do arquivo debug.keystore que fica na subpasta ".android" no diretório do usuário (C:\Users\usuario\.android ou /Users/usuario/.android). Depois de um certo tempo sem utilizar a máquina, a mensagem abaixo pode ser exibida ao tentar rodar uma aplicação:

Error generating final archive: Debug Certificate expired on 24/09/2011.

Para resolver o problema é só apagar o arquivo debug.keystore, dar um clean no projeto do Eclipse (menu Project > Clean) e tentar rodar novamente.

Dica 7: Habilite a aceleração de Hardware (por Fernado Fragoso)
Habilitar a aceleração de hardware quando necessitar de manipulação de imagens, adicionar na tag application do AndroidManifest.xml.

android:hardwareAccelerated="true"

Faz uma diferença gigantesca! Valeu Fernando!

Por hoje é só, qualquer dúvida ou sugestão, deixem seus comentários.

4br4ç05,
nglauber