terça-feira, 30 de outubro de 2012

Gesto de Swipe no Android

Olá povo,

Recentemente precisei implementar a detecção do gesto de swipe em uma aplicação iOS que estava desenvolvendo. Achei super fácil e resolvi pesquisar como fazer o mesmo no Android. Eis que achei a solução abaixo. (Não me lembro o post original, quem souber, posta um comentário pls).
public class MainActivity extends Activity 
  implements OnClickListener, OnTouchListener {

  private GestureDetector gestureDetector;
  private TextView txtResultado;
    
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
        
    txtResultado = 
      (TextView)findViewById(R.id.txtResultado);
        
    gestureDetector = 
      new GestureDetector(this, new DetectorDeGesto());
    
    // Apesar da linha abaixo não fazer nada, 
    // não funcionou sem...
    findViewById(R.id.raizLayout).
      setOnClickListener(this);
    findViewById(R.id.raizLayout).
      setOnTouchListener(this);
  }

  @Override
  public void onClick(View v) {
    Log.d("NGVL", "onclick");
  }  
    
  @Override
  public boolean onTouch(View v, MotionEvent event) {
    return gestureDetector.onTouchEvent(event);
  } 

  class DetectorDeGesto 
    extends SimpleOnGestureListener {
     
    private static final int DISTANCIA_MINIMA  = 120;
    private static final int DISTANCIA_MAXIMA  = 250;
    private static final int VELOCIDADE_MINIMA = 200;
     
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, 
      float velocityX, float velocityY) {
      try {
        boolean excedeuDistanciaEmY =
          Math.abs(e1.getY() - e2.getY()) 
          > DISTANCIA_MAXIMA;
               
        if (excedeuDistanciaEmY) return false;
                
        boolean velocidadeValida =
          Math.abs(velocityX) > VELOCIDADE_MINIMA;
                
        boolean swipeLeft = 
          e1.getX() - e2.getX() > DISTANCIA_MINIMA;
        boolean swipeRight =
          e2.getX() - e1.getX() > DISTANCIA_MINIMA;
                  
        // right to left swipe
        if(swipeLeft && velocidadeValida) {
          txtResultado.setText("Left Swipe");

        // left to right swipe
        }  else if (swipeRight && velocidadeValida) {
          txtResultado.setText("Right Swipe");
        }
      } catch (Exception e) {
      }
      return false;
    }
  }  
}
A classe SimpleOnGestureListener é uma classe básica do Android que consegue detectar diversos gestos simples como scroll, double tap, entre outros. Nossa classe DetectorDeGesto, herda dessa classe e sobrescreve o método onFling, que é chamado quando o usuário está deslizando o dedo sobre a tela (esse evento é armazenado em e2) e guarda a referência de quando o usuário pressionou o dedo sobre a tela inicialmente (armazenado em e1). Nesse método verificamos a direção que foi feito o swipe, bem como a velocidade que o movimento foi feito. As distâncias mínima e máxima para considerarmos o gesto válido, assim como a velocidade mínima estão definidas nas constantes da classe.
Nossa Activity implementa as interfaces View.OnClickListener e OnTouchListener. No método onTouch, delegamos o tratamento do evento de touch para o nosso detector de Gesto. Para que isso aconteça, no onCreate, criamos um GestureDetector (que é outra classe do próprio Android) passando o nosso detector de gestos. Em seguida, pegamos a referência da view que queremos tratar o evento de touch e setamos o onTouchListener e o onClickListener.
O arquivo de layout usado no projeto é mostrado abaixo:
<RelativeLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/raizLayout"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >

  <TextView
    android:id="@+id/txtResultado"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:layout_centerVertical="true"
    android:textSize="40dp"
    tools:context=".MainActivity" />
</RelativeLayout>
Pronto! É só rodar a aplicação e testar SWIPE pra esquerda e direita.

Qualquer dúvida, é só deixar seus comentários.

4br4ç05,
nglauber

segunda-feira, 22 de outubro de 2012

Context Menu em Listas no Android

Olá povo,

Estava dando aula e precisei mostrar para os meus alunos um exemplo de um menu de contexto. Mas o que é um menu de contexto? É basicamente é uma lista de opções que é exibida quando o usuário dá um clique longo em um componente da tela. Isso é particularmente útil em uma lista, onde você pode realizar várias operações sobre um determinado item.
É esse exemplo que mostraremos aqui. Crie um novo projeto, e em seguida crie o arquivo menu_contexto.xml na pasta menu (essa pasta não existe, você deve criá-la).
<menu 
  xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:id="@+id/opcao1" 
    android:title="Opção 1"/>
  <item android:id="@+id/opcao2" 
    android:title="Opção 2"/>
  <item android:id="@+id/opcao3" 
    android:title="Opção 3"/>
