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

sexta-feira, 12 de fevereiro de 2016

Dominando o Android 2 edição: correções e melhorias

Olá povo,

Quem me vê no dia a dia, sabe que eu ando com um exemplar do meu livro o tempo todo. Além de utiliza-lo como forma de consulta, eu saio anotando nele:
  • as novidades em APIs que saíram após o lançamento do livro; 
  • novos tópicos para colocar em versões futuras; 
  • e obviamente erros que os leitores vão reportando. 
Na primeira edição, o exemplar que eu andava foi muito útil para melhorar a segunda edição, mas quase que eu perco o livro quando ele caiu em uma poça d'água! o.O
Por isso resolvi compartilhar com vocês minhas anotações, assim vocês já vão sabendo o que está errado, o que já mudou em termos de API, e o que pode mudar para a próxima ;) A medida que for achando mais problemas, vou atualizando esse post.
Ah! Antes que vocês perguntem, não tem nenhuma previsão para a próxima edição. Mas provavelmente o que vai acontecer é eu começar a trabalhar no livro após o Google I/O desse ano que vai acontecer entre 18 e 20 de maio.
Segue abaixo o que eu anotei até agora. Eu dividi em duas categorias: melhorias e erros.

Melhorias

Capítulo 0 / Pág. 29 - essa seção deve receber uma atualização com as novidades do emulador 2.0.
http://tools.android.com/tech-docs/emulator

Capítulo 3 - vou adicionar mais uma seção nesse capítulo falando dos layouts percentuais PercentFrameLayout e PercentRelativeLayout que eu falei nesse post aqui.

Capítulo 4 / Pág. 132 - esqueci de falar de uma propriedade bacana do TextInpuLayout que mostra a quantidade de caracteres digitados.
http://developer.android.com/reference/android/support/design/widget/TextInputLayout.html#setCounterEnabled(boolean)
<android.support.design.widget.TextInputLayout
    ...
    app:counterEnabled="true">
    <EditText
        ...
        android:maxLength="100"/>

Capítulo 4 / Pág. 163 - esqueci de mencionar que o método performFiltering() é executado em uma thread separada, enquanto que o método publishResults() é executado na UI thread.

Capítulo 6 / Pág. 254 - deveria ter criado um estilo para a Toolbar.

Capítulo 7 / Pág. 297 - era para ter enfatizado que, se uma classe implementar OnPreferenceChangeListener, caso uma SharedPreference seja modificada em qualquer ponto da aplicação, essa classe será notificada (obviamente se ela estiver instanciada).

Capítulo 7 / Pág. 310 - o trecho do método onClick() deveria estar em negrito.

Capítulo 7 / Pág. 325 - deveria estar realizando as operação com o ContentProvider utilizando a classe AsyncQueryHandler como falei nesse post aqui.

Capítulo 10 / Pág. 388 - substituir a enum Status por annotations como falei nesse post.

Capítulo 14 / Pág. 473 - no método onMessageReceived(), checar se from.equals(senderId) e se !data.isEmpty().

Capítulo 15 / Pág. 519 - o método getMap() tornou-se obsoleto (deprecated), em seu lugar, deve-se usar o método getMapAsync().
https://developers.google.com/android/reference/com/google/android/gms/maps/SupportMapFragment.html#getMapAsync(com.google.android.gms.maps.OnMapReadyCallback)

Capítulo 15 / Pág. 551 - podemos enviar localização fictícia com o emulador 2.0 do Android ou com o Genymotion.

Capítulo 20 - Falar sobre TransitionManager. Depois de ver essa palestra do Lúcio Maciel, me arrependi de não ter colocado esse assunto nesse capítulo.
https://www.youtube.com/watch?v=GKjPf0NI9ps

Capítulo 23 - Muitos fundamentos interessantes que são mostrados no treinamento de Gradle do Udacity seriam interessantes de colocar no livro. Principalmente linha de comando.
https://www.udacity.com/course/gradle-for-android-and-java--ud867

Capítulo 26 / Pág. 843 - Colocar alguns detalhes interessantes no uso da RecyclerView que eu coloquei nesse post.

Capítulo 26 / Pág. 891 - A biblioteca Otto foi recentemente marcada como deprecated em favor do RxJava e RxAndroid. Mas não se preocupem, o Otto continua funcionando. Uma outra opção seria usar o EventBus.

Capítulo 27 - Colocar um exemplo de DataBinding com listas.
http://developer.android.com/intl/pt-br/tools/data-binding/guide.html


Erros

Capítulo 1 / Pág. 39 e 49 - Quando falo das pasta res/mipmap eu menciono quatro pastas, quando na verdade são 5.

Capítulo 2 / Página 81 - Ao girar a activity sem selecionar um estado, o texto do botão fica em branco. Para contornar isso, basta colocar o código a seguir no onCreate.

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    if (savedInstanceState != null) {
        estado = savedInstanceState.getString(STATE_ESTADO);
        if (estado != null) botaoEstado.setText(estado);
    }
}

Capítulo 3 / Pág. 110 - a biblioteca de compatibilidade do GridLayout é melhor do que a versão nativa, pois ela teve vários bugs corrigidos.
http://developer.android.com/tools/support-library/features.html#v7-gridlayout
Basta adicionar a dependência no build.gradle.
compile 'com.android.support:gridlayout-v7:+'
E no layout modificar para
<android.support.v7.widget.GridLayout ...

Capítulo 5 / Pág. 170 - a figura 5.6 está errada... ela deveria estar mostrando as marcações na ferramenta draw9patch. Ao invés disso está mostrando o GIMP :P

Capítulo 6 / Pág. 212 - No último parágrafo, a primeira frase deve ser "... o parâmetro savedInstanceState é igual a nulo, ..."

Capítulo 6 / Pág. 255 - o exemplo está com problema na Toolbar quando rodamos a aplicação no KitKat. A correção do bug já está no GitHub nesse commit aqui. E eu fiz até um vídeo explicando a solução :)

Capítulo 10 / Pág. 385 - na definição da tabela Hotel, o campo "estrelas" deve usar o tipo DECIMAL(2,1). Essa modificação já está no GitHub.
https://github.com/nglauber/dominando_android2/blob/master/server/hotel_service/webservice.php

Capítulo 14 / Pág. 511 - no último parágrafo, falei que após o realizar o login com a conta do Google, uma boa opção seria utilizar o ID da conta e enviar para o seu serviço web. Mas a solução recomendada pelo Google é o uso do Token.
http://android-developers.blogspot.com.br/2016/01/using-google-sign-in-with-your-server.html

Capítulo 24 / Pág. 764 - a figura 24.8 ficou "estranha".

Capítulo 25 / Pág. 839 - esse foi o pior erro do livro. Ficou faltando praticamente uma página.
Notei que um trecho grande do capítulo 26 (1ª impressão) acabou ficando de fora do livro. Creio que foi no momento da diagramação do livro na editora. Na página 839, no segundo parágrafo, após o texto "... que estamos executando." deveria vir o texto abaixo:

Biblioteca 1: ButterKnife
O ButterKnife (http://jakewharton.github.io/butterknife/) é um biblioteca desenvolvida por Jake Wharton (Google Developer Expert de Android) que facilita a atribuição de views à classes sem precisar utilizar o método findViewById(int).
Vejamos o exemplo a seguir:
public class MainActivity extends Activity {
  @Bind(R.id.txtTitulo) TextView mTxtTitulo;
  @Bind(R.id.edtNome) EditText mEdtNome;
  @Bind(R.id.listview) ListView mListPessoas;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
    // Pronto! Pode usar os atributos!
  }
}
Perceba que anotamos os atributos da classe que representam componentes no arquivo de layout com a anotação @Bind(int) passando o id do componente. No método onCreate(Bundle) inicializamos esses atributos simplemente invocando ButterKnife.bind(this). O ButterKnife também funciona em Fragments e Adapters como veremos mais adiante. Essa biblioteca ainda possui outros recursos interessantes como definir evento de clique em botões.
@OnClick(R.id.botao1)
public void onClickBotao1(View view) {
}

@OnClick(R.id.botao2)
public void onClickBotao2() {
}
Perceba que no evento de clique do segundo botão, não colocamos o parâmetro, pois para a biblioteca ele é opcional. Vamos utilizar o ButterKnife em nosso aplicativo. Para isso, adicione a dependência no build.gradle.
dependencies {
    …
    compile 'com.jakewharton:butterknife:7.0.1'
}

Biblioteca 2: OkHttp
No capítulo 8 vimos como acessar um servidor web utilizando a classe HttpUrlConnection. Ela funciona muito bem, o único incoveniente é quando precisamos ler o retorno do servidor,

x.x.x.x.x.x.x.x.x.x.x

Peço desculpas pelo inconveniente, mas pelos menos agora vocês vão entender porque o texto está maluco :)
------------------

Quero deixar meu muito obrigado a todos os leitores que reportam os erros encontrados no livro. E se você adquiriu ou livro, não se esqueça de se cadastrar no grupo de discussão da segunda edição do "Dominando o Android".

4br4ç05,
nglauber

quinta-feira, 8 de setembro de 2011

Twitter API no Android

Olá povo,

Nunca me interessei em testar APIs para redes sociais por dois motivos: não gosto muito dessa febre; e pensava que era bem trivial ao ponto de ter milhões de tutoriais por aí. Grande engano. Quando fui adicionar suporte ao Twitter em uma aplicação que estou trabalhando, notei que existem algumas etapas 'chatinhas' para realizar essa integração. Não sei se a forma que estou utilizando aqui é a melhor, inclusive, se vocês tiverem sugestões, deixem seus comentários :)

Sem mais "delongas", vamos começar! Nesse post, vou mostrar como realizar o processo de autenticação no Twitter e 'twitar' uma mensagem. Para começar, obviamente você deve ter uma conta comum no Twitter. Para ter acesso aos recursos da API, você (como desenvolvedor) deverá cadastrar sua aplicação no site https://dev.twitter.com. Esse cadastro servirá para a equipe do Twitter saber qual aplicação (e consequentemente o login) responsável pelo cliente que está acessando o Twitter. Isso é similar ao que é feito quando utilizamos a API de Mapas do Google, onde você deve usar sua conta do Google para obter uma chave, para então utilizar o serviço.



O cadastro é bem simples, basta informar: o nome da aplicação, descrição da mesma, URL para contato e uma URL de callback. Este último é mais importante para aplicações web, onde após efetuar o login, a chave de acesso será direcionada.
Uma vez cadastrada, serão geradas alguma informações que serão utilizadas para integração da nossa aplicação com o Twitter. Nós precisaremos da "Consumer Key" e "Consumer Secret".

Como funciona a autenticação no Twitter?
Até meados do segundo semestre de 2010, o acesso ao twitter funcionava basicamente passando o login e a senha em texto plano. Por motivos óbvios de segurança, essa forma foi descontinuada, e passou-se a utilizar a forma de autenticação OAuth. Esse processo é ilustrado na figura abaixo:

Não vou entrar em muitos detalhes sobre esse protocolo, mas basicamente o cliente envia um solicitação de acesso utilizando a consumerKey e consumerSecret para o servidor, o servidor então retorna uma URL para que o usuário se autentique. Nesse momento o usuário insere seu usuário e senha do Twitter. Uma vez passadas essas informações, o servidor valida esses dados e gera um token de acesso, que será usada para toda a comunicação com o Twitter. Esse tone deve ser salvo no cliente para que ele possa se comunicar com o servidor sem ter passar o login e a senha a cada transacção.

Mãos à obra!
De posse do conceito sobre a autenticação do Twitter, vamos implementar nosso exemplo. Ele constará de apenas uma tela, onde o usuário poderá fazer o login/logout no Twitter e atualizar o seu status a partir da mesma. A tela da aplicação é exibida abaixo:


Você pode implementar a comunicação com o Twitter do zero, utilizando JSON. Mas existem algumas APIs prontas que facilitam nosso trabalho. A que utilizarei aqui é a Twitter4J. Baixe a última versão estável, descompacte em algum local do seu HD. Crie um novo projeto Android, e copie o arquivo twitter4j-core-android-X.X.X.jar que está na pasta lib do Twitter4J para a pasta lib do seu projeto Android (essa pasta não é criada por padrão, então crie na raiz do seu projeto). Em seguida, adicione esse JAR no Build-Path do Eclipe clicando com o botão direito sobre o arquivo, e depois selecionando Build Path > Add to Build path. Seu projeto deve ficar como na figura abaixo.



Vamos ao código :) Vou começar pelo AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ngvl.android.twitter"
android:versionCode="1"
android:versionName="1.0">

<uses-sdk android:minSdkVersion="10" />
 
<uses-permission android:name="android.permission.INTERNET"/>

<application
 android:icon="@drawable/icon"
 android:label="@string/app_name">
 <activity
   android:name=".ExemploTwitterActivity"
   android:label="@string/app_name"
   android:noHistory="true">
   <intent-filter>
     <action 
      android:name="android.intent.action.MAIN"/>
     <category 
       android:name="android.intent.category.LAUNCHER"/>
   </intent-filter>
   <intent-filter>
    <action 
      android:name="android.intent.action.VIEW"/>
     <category 
       android:name="android.intent.category.DEFAULT"/>
     <category 
       android:name="android.intent.category.BROWSABLE"/>
     <data android:scheme="nglauber-android"/>
   </intent-filter>
 </activity>
</application>
</manifest>

Aqui nesse arquivo temos algumas coisas muito importantes:
- A tag uses-permission para nossa aplicação poder acessar internet;
- Foi adicionada a propriedade andróide:noHistory à tag activity, para que quando o Browser seja aberto para o usuário fazer o login, ao voltar, ele não crie outra instância dessa activity.
- Essa activity tem dois intent-filter. O primeiro é pra execução normal da aplicação e o segundo é usado pela página do Twitter. Após a autenticação, ele redirecionará para o endereço nglauber-android://?oauthverifier=sua_chave. Estamos informando que essa atividade trata esse "protocolo", dessa forma nossa activity será re-executada.

Agora vamos ver o código da Activity.
package ngvl.android.twitter;

import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

public class ExemploTwitterActivity extends Activity {

  private final String 
    consumerKey = "SUA CONSUMER KEY";
  private final String 
    consumerSecret = "SUA CONSUMER SECRET";
  private final String 
    CALLBACKURL = "nglauber-android:///";

  private Twitter twitter;

No código acima, as constantes cosumerKey e consumerSecret devem ser preenchidas com as informações do cadastro da sua aplicação no site do Twitter. Já a constante CALLBACKURL deve estar igual a que foi declarada no AndroidManifest.xml.
O atributo twitter proverá acesso ao processo de login e de atualização do status.

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  
  twitter = new TwitterFactory().getInstance();
  twitter.setOAuthConsumer(
    consumerKey, consumerSecret);
}

No onCreate apenas inicializamos o layout da tela, obtemos a instância do Twitter através da classe TwitterFactory e setamos a consumer key e secret.
 public void clickLogin(View v) {
  try {
    AccessToken accessToken = loadAccessToken();
    if (accessToken == null) {
      twitter = new TwitterFactory().getInstance();
      twitter.setOAuthConsumer(
        consumerKey, consumerSecret);
    
      RequestToken requestToken = 
        twitter.getOAuthRequestToken(CALLBACKURL);
 
      String url = requestToken.getAuthenticationURL();
      Intent it = new Intent(
        Intent.ACTION_VIEW, Uri.parse(url));
      it.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
      startActivity(it);
    
      saveRequestData(requestToken.getToken(), 
        requestToken.getTokenSecret());
   
    } else {
      twitter.setOAuthAccessToken(accessToken);
    }
  } catch (Exception e) {
    e.printStackTrace();
    showToast(e.getMessage());
  }
} 

O método acima, iniciará o processo de login e será chamado ao clicar no botão de login. Através do método loadAccessToken (definido mais adiante), ele tenta obter um objeto AccessToken salvo anteriormente. Caso não exista, o processo de login inicia com a obtenção da url para fazermos o login no Twitter. Notem que é passada a constante CALLBACKURL como parâmetro, isso indica qual endereço será chamado quando o login for concluído. Essa é uma URL "fake" que será chamada quando o login for efetuado. Neste momento, o browser redireciona a chave para nglauber-android://?oauthverifier=sua_chave, e o "protocolo" nglauber-android está sendo tratado por nossa atividade, como definido no AndroidManifest.xml.
A flag FLAG_ACTIVITY_NO_HISTORY evita que, ao pressionar back após o login, o browser seja exibido novamente. Após chamar o browser para o login, o token e o token secret da requisição são salvos pelo método saveRequestData (veremos esse método mais adiante).