</menu>
Esse arquivo contém as opções de menu que aparecerão no menu de contexto. Carregaremos essas opções no método onCreateContextMenu na nossa Activity.
public class MainActivity extends ListActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
        
    String[] nomes = {
      "Fulano", "Cicrano", "Beltrano", "João",
      "Maria", "José", "Antônio", "Francisco" };
        
    ArrayAdapter<String> adapter = 
      new ArrayAdapter<String>(this,
        android.R.layout.simple_list_item_1, nomes);
    setListAdapter(adapter);
    registerForContextMenu(getListView());
  }
    
  @Override
  public void onCreateContextMenu(ContextMenu menu, 
    View v, ContextMenuInfo menuInfo) {
    super.onCreateContextMenu(menu, v, menuInfo);
    getMenuInflater().inflate(
      R.menu.menu_contexto, menu);
  }
    
  @Override
  public boolean onContextItemSelected(MenuItem item){
    AdapterContextMenuInfo info = 
      (AdapterContextMenuInfo)item.getMenuInfo();
     
    String nomeSelecionado = (String)
      getListView().getItemAtPosition(info.position);
     
    switch (item.getItemId()) {
    case R.id.opcao1:
      Toast.makeText(this, "Opção 1 - "+ 
        nomeSelecionado, Toast.LENGTH_SHORT).show();
      return true;
    case R.id.opcao2:
      Toast.makeText(this, "Opção 2 - "+ 
        nomeSelecionado, Toast.LENGTH_SHORT).show();
      return true;
    case R.id.opcao3:
      Toast.makeText(this, "Opção 3 - "+ 
        nomeSelecionado, Toast.LENGTH_SHORT).show();
      return true;   
    }
    return super.onContextItemSelected(item);
  }
}
A primeira etapa para criar o menu de contexto é chamar o método registerForContextMenu, passando um objeto View. Pode ser qualquer View, mas no nosso exemplo estamos pegando a ListView (que a classe ListActivity tem internamente). No método onCreateContextMenu utilizamos a classe MenuInflater para carregar o arquivo de menu que criamos anteriormente.
Por fim, para tratar as opções de menu, utilizamos o método onContextItemSelected. Esse método começa resolvendo um problema que temos quando usamos um menu de contexto em uma lista: quando o menu de contexto é aberto, perdemos o item que estava selecionado na lista.
Felizmente, podemos obter essa informação através do método getMenuInfo, que quando chamado em um item de menu que está associada a view, que é preenchida por um Adapter (a ListView está usando um ArrayAdapter para exibir seu conteúdo), retorna uma instância da classe AdapterContextMenuInfo. Nela podemos obter a linha que foi selecionada através do atributo position.
Para saber a opção de menu que foi selecionada, usamos o método getItemId, e comparamos com os ids que foram definidos no arquivo XML de menu. Abaixo temos a nossa aplicação em execução.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

terça-feira, 16 de outubro de 2012

Pós Graduação em Dispositivos Móveis em Fortaleza-CE

Olá povo,

A convite do meu nobre colega Robério Gomes, estarei ministrando nos próximos finais de semana, a disciplina de Google Android na Especialização em Desenvolvimento de Sistema para Dispositivos Móveis na Faculdade FA7 em Fortaleza-CE.
Ainda nesse mesmo curso, ministrarei a disciplina de iOS, onde estudaremos como desenvolver aplicações para os dispositivos da Apple (iPhone, iPad e iPod).

Espero que dê tudo certo e que os alunos aproveitem.

4br4ç05,
nglauber


terça-feira, 9 de outubro de 2012

NSPredicate no iOS

Olá povo,

Na última Campus Party Recife, assisti uma palestra do professor Fernando Castor da UFPE, e uma coisa que ele falou me chamou a atenção (veja minuto 29:58): "Java é igual ao Marlon Brando, quando chegou era bonitão, mas hoje envelheceu"(e o ator já morreu).
Estou atualmente em um projeto com iOS, e recentemente fiz um curso de Adobe Air e outro de Windows Phone. O que pude notar, é que em todos esses ambientes, as linguagens evoluíram e estão trazendo dinamismos que facilitam bastante a vida do desenvolvedor. O conceito de Binding utilizado pelo Air e pelo WP7 é um exemplo desse tipo de coisa.
Depois dessa introdução poética :) hoje quero falar de um conceito bem bacana do Objective-C, linguagem utilizada para criar aplicativos para o iOS: os predicates. Eles permitem fazer buscas em listas de objetos sem ter que iterar sobre eles. Felipe Vasconcelos me mostrou isso e achei bem bacana! :)

#define KEY_NOME  @"nome"
#define KEY_IDADE @"idade"
#define KEY_PAI   @"pai"

@interface MinhaPessoa : NSObject
@property (strong, nonatomic) NSString *nome;
@property (assign, nonatomic) int idade;
@property (strong, nonatomic) MinhaPessoa *pai;
@end

@implementation MinhaPessoa

@end

int main(int argc, char *argv[])
{
  NSMutableArray *complexList = 
    [NSMutableArray new];

  // Adicionando um NSDictionary
  [complexList addObject: @{
    KEY_NOME  : @"Antônio",
    KEY_IDADE : @10,
    KEY_PAI   : @{ KEY_NOME : @"Fulano" }
  }];

  // Outro NSDictionary
  [complexList addObject: @{
    KEY_NOME  : @"Francisco",
    KEY_IDADE : @20,
    KEY_PAI   : @{ KEY_NOME : @"Fulano"}
  }];

  // Agora um objeto Pessoa
  MinhaPessoa *p  = [MinhaPessoa new];
  p.nome     = @"João";
  p.idade    = 30;
  p.pai      = [MinhaPessoa new];
  p.pai.nome = @"Cicrano";
  [complexList addObject:p];

  // De novo um NSDicionary
  [complexList addObject: @{
    KEY_NOME  : @"Nelson",
    KEY_IDADE : @28,
    KEY_PAI   : @{ KEY_NOME : @"Beltrano"}
  }];
  // E pra finalizar, outra Pessoa
  p          = [MinhaPessoa new];
  p.nome     = @"Glauber";
  p.idade    = 28;
  p.pai      = [MinhaPessoa new];
  p.pai.nome = @"Beltrano";
  [complexList addObject:p];

  // pai.nome (nome do pai que contenha B)
  NSString *attribute = [NSString 
    stringWithFormat:@"%@.%@", KEY_PAI, KEY_NOME];
  // predicado para a busca
  NSPredicate* predicate = [NSPredicate 
    predicateWithFormat:
      @"%K contains[cd] %@", attribute, @"B"];
  NSArray *resultado = [complexList 
    filteredArrayUsingPredicate:predicate];

  for (id item in resultado) {
    NSLog(@"%@", [item description]);
  }
}
O código acima mostra bem o dinamismo no Objective-C. Primeiro, criamos um array misturando dois tipos de objetos: NSDictionary e MinhaPessoa. Essas classes têm em comum que os atributos da classe MinhaPessoa são iguais às chaves usadas no dicionário.
Uma vez alimentado o array complexList, criamos um objeto NSPredicate que fará o filtro "pai.nome contains[cd] 'B'", ou seja, todos os objetos onde o nome do pai contenha "B". O 'cd' significa que a busca será case insensitive e que serão ignorados os acentos. Através do método filteredArrayUsingPredicate obtemos um array resultado com os objetos filtrados. Por fim, fazemos um foreach para percorrer os resultados.

Saiba mais sobre predicados aqui.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 1 de outubro de 2012

Upload de arquivos no Android

Olá povo,

Esse post tava há um bom tempo como rascunho no blog e só agora consegui terminar para postar aqui. Vou mostrar como fazer uma aplicação Android que faz upload de arquivos para um servidor web.
public class UploadActivity extends Activity {