Ao clicar no botão de login, será exibida a tela de autenticação do Twitter. Aqui é solicitado autorização para que a aplicação que cadastramos no começo desse post tenha acesso a sua conta do Twitter. Digite seu login e senha e clique em "Sign In".


Após realizar o login, a página do Twitter envia a chave para nossa Activity. Tratamos o recebimento dessa chave no método onResume conforme abaixo:
 @Override
protected void onResume() {
  super.onResume();
  
  Uri uri = getIntent().getData();
  if (uri != null) {
    String oauthVerifier = 
      uri.getQueryParameter("oauth_verifier");
 
    try {
      RequestToken requestToken = loadRequestToken();
      AccessToken at = twitter.getOAuthAccessToken(
        requestToken, oauthVerifier);

      saveAccessToken(
        at.getToken(), at.getTokenSecret());

    } catch (TwitterException e) {
      e.printStackTrace();
      showToast(e.getMessage());
    }
  }
}

Quando a atividade é chamada pela página do twitter, uma chave de verificação da requisição de acesso é retornada. Com a requisição e a chave de verificação podemos criar um objeto AccessToken para acessar os recursos do Twitter. O token e o token secret do objeto AccessToken são salvos no método saveAccessToken (definido mais adiante) e, como vimos anteriormente podem ser recuperados quando clicamos no botão de login.

Abaixo temos o método que é chamado ao clicar no botão Tweet.

public void clickTweet(View v) {
  try {
    if (loadAccessToken() != null){
      EditText edt = 
        (EditText) findViewById(R.id.editText1);
      String tweet = edt.getText().toString();

      twitter.updateStatus(tweet);
      showToast("Status atualizado com sucesso!");

    } else {
      showToast("Faça o login antes de Twittar");
    }
  } catch (TwitterException e) {
      e.printStackTrace();
      showToast(e.getMessage());
  }
}

Nada de mais aqui. Apenas pega o conteúdo da caixa de texto e manda alterar o status do objeto Twitter.

Abaixo temos método auxiliares que utilizamos no nosso código. O primeiro exibe um Toast na tela.
private void showToast(String s){
  Toast.makeText(this, s, Toast.LENGTH_LONG).show();
}

Os métodos que salvam e recuperam as chaves de solicitação (RequestToken) e de acesso (AccessToken) estão definidos abaixo. Para ambos são utilizados SharedPreferences.
 
private RequestToken loadRequestToken(){
  SharedPreferences prefs = PreferenceManager.
    getDefaultSharedPreferences(this);
  String reqToken = 
    prefs.getString("request_token", null);
  String reqTokenSecret = 
    prefs.getString("request_tokensecret", null);
  
  return new RequestToken(reqToken, reqTokenSecret);
}
 
private void saveRequestData(
  String requestToken, String requestTokenSecret){

  SharedPreferences prefs = PreferenceManager.
    getDefaultSharedPreferences(this);
  SharedPreferences.Editor editor = prefs.edit();

  editor.putString(
    "request_token", requestToken);
  editor.putString(
    "request_tokensecret", requestTokenSecret);

  editor.commit();  
}
 
private AccessToken loadAccessToken() {
  SharedPreferences prefs = PreferenceManager.
    getDefaultSharedPreferences(this);
  String acToken = 
    prefs.getString("access_token", null);
  String acTokenSecret = 
    prefs.getString("access_tokensecret", null);

  if (acToken != null || acTokenSecret != null){
    return new AccessToken(acToken, acTokenSecret);
  }
  return null;
}
 
private void saveAccessToken(
  String accessToken, String accessTokenSecret) {

  SharedPreferences prefs =
    PreferenceManager.getDefaultSharedPreferences(this);

  SharedPreferences.Editor editor = prefs.edit();

  editor.putString(
    "access_token", accessToken);
  editor.putString(
    "access_tokensecret", accessTokenSecret);

  editor.commit();
}

Por fim, o método que faz o logout, limpa os valores da sharedpreference de AccessToken, o que vai forçar a realização de um novo login.
public void clickLogout(View v) {
  saveAccessToken(null, null);
}


É isso pessoal, qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

sexta-feira, 22 de outubro de 2010

Android Live Wallpaper

Olá povo,

Engraçado, esse post aqui eu jurava que já tinha escrito, mas quando pesquisei aqui no blog, vi que tinha ficado só na imaginação :) Então vamos lá. Hoje quero falar um pouquinho de um recurso muito bacana que foi introduzido no Android 2.1: os papéis de parede animados ou simplesmente live wallpapers.

Os live wallpapers são diferentes dos papéis de parede comuns pois são dinâmicos e permitem interação com o usuário. Com isso, ao invés de termos uma imagem parada, eles podem exibir animações e até mesmo mudar de comportamento de acordo com o hora do dia, posição do telefone (baseado no acelerômetro), entre outros.

Temos alguns exemplos bem bacanas que já vêm no próprio Android. O live wallpaper "Água" mostra algumas folhas secas descendo na tela como se estivesse sobre a água, e quando o usuário toca na área de trabalho do telefone faz-se uma onda como se o usuário estivesse tocando sobre água.
Outro exemplo interessante é o "Grama". Ele mostra um gramado balançando com o vento. Com o céu ao fundo, ele muda de cor de acordo com a hora do dia.



Vou apresentar um exemplo simples de um papél de parede animado que mostrará o texto passando da tela e a cor da tela mudando aleatoriamente quando o usuário tocar na tela.
A estrutura do projeto ficará como abaixo:


Vamos começar de baixo pra cima. Vejamos com está o AndroidManifest.xml.


<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ngvl.android.wallpaper"
android:versionCode="1"
android:versionName="1.0">

<application
android:icon="@drawable/icon"
android:label="@string/app_name">

<service
android:name=".MeuPapelDeParede"
android:permission="android.permission.BIND_WALLPAPER">

<intent-filter>
<action
android:name="android.service.wallpaper.WallpaperService" />
</intent-filter>

<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/meupapeldeparede" />
</service>

<activity
android:name=".MeuPapelDeParedeConfig"
android:exported="true">
</activity>
</application>

<uses-sdk android:minSdkVersion="7" />
<uses-feature
android:name="android.software.live_wallpaper" />
</manifest>

Notem que temos um serviço e uma atividade declaradas no manifest. O serviço é o papel de parede em si. Ele exige a permissão BIND_WALLPAPER para poder defini-lo como wallpaper do telefone. Além disso ele tem a uma ação (action) específica para obedecer à chamadas do S.O. para o pel de parede animado. A tag meta-data referencia o arquivo de configuração do wallpaper, que está em res/xml/meupapeldeparede.xml. Esse arquivo vai definir a tela de configuração do papel de parede que é a atividade declarada no nosso manifest.

<wallpaper
xmlns:android="http://schemas.android.com/apk/res/android"
android:thumbnail="@drawable/icon"
android:settingsActivity="ngvl.android.wallpaper.MeuPapelDeParedeConfig"
/>

Essa tela de configuração permitirá ao usuário configurar o texto que aparecerá no papel de parede e armazenará automaticamente em uma SharedPreference.

<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/app_name"
android:key="meupapeldeparede_config">

<EditTextPreference
android:key="texto_do_papel_de_parede"
android:title="Texto do Papel de Parede" />
</PreferenceScreen>



public class MeuPapelDeParedeConfig
extends PreferenceActivity {

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

getPreferenceManager().setSharedPreferencesName(
MeuPapelDeParede.SHARED_PREFS_NAME);

addPreferencesFromResource(R.xml.tela_de_config);
}
}

Agora vamos ver a classe que faz o trabalho legal :)

public class MeuPapelDeParede
extends WallpaperService {

// Contante da SharedPreference do WallPaper
public static final String SHARED_PREFS_NAME =
"meupapeldeparede_config";

public static final String PREF_KEY =
"texto_do_papel_de_parede";

// Handler que ficará recebendo requisições
// para redesenhar a tela
private final Handler mHandler = new Handler();

// X e Y do texto e a cor de background
private int x = 200, y = 100, bg = 0xF00;

// texto do papel de parede
private String texto;

// Retorna a Engine que pinta a tela e
// trata eventos de toque
// Utilizado pelo framework
public Engine onCreateEngine() {
return new MinhaEngine();
}

// Engine para desenhar o papel de parede
private class MinhaEngine extends Engine implements
SharedPreferences.OnSharedPreferenceChangeListener{

// Pincel pra desenhar na tela
private final Paint mPaint;

// Flag que indica se wallpaper está visível
private boolean mVisible;

public MinhaEngine() {
// Configura o pincel para a cor branca
// e coloca anti-alias pra tirar o "serrilhado"
mPaint = new Paint();
mPaint.setColor(0xffffffff);
mPaint.setAntiAlias(true);

// Se registra para ser notificado
// caso o usuário mudar o texto
SharedPreferences prefs =
MeuPapelDeParede.this.getSharedPreferences(
SHARED_PREFS_NAME, 0);

prefs.registerOnSharedPreferenceChangeListener(
this);

onSharedPreferenceChanged(prefs, null);
}

public void onCreate(SurfaceHolder surfaceHolder){
super.onCreate(surfaceHolder);
// Habilita eventos de touch
setTouchEventsEnabled(true);
}

public void onDestroy() {
super.onDestroy();
// Suspende qualquer "pintura" pendente
mHandler.removeCallbacks(threadPintura);
}

public void onVisibilityChanged(boolean visible){
// Se o display estiver apagado, não fica
// pintando a tela evita desperdiçar
// processamento
mVisible = visible;
if (visible) {
pintaPapelDeParede();
} else {
mHandler.removeCallbacks(threadPintura);
}
}

public void onSurfaceDestroyed(SurfaceHolder sh){
super.onSurfaceDestroyed(holder);
// Ao destruir o "canvas" suspende todas
// as threads pendentes
mVisible = false;
mHandler.removeCallbacks(threadPintura);
}

public void onTouchEvent(MotionEvent event) {
// Captura o X e o Y de onde o usuário
// tocou na tela.
// Gera uma cor de BG aleatóra.
if (event.getAction() ==
MotionEvent.ACTION_DOWN) {

x = (int) event.getX();
y = (int) event.getY();
bg = new Random().nextInt(0xffffff);
}
super.onTouchEvent(event);
}

// Thread que solicita a pintura da tela
private final Runnable threadPintura =
new Runnable() {
public void run() {
pintaPapelDeParede();
}
};

// Método que pinta cada frame da
// animação do papel de parede
void pintaPapelDeParede() {
final SurfaceHolder holder =
getSurfaceHolder();

Canvas c = null;
try {
c = holder.lockCanvas();
if (c != null) {
desentaTexto(c);
}
} finally {
if (c != null)
holder.unlockCanvasAndPost(c);
}

// Depois de efetuar a pintura, agenda o handler
// pra pintar o próximo frame.
// Dessa forma, teremos 40 frames por segundo
mHandler.removeCallbacks(threadPintura);
if (mVisible) {
mHandler.postDelayed(threadPintura, 1000/25);
}
}

// Desenha o texto na tela
private void desentaTexto(Canvas c) {
c.save();

c.drawColor(0xff000000 | bg);

float textWidth = 0;
float[] widths = new float[texto.length()];

mPaint.setTextSize(20);
mPaint.getTextWidths(texto, widths);

for (int i = 0; i < widths.length; i++) {
textWidth += widths[i];
}

c.drawText(texto, x, y, mPaint);
x -= 5;
if (x <= -textWidth) {
x = c.getWidth();
}

c.restore();
}

// Se o usuário mudar o texto a ser exibido,
// atualiza o texto do wallpaper
public void onSharedPreferenceChanged(
SharedPreferences prefs, String key) {
texto = prefs.getString(PREF_KEY, "Texto");

}
}
}


Pronto! Agora pra testar, basta ir na tela principal pressionar Menu > Wallpaper > Live wallpapers. Nosso papel de barede deve aparecer na lista. Abaixo temos a figuras do nosso wallpaper em execução.



4br4ç05,
nglauber

quinta-feira, 30 de setembro de 2010

Multi-Touch no Android

Olá povo,

Esse blog sempre é "alimentado" com dúvidas de alunos e colegas meus que eu me atrevo a querer resolver. Hoje pela manhã, chegou um amigo e me perguntou se eu já tinha feito algo com Multi-Touch no Android. Falei que já tinha feito no iPhone, então fui à caça. Achei um ótimo exemplo no site do próprio Android. Quem quiser pode baixar via SVN aqui.
Esse exemplo demonstra a utilização do "efeito pinça" que aumenta o diminui um objeto na tela fazendo movimentos com os dedos similares aos de uma pinça.

O que é preciso é criar um objeto da classe android.view.ScaleGestureDetector passando em seu construtor o contexto (Context) e um objeto que implementa a interface ScaleGestureDetector.OnScaleGestureListener. Esse objeto será notificado quando o "evento de pinça" for disparado.


public class TouchExampleView extends View {
private static final int INVALID_POINTER_ID = -1;

private Drawable mIcon;
private float mPosX;
private float mPosY;

private float mLastTouchX;
private float mLastTouchY;
private int mActivePointerId = INVALID_POINTER_ID;

private ScaleGestureDetector mScaleDetector;

private float mScaleFactor = 1.f;

public TouchExampleView(Context context) {
this(context, null, 0);
}

public TouchExampleView(Context context,
AttributeSet attrs) {

this(context, attrs, 0);
}

public TouchExampleView(Context context,
AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

// A imagem será o ícone da aplicação
mIcon = context.getResources().
getDrawable(R.drawable.icon);
// Define os limites da imagem
mIcon.setBounds(0, 0,
mIcon.getIntrinsicWidth(),
mIcon.getIntrinsicHeight());

// Cria o objeto que capturará o "evento de pinça"
mScaleDetector = new ScaleGestureDetector(
context, new ScaleListener());
}

@Override
// Esse método servirá pra mover a imagem na tela
// e delegar o multi-touch pro ScaleGestureDetector
public boolean onTouchEvent(MotionEvent ev) {

// Delega o evento pro ScaleGestureDetector
// que verificará se o multitouch foi utilizado
mScaleDetector.onTouchEvent(ev);

final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();

// Ao tocar na tela, salva o local tocado
mLastTouchX = x;
mLastTouchY = y;
mActivePointerId = ev.getPointerId(0);
break;
}

case MotionEvent.ACTION_MOVE: {
final int pointerIndex =
ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);

// Ao mover, checa se o ScaleGestureDetector
// não está processando algum gesto
if (!mScaleDetector.isInProgress()) {
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;

// Salva o deslocamento feito ao mover
mPosX += dx;
mPosY += dy;

invalidate();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}

case MotionEvent.ACTION_UP: {
// nenhum dedo da tela, ponto inválido
mActivePointerId = INVALID_POINTER_ID;
break;
}

case MotionEvent.ACTION_CANCEL: {
// nenhum dedo da tela, ponto inválido
mActivePointerId = INVALID_POINTER_ID;
break;
}

case MotionEvent.ACTION_POINTER_UP: {
// Ao retirar um dos dedos, atualize o ponto
// com o dedo que ficou na tela
final int pointerIndex =
(ev.getAction() &
MotionEvent.ACTION_POINTER_ID_MASK)
>> MotionEvent.ACTION_POINTER_ID_SHIFT;

final int pointerId =
ev.getPointerId(pointerIndex);

if (pointerId == mActivePointerId) {

final int newPointerIndex =
pointerIndex == 0 ? 1 : 0;

mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);

mActivePointerId =
ev.getPointerId(newPointerIndex);
}
break;
}
}
return true;
}

@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);

canvas.save();

// Reposicona a matriz do canvas
canvas.translate(mPosX, mPosY);
// Define a escala (zoom) do canvas
canvas.scale(mScaleFactor, mScaleFactor);
// Redesenha a imagem
mIcon.draw(canvas);

canvas.restore();
}

// classe que ouve o evento de multi-touch
// e calcula a escala
private class ScaleListener extends
ScaleGestureDetector.SimpleOnScaleGestureListener{

@Override
public boolean onScale(ScaleGestureDetector d) {
mScaleFactor *= d.getScaleFactor();

// Não permite que o objeto fique
// muito grande nem muito pequeno
mScaleFactor =
Math.max(0.5f, Math.min(mScaleFactor, 5.0f));

invalidate();
return true;
}
}
}

Algumas considerações sobre esse post:
- Para testar esse exemplo você precisará do dispositivo real, afinal você não tem dois cliques de mouse :)
- O suporte a multi touch depende do aparelho;
- API de MultiTouch está disponível apenas para aparelhos com Android 2.0 ou superior.

Abaixo temos a aplicação executando no Motorola FlipOut.



É isso pessoal! Estou com muita coisa pra fazer, estou tentando terminar o mestrado, trabalhando e ainda dando aulas. Quaisquer dúvidas, postem seus comentários abaixo ou consulte o post orginal do blog do android.



Editado pra tentar atender o primeiro comentário desse post.

public class MinhaView extends View {

private Paint bluePaint;
private ArrayList<Point> points;

public MinhaView(Context c) {
this(c, null);
}

public MinhaView(Context c, AttributeSet attrs) {
super(c, attrs);
points = new ArrayList<Point>();

bluePaint = new Paint();
bluePaint.setARGB(255, 0, 0, 255);

setFocusable(true);
}

public boolean onTouchEvent(MotionEvent event){
// Se tocou na tela ou está movendo o dedo
if (event.getAction() ==
MotionEvent.ACTION_DOWN ||
event.getAction() ==
MotionEvent.ACTION_MOVE) {

// Pegue os pontos na tela e alimente a lista
// Normalmente são 1 ou 2 pontos
for(int i=1; i <= event.getPointerCount(); i++){
Point p = new Point(
(int) event.getX(event.getPointerId(i)),
(int) event.getY(event.getPointerId(i)));

points.add(p);
}
// Solicita que a view seja redesenhada
invalidate();
}
return true;
}


public void draw(Canvas canvas) {
super.draw(canvas);

// percorre a lista de pontos
while (points.size() > 0) {
Point p = points.remove(0);
canvas.drawCircle(p.x, p.y, 50, bluePaint);
}
// Limpa a lista pra evitar lixo
points.clear();
}
}


4br4ç05,
nglauber

domingo, 30 de agosto de 2009

Utilizando Banco de Dados SQLite no Android

Olá povo,

Entre as muitas coisas legais que o Android trouxe, o suporte nativo ao banco de dados SQLite foi uma das mais importantes, comercialmente falando. Com ele podemos utilizar os bons e famosos comandos SQL (SELECT, INSERT, UPDATE, DELETE, etc) no mundo mobile. Quem programou em J2ME sabe o quão complicado é implementar um repositório "digno" que suporte uma boa quantidade de registros nessa plataforma (não me falem de RMS... :)

Nesse POST, vou colocar trechos de código que utilizam esses conceitos e tentarei explicá-los. A parte da interface gráfica, vou explicar apenas o que for relevante para o uso com banco de dados.

Para criarmos um banco de dados SQLite, podemos:
  • utilizar a ferramenta SQLite Expert Personal e colocar o arquivo do banco de dados na pasta /data/data/pacote_da_minha_aplicacao/databases;
  • ou via código, utilizando uma classe "helper" do SQLite.
Utilizaremos a segunda abordagem, pois na vida real não queremos colocar esse arquivo manualmente no aparelho. Pois, caso seja necessária uma atualização, teremos que atualizar N aparelhos.

Mãos à obra! Vamos criar uma classe que herda de android.database.sqlite.SQLiteOpenHelper. Ela será responsável por criar o banco quando a aplicação for instalada (método onCreate) e atualizá-lo para novas versões da aplicação (método onUpgrade).

public class SQLiteHelper extends SQLiteOpenHelper {
  private String scriptCreate;
  private String scriptDelete;

  public SQLiteHelper(Context ctx, String nomeBd, 
      int versaoBanco, String scriptCreate, 
      String scriptDelete) {

      super(ctx, nomeBd, null, versaoBanco);
      this.scriptCreate = scriptCreate;
      this.scriptDelete = scriptDelete;
  }

  public void onCreate(SQLiteDatabase db) {
      db.execSQL(scriptCreate);
  }

  public void onUpgrade(SQLiteDatabase db, 
      int oldVersion, int newVersion) {

      db.execSQL(scriptDelete);
      onCreate(db);
  }
}


Agora vamos criar a classe que realizará as operações de abrir, selecionar, inserir, atualizar e excluir registos no banco de dados:

public class RepositorioCarro {
  private SQLiteDatabase db;
  private SQLiteHelper dbHelper;
  private static final String SCRIPT_DB_DELETE = 
    "DROP TABLE IF EXISTS carros";
  private static final String SCRIPT_DB_CREATE =
    "create table carros (_id integer primary "+
    "key autoincrement, nome text not null, "+
    "placa text not null, ano text not null);";

  public RepositorioCarro (Context ctx){
    dbHelper = new SQLiteHelper(ctx, "curso", 1, 
      SCRIPT_DB_CREATE, SCRIPT_DB_DELETE);
  }

  private long inserir(Carro c){
    ContentValues cv = new ContentValues();
    cv.put("nome", c.getNome())
    cv.put("placa", c.getPlaca());
    cv.put("ano", c.getAno());
    db = dbHelper.getWritableDatabase();
    long id = db.insert("carros", null, cv);
    db.close();
    return id;
  }

  private long atualizar(Carro c){
    ContentValues cv = new ContentValues();
    cv.put("nome", c.getNome());
    cv.put("placa", c.getPlaca());
    cv.put("ano", c.getAno());

    db = dbHelper.getWritableDatabase();
    long rows = db.update("carros", cv, "_id = ?", 
       new String[]{ String.valueOf(c.getId())});
    db.close();
    return rows; // qtde. de linhas afetadas 
  } 

  public int excluir(int id){
    db = dbHelper.getWritableDatabase();
    int rows = db.delete("carros", "_id = ?", 
      new String[]{ String.valueOf(id) });
    return rows; // qtde. de linhas afetadas
  }

  public List<Carro> buscarCarroPorNome(String nome){
    List<Carro> lista = new ArrayList<Carro>();

    String[] columns = new String[]{
       "_id", "nome", "placa", "ano"};
    String[] args = new String[]{nome+"%"};

    db = dbHelper.getWritableDatabase();
    Cursor c = db.query("carros", columns, 
       "nome like ?", args, null, null, "nome");

    c.moveToFirst();
    while(!c.isAfterLast()){
      Carro carro = fillCarro(c);
      lista.add(carro);
      c.moveToNext();
    }
    c.close();
    db.close();
    return lista;
  }

  private Carro fillCarro(Cursor c) {
    Carro carro = new Carro();
    carro.setId((int)c.getLong(0));
    carro.setNome(c.getString(1));
    carro.setPlaca(c.getString(2));
    carro.setAno(c.getString(3));
    return carro;
  }
}


Como vocês podem ver, estou usando uma classe Carro também. Não vou listá-la aqui por se tratar de um POJO com os campos: id, nome, placa e ano do veículo.

Na classe acima, o construtor recebe uma referência do contexto da aplicação. Ela é repassada para nossa classe SQLHelper, que por sua vez, se encarregará de criar o nosso banco de dados se ele não existir. A classe android.database.sqlite.SQLiteOpenHelper (superclasse do nosso helper) tem o método getWritableDatabase() que retorna uma instância da classe android.database.sqlite.SQLiteDatabase, que será nosso canal de comunicação com o banco de dados.

As operações de inserir e atualizar utilizam: uma string que representa o nome da tabela no banco de dados; e um objeto da classe ContentValues, que nada mais é do que um conjunto par/valor com os parâmetros do comando SQL. Mas notem que o método update recebe ainda uma String que representa a cláusula WHERE do comando UPDATE (do SQL), bem como os parâmetros da mesma, que estão mapeados via caracter '?'.
"Por tabela", o método excluir vocês já devem ter entendido :)

O último método é o buscarCarroPorNome(String). Toda consulta SQL, retorna um objeto do tipo Cursor. Ele é bem parecido com o ResultSet do JDBC. Os parâmetros do método query são, respectivamente: String nome da tabela, String[] colunasDaTabela, String clausulaWhere, String[] paramsWhere, String groupBy, String having, String orderBy.

Para exibir os dados, precisaremos criar um Adapter, que converterá um Carro em uma linha de uma lista. Crie o arquivo de layout list_item.xml e coloque-o na pasta res/layout:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout android:layout_height="wrap_content" 
xmlns:android="http://schemas.android.com/apk/res/android" 
android:layout_width="fill_parent" 
android:id="@+id/carroRow">

<TextView android:layout_width="wrap_content" 
android:layout_height="wrap_content" 
android:id="@+id/txtNome" 
android:layout_weight="1" 
android:textSize="20dp"/>

<TextView android:layout_width="wrap_content" 
android:layout_height="wrap_content" 
android:id="@+id/txtAno" 
android:layout_marginRight="10px"/>

<TextView android:layout_width="wrap_content" 
android:layout_height="wrap_content" 
android:id="@+id/txtPlaca"  
android:layout_marginRight="10px"/>

</LinearLayout>


E agora, vamos criar nosso adapter.

public class CarroListAdapter extends BaseAdapter {
  private Context context;
  private List<Carro> lista;

  public CarroListAdapter(Context ctx, 
    List<Carro> carros) {

    context = ctx;
    lista = carros;
  }

  public int getCount() {
    return lista.size(); 
  }

  public Object getItem(int arg0) {
    return lista.get(arg0);
  }

  public long getItemId(int arg0) {
    return lista.get(arg0).getId();
  }

  public View getView(int pos, View convertView, 
    ViewGroup parent) {

    Carro c = lista.get(pos);
    LayoutInflater inflater = 
      (LayoutInflater)context.getSystemService(
        Context.LAYOUT_INFLATER_SERVICE);

    View view = inflater.inflate(
      R.layout.list_item, null);

    ((TextView)view.findViewById(R.id.txtNome)).
      setText(c.getNome());

    ((TextView)view.findViewById(R.id.txtAno)).
      setText(c.getAno());

    ((TextView)view.findViewById(R.id.txtPlaca)).
      setText(c.getPlaca());

    return view;
  }
}


Crie uma classe que herde de ListActivity e no onCreate, digite o seguinte código:
RepositorioCarro carrosDB = new RepositorCarro(this);
List<Carro>lista = carrosDB.buscarCarroPorNome("");
setAdapter(new CarroListAdapter(this, lista));

Para utilizar os demais métodos é facinho, não é? Basta criar objetos do tipo Carro, no qual os valores serão preenchidos por widgets (componentes de UI) e passá-los como parâmetros para os métodos do RepositorioCarro.

Espero que tenham entendido e gostado :)

Editado em 03/11/2011
Sempre que fizermos uma operação no banco de dados é importante que encerremos a conexão com ele. Tinha faltado isso nesse post, e agora lembrei de corrirgir.

4br4ç05
nglauber