  private TextView txtArquivo;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_upload);

    txtArquivo = (TextView)
      findViewById(R.id.txtArquivo);
  }
 
  // Ao selecionar o arquivo, preenchemos o 
  // TextView com o caminho real do arquivo
  @Override
  protected void onActivityResult(int requestCode, 
    int resultCode, Intent data) {
    super.onActivityResult(requestCode, 
      resultCode, data);

    if (resultCode == RESULT_OK) {
      if (requestCode == 0) {
        Uri selectedImageUri = data.getData();
        txtArquivo.setText(getPath(selectedImageUri));
      }
    }  
  }

  // Botão que abre a galeria de mídia para 
  // selecionar um arquivo
  public void selecionarArquivo(View v){
    Intent intent = new Intent();
    intent.setType("image/*");
    intent.setAction(Intent.ACTION_GET_CONTENT);
    startActivityForResult(Intent.createChooser(
      intent,"Selecione uma imagem"), 0);
  }
 
  // Botão que envia o arquivo selecionado
  public void enviarArquivo(View v){
    try {
      new Thread(){
        public void run(){
          executeMultipartPost();
        }
      }.start();
    } catch (Exception e) {
      Log.e("NGVL", e.getMessage());
    }  
  }
 
  // Obtém o caminho real do arquivo 
  // através do ContentProvider
  public String getPath(Uri uri) {
    String[] projection = { 
      MediaStore.Images.Media.DATA };

    Cursor cursor = getContentResolver().query(
      uri, projection, null, null, null);

    int column_index = cursor.getColumnIndexOrThrow(
      MediaStore.Images.Media.DATA);

    cursor.moveToFirst();
    return cursor.getString(column_index);
  }
 
  // Método que realmente envia o arquivo
  public void executeMultipartPost() 
    throws Exception {

    String caminhoDoArquivoNoDispositivo = 
      txtArquivo.getText().toString();

    String urlDoServidor = 
      "http://url_servidor/pagina_que_trata_upload";
    String lineEnd = "\r\n";
    String twoHyphens = "--";
    String boundary = "*****"; // Delimitador

    byte[] buffer;
    int bytesRead, bytesAvailable, bufferSize;
    int maxBufferSize = 1 * 1024 * 1024; // 1MB

    try {
      // Criando conexão com o servidor
      URL url = new URL(urlDoServidor);
      HttpURLConnection connection = 
        (HttpURLConnection) url.openConnection();

      // Conexão vai ler e escrever dados
      connection.setDoInput(true);
      connection.setDoOutput(true);
      connection.setUseCaches(false);

      // Setando método POST
      connection.setRequestMethod("POST");

      // Adicionando cabeçalhos
      connection.setRequestProperty(
        "Connection", "Keep-Alive");
      connection.setRequestProperty(
        "Content-Type",
        "multipart/form-data;boundary=" + boundary);
      connection.connect();
   
      // Escrevendo payload da requisição
      DataOutputStream outputStream = 
        new DataOutputStream(
          connection.getOutputStream());
      outputStream.writeBytes(
        twoHyphens + boundary + lineEnd);
      outputStream.writeBytes(
        "Content-Disposition: form-data; "+
        "name=\"uploadedfile\";filename=\""+ 
        caminhoDoArquivoNoDispositivo + "\"" +
        lineEnd);
      outputStream.writeBytes(lineEnd);

      // Stream para ler o arquivo
      FileInputStream fileInputStream = 
        new FileInputStream(new File(
          caminhoDoArquivoNoDispositivo));
   
      // Preparando para escrever arquivo
      bytesAvailable = fileInputStream.available();
      bufferSize = Math.min(
        bytesAvailable, maxBufferSize);
      buffer = new byte[bufferSize];

      // Lendo arquivo e escrevendo na conexão
      bytesRead = fileInputStream.read(
        buffer, 0, bufferSize);

      while (bytesRead > 0) {
        outputStream.write(buffer, 0, bufferSize);
        bytesAvailable = fileInputStream.available();
        bufferSize = Math.min(
          bytesAvailable, maxBufferSize);
        bytesRead = fileInputStream.read(
          buffer, 0, bufferSize);
      }
      outputStream.writeBytes(lineEnd);
      outputStream.writeBytes(
        twoHyphens + boundary + 
        twoHyphens + lineEnd);

      // Obtendo o código e a mensagem
      // de resposta do servidor
      int serverResponseCode = 
        connection.getResponseCode();
      String serverResponseMessage = 
        connection.getResponseMessage();

      Log.d("NGVL", serverResponseCode +" = "+ 
        serverResponseMessage);
   
      fileInputStream.close();
      outputStream.flush();
      outputStream.close();
   
    } catch (Exception ex) {
      // Exception handling
    }
  }
}
O código acima está todo comentado, então vou mostrar o arquivo de layout da aplicação. O único detalhe que eu quero ressaltar, é que utilizei uma Thread apenas pra facilitar o código, mas deveríamos utilizar uma AsyncTask ou até um Service.
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical" >

  <TextView
    android:id="@+id/txtArquivo"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Arquivo a ser enviado" />

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Selecionar arquivo" 
    android:onClick="selecionarArquivo"/>

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Enviar" 
    android:onClick="enviarArquivo"/>

</LinearLayout>
A última coisa a fazer é adicionar a permissão de INTERNET no AndroidManifest.xml.
<uses-permission 
  android:name="android.permission.INTERNET"/>
A figura abaixo mostra a aplicação em execução:

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber