segunda-feira, 15 de julho de 2013

ListView com seleção múltipla + ActionBar + ActionMode

Olá povo,

Depois de quase um mês sem postar, resolvi aproveitar o feriadão para colocar um post colaborativo e que me tirou um trauma. Senta que lá vem história...
Um dia desses, meu amigo André Melo (a.k.a. Deco Balaca) me perguntou como fazia uma ListView com seleção múltipla, e que os itens selecionados ficassem com o background diferente. Quatro anos mexendo com o danado do Android e me enrolei pra fazer isso.
A classe ListView tem uma propriedade android:listSelector que nos permite modificar o visual dos itens da lista de acordo com o seu estado (eu falei de selector nesse post aqui).
O problema é que apesar de haver a propriedade android:state_checked (usado também em componentes como Checkbox e ToggleButton), devemos usar a propriedade android:state_activated para indicar que um item da lista está checado (#tenso).
Muito bem! Tudo resolvido? Sim, se você não quiser dar suporte a versões anteriores à 3.0 (Honeycomb), caso contrário você terá que fazer as coisas manualmente.
Meu aluno do TECDAM, Carlos Eduardo Carneiro, me deu essa dica. Abaixo temos um adapter que verifica se a linha está checada, em caso positivo, ele muda o background da View da linha correspondente.
class MultiSelectAdapter extends ArrayAdapter<String>{

  public MultiSelectAdapter(
    Context context, 
    int textViewResourceId,
    List<String> objects) {

    super(context, textViewResourceId, objects);
  }

  @Override
  public View getView(int position, 
    View convertView, ViewGroup parent) {

    View v = super.getView(
      position, convertView, parent);

    ListView listView = (ListView) parent;
    int color = listView.isItemChecked(position) ? 
      Color.argb(0xFF, 0x31, 0xB6, 0xE7) : 
      Color.TRANSPARENT;

    v.setBackgroundColor(color);

    return v;
  }
}
Estou usando um ArrayAdapter simples, mas essa abordagem funcionaria para um adapter customizado também, só que você setaria o background do layout que você carregou...
Resolvido esse problema, resolvi aproveitar esse post para mostrar como utilizar um recurso muito utilizado em aplicações Android: o Action Mode. Essa é uma característica da ActionBar do Android que permite exibir opções de menu de acordo com a necessidade.
Se tornou praticamente padrão, excluir múltiplos itens de uma lista dando um clique longo em um dos itens e depois selecionar outros (podemos ver isso no Gmail, galeria de mídia, etc.). No nosso exemplo, vou mostrar como permitir excluir múltiplos itens de uma ListView. Mas você pode estar se perguntando: ActionBar não é só pro Android 3.0 ou superior? Dê uma olhada nesse link e conheça o Sherlock, pois vamos usá-lo aqui.
O exemplo consta apenas de uma Activity mostrada (uma parte) abaixo:
public class MainActivity 
  extends SherlockActivity 
  implements 
    OnItemClickListener, 
    OnItemLongClickListener, 
    ActionMode.Callback {

  private ListView listView;
  private List<String> nomes;
  private ActionMode actionMode;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setTheme(R.style.Theme_Sherlock_Light_DarkActionBar);

    nomes = new ArrayList<String>();
    nomes.add("nglauber");
    nomes.add("CESAR.edu");
    nomes.add("CESAR");
    nomes.add("Unibratec");

    listView = new ListView(this);
    listView.setOnItemClickListener(this);
    listView.setOnItemLongClickListener(this);
    listView.setAdapter(new MultiSelectAdapter(this,
      android.R.layout.simple_list_item_1, nomes));
      setContentView(listView);
  }

  @Override
  public void onItemClick(
    AdapterView<?> adapterView, View view,

    int position, long id) {
    if (actionMode == null) {
      // Faça algo ao clicar no item normalmente
    } else {
      int checkedCount =  
        atualizarItensMarcados(listView, position);

      if (checkedCount == 0) {
        actionMode.finish();
      }
    }
  }

  @Override
  public boolean onItemLongClick(
    AdapterView<?> adapterView, View view,
    int position, long id) {

    boolean consumed = (actionMode == null);

    if (consumed) {
      actionMode = startActionMode(this);
      listView.setChoiceMode(
        ListView.CHOICE_MODE_MULTIPLE);

      listView.setItemChecked(position, true);
      atualizarItensMarcados(listView, position);
    }
    return consumed;
  }
A nossa classe implementa três interfaces, onde as duas primeiras são bem conhecidas. A última é que vamos detalhar mais no próximo trecho de código. O atributo actionMode é que vai exibir a opção de excluir e mostrar quantos itens estão selecionados. Esse atributo é inicializado no método onItemLongClick através da chamada do método startActionMode. Nesse momento, habilitamos a seleção múltipla na ListView e já checamos o item da posição onde clicamos.
Sendo assim, no método onItemClick, nós verificamos se actionMode é igual a null, nesse caso, não estamos com o ActionMode ativo na ActionBar e devemos processar o clique normalmente. Caso contrário, atualizamos a lista com novo item que foi clicado. Mas se não houver mais itens selecionados, desativamos o ActionMode chamando o método finish.

Vamos agora ver o código da interface ActionMode.Callback e o método atualizarItensMarcados.
  @Override
  public boolean onCreateActionMode(
    ActionMode mode, Menu menu) {

    getSupportMenuInflater().inflate(
      R.menu.menu_delete_list, menu);

    return true;
  }

  @Override
  public boolean onPrepareActionMode(
    ActionMode mode, Menu menu) {

    return false;
  }

  @Override
  public boolean onActionItemClicked(
    ActionMode mode, MenuItem item) {

    if (item.getItemId() == R.id.action_delete) {
      SparseBooleanArray checked = 
        listView.getCheckedItemPositions();

      for (int i = checked.size()-1; i >= 0; i--) {
        if (checked.valueAt(i)) {
          nomes.remove(checked.keyAt(i));
        }
      }
      actionMode.finish();
      return true;
    }
    return false;
  }

  @Override
  public void onDestroyActionMode(ActionMode mode) {
    actionMode = null;
    listView.clearChoices();
    ((BaseAdapter) listView.getAdapter())
      .notifyDataSetChanged();

    listView.setChoiceMode(ListView.CHOICE_MODE_NONE);
  }

  private int atualizarItensMarcados(
    ListView l, int position) {

    SparseBooleanArray checked = 
      l.getCheckedItemPositions();

    l.setItemChecked(position, 
      l.isItemChecked(position));

    int checkedCount = 0;
    for (int i = 0; i < checked.size(); i++) {
      if (checked.valueAt(i)) {
        checkedCount++;
      }
    }

    actionMode.setTitle(
      checkedCount + " selecionados");

    return checkedCount;
  }
}
O método onCreateActionMode permite carregar um arquivo de menu para actionMode, note que ele retorna true para informar que o actionMode pode ser criado. Já o método onPrepareActionMode é usado para quando queremos atualizar o actionMode após sua criação, retornamos falso para indicar que ele não foi atualizado. Quando uma opção de menu do ActionMode é selecionada, o método onActionItemClicked é chamado. Nesse método, checamos se a opção selecionada foi a de excluir, em caso positivo, obtemos a lista das posições da lista que estão marcadas. Removemos essas posições da lista e depois finalizamos o ActionMode.
Ao chamar o método finish do ActionMode, o método onDestroyActionMode é chamado. Nesse momento, setamos o atributo actionMode para null, desmarcamos os itens (caso haja algum), chamamos o notifyDatasetChanged para que a ListView seja redesenhada, e por fim, voltamos o tipo de seleção da lista como nenhum.
O método atualizarItensMarcados apenas retorna a quantidade de itens que estão checados e seta o título da actionMode.

O arquivo de menu utilizado no método onCreateActionMode é mostrado abaixo:
<menu 
  xmlns:android="http://schemas.android.com/apk/res/android">
  <item
    android:id="@+id/action_delete"
    android:icon="@android:drawable/ic_menu_delete"
    android:orderInCategory="100"
    android:showAsAction="ifRoom"/>
</menu>

E o resultado, podemos ver na figura a seguir:

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

quarta-feira, 19 de junho de 2013

SlidingPaneLayout: um menu lateral estilo Facebook

Olá povo,

Uma das coisas boas de mexer a muito tempo com uma tecnologia, é que as pessoas sempre perguntam a você, mas obviamente, nem sempre você sabe a resposta. Meu amigo Rodrigo Jardim (a.k.a. Praieiro), me perguntou se eu conhecia o SlidingPaneLayout, e eu nunca tinha visto o dito cujo. Mas ele permite implementarmos um padrão de interface gráfica amplamente utilizada nas aplicações Android: menus laterais. Abaixo, coloquei um screenshot de duas "pequenas" aplicações que usam essa abordagem.


Como podemos notar, o Facebook e o GMail são dois bons exemplos de grandes aplicações que usam essa abordagem. Nesse post vou mostrar como dar os primeiros passos para construir um menu desse tipo. Vamos começar pelo arquivo de layout da aplicação, mostrado abaixo.
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SlidingPaneLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/sliding_pane_layout"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >

  <!-- Menu Lateral -->
  <ListView
    android:id="@+id/left_pane"
    android:layout_width="280dp"
    android:layout_height="match_parent"
    android:layout_gravity="left" />
    
  <!-- Conteúdo da tela -->
  <RelativeLayout
    android:id="@+id/rightPane"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ff333333" >

    <Button
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:text="MENU" 
      android:onClick="abrirMenu"/>

    <ImageView
      android:id="@+id/imageView1"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:src="@drawable/ic_launcher" />

  </RelativeLayout>

</android.support.v4.widget.SlidingPaneLayout>
No SlidingPanelLayout temos apenas duas partes, a primeira (uma ListView) será o menu da aplicação. Enquanto que a segunda, será o conteúdo da tela em si. Abaixo temos o código da Activity.
public class MainActivity extends FragmentActivity 
  implements OnItemClickListener, PanelSlideListener {

  private SlidingPaneLayout mSlidingLayout;
  private ListView mList;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Colocando a Activity em tela cheia (opcional)
    requestWindowFeature(Window.FEATURE_NO_TITLE);

    setContentView(R.layout.activity_main);
  

    mSlidingLayout = (SlidingPaneLayout) 
      findViewById(R.id.sliding_pane_layout);
    mSlidingLayout.setPanelSlideListener(this);
  
    String[] opcoes = new String[] { 
      "Opção 1", "Opção 2", "Opção 3",
      "Opção 4", "Opção 5" };

    mList = (ListView) findViewById(R.id.left_pane);
    mList.setAdapter(new ArrayAdapter<String>(
      this, 
      android.R.layout.simple_list_item_1, 
      opcoes));
    mList.setOnItemClickListener(this);
  }

  // Evento de clique do botão
  public void abrirMenu(View v){
    // Se estive aberto, feche. Senão abra.
    if (mSlidingLayout.isOpen()){
      mSlidingLayout.closePane();
    } else {
      mSlidingLayout.openPane();
    }
  }

  @Override
  public void onItemClick(AdapterView<?> adapterView, 
    View view, int position, long id) {
    // TODO Tratar opções de Menu (ListView) aqui!
  }

  @Override
  public void onPanelClosed(View arg0) {
    // TODO Ao fechar o painel
  }

  @Override
  public void onPanelOpened(View arg0) {
    // TODO Ao abrir o painel
  }

  @Override
  public void onPanelSlide(View arg0, float arg1) {
    // TODO Enquanto o painel desliza
  }
}
O código acima é bem simples e está comentado.
O mais legal desse componente é que ao deslizar o dedo sobre a tela da esquerda para direita, o menu exibido (da direita para esquerda, ele fecha). Ou seja, o botão acima é opcional, mas indicado, como vemos nas aplicações em geral (como a do Facebook e Gmail) para facilitar a visualização do usuário.
Abaixo podemos ver nossa aplicações em execução com o menu lateral aberto.

É isso pessoal! Temos um menu no padrão de aplicações profissionais. Vale salientar que esse componente funciona em todas as versões do Android.

EDITADO em 29/11/2013

Várias pessoas me perguntaram (por email.... deixem os comentários aqui povo!) o que fazer com o clique do item da lista. O mais legal aqui é usar Fragments e sua stack. Ou seja, criar uma pilha de Fragments como o Android faz com as Activities. Ou seja, ao invés de irmos chamando Activities, chamamos Fragments e vamos empilhando-os no layout da direita.
Vamos ao exemplo... Crie a classe MeuFragment conforme abaixo:
public class MeuFragment extends Fragment {

  public static MeuFragment newInstance(String s){
    Bundle args = new Bundle();
    args.putString("texto", s);
  
    MeuFragment f = new MeuFragment();
    f.setArguments(args);
    return f;
  }
 
  @Override
  public View onCreateView(LayoutInflater inflater, 
    ViewGroup container, Bundle savedInstanceState) {

    View layout = inflater.inflate(
      R.layout.meu_fragment, container, false);

    TextView txt = (TextView)
      layout.findViewById(R.id.textView1);

    txt.setText(getArguments().getString("texto"));
    return layout;
  }
}
O arquivo de layout do Fragment acima, só tem um RelativeLayout com um TextView centralizado (nada de mais, vocês conseguem :)
Agora, no clique de cada um dos itens, instanciamos esse fragment passando o texto da opção clicada.
@Override
public void onItemClick(AdapterView<?> adapterView, 
  View view, int position, long id) {

  String opcao = (String)
    mList.getAdapter().getItem(position);
  
  MeuFragment f = MeuFragment.newInstance(opcao);
  
  FragmentManager fm = getSupportFragmentManager();
  // Opcional: isso removerá o fragment anterior 
  // da pilha.
  fm.popBackStack(); 
  
  fm.beginTransaction()
    .replace(R.id.rightPane, f, "frag1")
    .addToBackStack(null)
    .commit();  
}
O método addToBackStack vai adicionar o Fragment a uma pilha, dessa forma, ao clicar no botão back do aparelho, o fragment será removido automaticamente e anterior será exibido.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

Fonte: http://androidtrainningcenter.blogspot.com.br/2013/06/slidingpanelayout-android-making.html

quarta-feira, 22 de maio de 2013

Android: Dicas 9

Olá povo,

Segue mais um post da série "Dicas de Android". Aproveitem!

Dica 1 - Driver ADB universal para Windows

Essa dica foi dada pelo Pedro Borba, meu aluno da Unibratec. Muitas vezes quando conectamos alguns devices menos populares e eles não são reconhecidos pelo Windows, e às vezes é até complicado achar esses drivers na internet. O site abaixo disponibiliza uma versão do driver ADB, que funciona em muitos desses aparelhos. Vale a pena conferir.

http://adbdriver.com/

Dica 2 - Imagem de background repetida
Como nas velhas páginas HTML, às vezes é interessante colocar uma imagem de background repetida lado a lado, dando a impressão que é uma imagem maior. A solução é criar um arquivo XML (meu_bg.xml por exemplo) na pasta drawable com o seguinte conteúdo.
<bitmap 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:src="@drawable/minha_imagem_que_repete"
  android:tileMode="repeat" />
Depois é só colocar @drawable/meu_bg como sua imagem de background.

Dica 3 - Vários núcleos do processador para o Emulador
Essa dica foi dada por Daniel Sant'Ana, meu aluno do TECDAM. Quando executamos o emulador,  apenas 1 núcleo do processador da máquina é ativado. Para permitir que o emulador utilize os outros núcleos do processador, abra o gerenciador de tarefas do windows, clique em processos, e encontre o processo referente ao emulador. Em seguida, clique com o botão direito nele, e escolha a opção afinidade, então marque todos os núcleos. Se quiser, você ainda pode definir a prioridade do processo do emulador como alta. Feito isso, é possível ganhar um pouco de desempenho no emulador.

Outra opção, em fase experimental, pode ser vista aqui, na seção "Configuring Virtual Machine Acceleration". Ou aqui, no post escrito pelo meu colega Eric Cavalcanti.

Dica 4 - HttpURLConnection ou HTTPClient
A maioria das aplicações Android utiliza HTTP para enviar e receber dados. O Android tem duas APIs para essa tarefa: HTTPClient da Apache e HttpURLConnection do próprio Java.
Conforme podemos ver aqui e aqui, o pessoal da Google está recomendando utilizar o HttpURLConnection para aplicações voltadas para Android 2.3 ou superior. Nas versões 2.2 e inferiores, essa API tinha uma série de bugs que foram corrigidos a partir da versão seguinte. Além disso, alguns recursos como compressão e cache.

Dica 5 - DumbleDroid e WebCachedImageView
Meu amigo Leocádio Tiné, um dos caras que mais conhece de Android que eu conheço, acabou de disponibilizar para a comunidade essas duas bibliotecas interessantíssimas. A primeira faz o download de arquivos XML e JSON e autoMAGICAmente cria as classes Java que representam essas estruturas.
Perguntei ao Leocádio qual a diferença entre essa biblioteca e a GSON (da Google) e o Simple XML. E a resposta foi:

"A diferença básica do Dumbledroid pra essas 2 libraries é que o Dumbledroid faz caching automático em memória e em disco, e roda código específico pra Android. Por exemplo: ele usa as classes de JSON incluídas no Android SDK, e não as do Java SDK, como o GSON. As diferenças específicas: GSON: Usando GSON, você tem que carregar o JSON manualmente. Usando Dumbledroid, basta passar a URL que ele faz o carregamento, parsing e caching. É mais simples. SimpleXML: Usando SimpleXML, além de ter que carregar o XML manualmente, você tem que escrever annotations nas classes pra mapear os campos aos nós do XML. No Dumbledroid, isso não é preciso."

Além disso o DumbleDroid já tem um plugin para Eclipse que facilita ainda mais o seu uso.
Já o WebCachedImageView é bem similar ao ImageView nativo, com o benefício de passarmos apenas a URL da imagem que desejamos e ainda fazer o cache da mesma.

Segue os links para download das libs:
https://github.com/leocadiotine/Dumbledroid
https://github.com/leocadiotine/WebCachedImageView

Vou tentar fazer um post só pra essas duas libs em breve.

Dica 6 - Detectando JavaScript da WebView na Activity
Um recurso que pode trazer grandes possibilidades é conseguirmos capturar funções Java Script que são executadas dentro de uma WebView. Criem o arquivo meu.html dentro da pasta assets do projeto e deixe-o conforme abaixo.
<html>
<header>
<script type="text/javascript">
function showAndroidToast(s, t) {
  window.nglauber.showToast(s, t);
} 
</script>
</header>
<body>
  <H1>Formulario em HTML</h1>
  <form name="meuForm">
    Nome: 
    <input type="text" name="txtNome"/><br>
    Idade: 
    <input type="text" name="txtIdade"/><br>
    <input type="button" 
      onclick="showAndroidToast(txtNome.value, txtIdade.value);" 
      value="Enviar">
  </form>
</body>
</html>
É possível interceptar a função showAndroidToast e chamar o método showToast dentro da nossa Activity. Dessa forma, quando o usuário pressionar o botão do HTML, o código da Activity será executado.
public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  
    WebView wv = (WebView)findViewById(R.id.webView1);
    WebSettings settings = wv.getSettings();
    settings.setJavaScriptEnabled(true);
    wv.addJavascriptInterface(this, "nglauber");
    wv.loadUrl("file:///android_asset/meu.html");
  }

  @JavascriptInterface
  public void showToast(String s, String t) {
    Toast.makeText(this, 
      "Nome:"+ s + " Idade:"+ t, 
      Toast.LENGTH_SHORT).show();
  }
}
Apesar de não ser algo que não encorajo, essa solução pode ser útil para alguns tipos de tela. Disponibilizamos a interface "nglauber" para o código JavaScript chamar. Notem que no JavaScript ficou window.nglauber.showToast. Outro detalhe aqui é que existe um bug do Android 2.3 que impede que esse código funcione nessa versão. Outro detalhe é a Annotation @JavascriptInterface que só é necessária a partir do Android 4.2, em versões anteriores, basta o método ser público.

Dica 7 - Definindo uma orientação fixa via código
É possível definir uma orientação fixa para um Activity através do arquivo AndroidManifest.xml através da propriedade screenOrientation.
<activity name=".MinhaActivity"   
  android:screenOrientation="landscape"/>
Mas e se quisermos que essa configuração seja feita dependendo de alguma condição? Podemos usar o código abaixo.
setRequestedOrientation(
  ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)

Dica 8 - Versão do Android do Aparelho
Obtendo a versão do Android que está rodando no aparelho.
int apiLevel = Build.VERSION.SDK_INT;
if (apiLevel < Build.VERSION_CODES.HONEYCOMB){
  // Usando Android 2.3 ou inferior
} else if (apiLevel < 
  Build.VERSION_CODES.ICE_CREAM_SANDWICH){
  // Android 3.x
} else {
  // ICS ou superior
}

4br4ç05,
nglauber

domingo, 5 de maio de 2013

Deploy/Debug via Wi-Fi no Android

Olá povo,

Recebi essa semana uma dúvida muito interessante do meu colega Kennedy Ximenes me perguntando se era possível fazer deploy de aplicações Android, em um dispositivo real, sem a necessidade do cabo USB. Fui pequisar e achei a solução aqui. O procedimento é bem simples e vou mostrar para vocês aqui.

Parto do pressuposto que você já consegue depurar a aplicação via USB, então você já deve possuir Google USB Driver e estar com a depuração USB habilitada nas configurações do aparelho. Feito isso, abra o terminal e vá até o diretório ANDROID_SDK/platform-tools (onde ANDROID_SDK é o diretório onde está instalado o SDK do Android). Conecte seu dispositivo via USB (é, você vai precisar do cabo nessa etapa). Depois é só digitar:

./adb tcpip 5555

Estamos habilitando o ADB para aceitar conexão via TCP através da porta 5555. O resultado deve ser como abaixo.

* daemon not running. starting it now on port 5037 *
* daemon started successfully *
restarting in TCP mode port: 5555

Depois é só conectar-se ao aparelho digitando o seguinte comando.

./adb connect 192.168.25.2:5555

Troque o endereço acima pelo IP do seu aparelho na rede Wi-Fi. Você pode checar o número IP do aparelho em Configurações > Sobre > Status. Se tudo sair bem, você receberá a mensagem abaixo:
connected to 192.168.25.2:5555

Para testar, digite:

./adb devices

Se estiver tudo ok, seu aparelho será listado como abaixo.

List of devices attached 
192.168.25.2:5555 device

Agora é só ir no Eclipse e mandar executar sua aplicação, que ela será executada magicamente no aparelho :) Obviamente também é possível fazer o debug (is on the table) da mesma.

Para voltar ao modo USB, use o comando:

./adb usb

E depois desconectamos usando o comando:

./adb disconnect 192.168.25.2:5555

4br4ç05,
nglauber

Fonte: Tech And Stuff
http://stuffandtech.blogspot.com.br/2012/03/android-quick-tip-adb-over-wifi.html


quarta-feira, 1 de maio de 2013

AsyncTaskLoader

Olá povo,

Uma das coisas mais comuns em aplicações móveis é o acesso a servidores web através do protocolo HTTP. O Android obviamente nos permite esse tipo de comunicação de várias formas. Mas uma forma interessante foi introduzida na versão 3 da plataforma: os Loaders. Esse tipo de abordagem permite isolar a Activity da classe que está fazendo o download dos dados.
Um exemplo típico dessa abordagem é a utilização da classe AsyncTask - que eu sempre usei, e ainda uso - jutamente com um ProgressDialog. Normalmente o ProgressDialog era um atributo da AsyncTask  e no onPreExecute ele era exibido, sendo ocultado no onPostExecute. O problema dessa abordagem é que ao rotacionar a tela do aparelho, a Activity é recriada. Dessa forma, quando a AsyncTask terminava seu trabalho, a instância do Dialog armazenada ficava apontando para a instância da Activity que o tinha criado, gerando uma exceção em tempo de execução.

Com a chegada dos Fragments, é possível reter a instância do mesmo e ter um controle melhor sobre a rotação da tela (e outras mudanças de configuração). Mas a abordagem que eu achei interessante é a utilização da classe AsyncTaskLoader em conjunto com o LoadManager e a interface LoaderCallbacks. Vejamos o exemplo abaixo:
public class MainActivity 
  extends FragmentActivity 
  implements LoaderCallbacks<String> {

  ProgressDialog dialog;
  String json;
 
  private static final int MEU_LOADER = 0;

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

    // Recuperando o estado da Activity
    if (savedInstanceState != null) {
      json = savedInstanceState.getString("valores");
      atualizaTela();
    }

    // Essa classe gerencia todos os Loaders
    LoaderManager lm = getSupportLoaderManager();

    // Se o JSON ainda não foi baixado
    if (json == null) {
      // Exibe o dialog
      dialog = ProgressDialog.show(
        this, "Aguarde...", "Baixando tweets");

      // E inicializa o Loader (se ele já foi 
      // inicializado, ele apenas continuará)
      lm.initLoader(MEU_LOADER, null, this);
    }
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    // Se estiver exibindo o progress, remova-o 
    if (dialog != null && dialog.isShowing()) 
      dialog.dismiss();
  } 

  @Override
  protected void onSaveInstanceState(Bundle outState){
    super.onSaveInstanceState(outState);
    // Salvando o estado da Activity
    outState.putString("valores", json);
  }

  // Métodos da interface LoaderCallbacks -----------
  @Override
  public Loader<String> onCreateLoader(
    int id, Bundle args) {
    // Instancia e retorna a AsyncTaskLoader
    return new MinhaAsyncTask(this);
  }

  @Override
  public void onLoadFinished(Loader<String> loader, 
    String dados) {
    // Chamado quando o Loader termina seu trabalho
    // Aqui, atribuímos o resultado ao 
    // atributo json e ocultamos o dialog
    json = dados;
    atualizaTela();
    dialog.dismiss();
  }

  @Override
  public void onLoaderReset(Loader<String> loader) {
     // Chamado se o Loader for resetado
     Log.d("NGVL", "onLoaderReset");
  }

  // AsyncTaskLoader
  public static class MinhaAsyncTask 
    extends AsyncTaskLoader<String> {

    // O JSON baixado será armazenado aqui para ser 
    // repassado para Activity
    String data;

    public MinhaAsyncTask(Context context) {
      super(context);
    }

    @Override
    protected void onStartLoading() {
      // Se não baixou os dados, faça agora!
      if (data == null) {
        forceLoad();

      // Se já baixou, apenas entregue o resultado 
      } else {
        deliverResult(data);
      }
    }

    @Override
    public String loadInBackground() {
      // Nesse método será feito o download do JSON
      return downloadJSON();
    }
  }
}
O código acima está todo comentado. Então vou focar nos detalhes que devem ser observados.
O primeiro ponto é utilizar as classes do pacote android.support.v4.* pois o conceito de Loaders só surgiu no Honeycomb, então utilizei aqui a API de compatibilidade para que esse código execute sem problemas nas versões anteriores ao Android 3. O segundo ponto interessante é que, com essa abordagem o download não é reiniciado quando a Activity é recriada.
Outro detalhe é que, caso você queira chamar o loader novamente, você deve usar o método restartLoader e não o initLoader. E se quiser passar parâmetros para o Loader, pode usar um objeto Bundle e passá-lo como o segundo argumento desses métodos.
getSupportLoaderManager().restartLoader(
  MEU_LOADER, null, this);
Os métodos downloadJSON() e atualizaTela() devem ser implementados por você :) para baixar o JSON desejado e atualizar a tela da maneira apropriada.
Abaixo eu coloquei um exemplo desses dois métodos que baixa o JSON de uma busca no Twitter e exibe os textos dos Tweets em uma ListView.
private void atualizaLista() {
  if (json == null) return;
  try {
    JSONObject jsonObj = new JSONObject(json);
    JSONArray jsonResults = 
      jsonObj.getJSONArray("results");
   
    List<String> texts = new ArrayList<String>();
    for (int i = 0; i < jsonResults.length(); i++) {
      JSONObject result = jsonResults.getJSONObject(i);
      texts.add(result.getString("text"));
    }
   
    ListView list = (ListView)
      findViewById(R.id.ListView1);

    list.setAdapter(new ArrayAdapter<String>(this, 
      android.R.layout.simple_list_item_1, texts));
   
  } catch (JSONException e) {
    e.printStackTrace();
  }
}
 
private static String downloadJSON() {
  try {
    URL url = new URL(
      "http://search.twitter.com/search.json?q=jctransito");
    HttpURLConnection conexao = (HttpURLConnection)
      url.openConnection();
    conexao.connect();
   
    InputStream is = conexao.getInputStream();
    BufferedReader reader = new BufferedReader(
      new InputStreamReader(is));

    String s = reader.readLine();
    return s;
   
  } catch (Exception e) {
    e.printStackTrace();
  }
  return null;
}
Não esqueça de adicionar a permissão de INTERNET no AndroidManifest.xml.

Com abordagem acima, mesmo ao girar o aparelho, o download continuará, e caso não tenha terminado quando a Activity carregar, o ProgressDialog será exibido novamente.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

Referências:
http://developer.android.com/reference/android/app/LoaderManager.html
http://developer.android.com/reference/android/app/LoaderManager.LoaderCallbacks.html
http://developer.android.com/reference/android/content/AsyncTaskLoader.html

sexta-feira, 12 de abril de 2013

Drag and Drop no Android

Olá povo,

Desde a versão 3 (API Level 11) o Android conta com uma API de Drag and Drop que facilita para o desenvolvedor a implementação da famosa ação de "arrastar e soltar". Nesse post, que foi baseado no texto orginal de Lars Vogel, vou mostrar um exemplo simples de como usar esse recurso na sua aplicação.

Para começar, crie um novo projeto Android e certifique-se de estar usando a API Level 11 ou superior. O projeto constará basicamente de uma tela com 4 LinearLayouts com uma ImageView em cada um deles. A idéia é que a ImageView de cada LinearLayout possa ser arrastada para outro LinearLayout.

Vamos começar criando a pasta res/drawable, onde vamos adicionar dois XMLs: o primeiro servirá de background normal dos LinearLayouts citados anteriormente, enquanto que o segundo será usado de background para quando estivermos arrastando a ImageView por sobre o LinearLayout.

O primeiro arquivo será o bg.xml
<shape 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:shape="rectangle" >

  <stroke
    android:width="2dp"
    android:color="#FFFFFFFF" />

  <gradient
    android:angle="225"
    android:endColor="#DD2ECCFA"
    android:startColor="#DD000000" />

  <corners
    android:bottomLeftRadius="7dp"
    android:bottomRightRadius="7dp"
    android:topLeftRadius="7dp"
    android:topRightRadius="7dp" />
</shape> 
O arquivo bg_over.xml é idêntico ao primeiro, a única mudança é a propriedade stroke color. Ela faz com que a cor da borda fique vermelha.
<shape 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:shape="rectangle" >

  <stroke
    android:width="2dp"
    android:color="##FFFF0000" />

  <gradient
    android:angle="225"
    android:endColor="#DD2ECCFA"
    android:startColor="#DD000000" />

  <corners
    android:bottomLeftRadius="7dp"
    android:bottomRightRadius="7dp"
    android:topLeftRadius="7dp"
    android:topRightRadius="7dp" />
</shape> 
O arquivo de layout da aplicação ficará como abaixo:
<TableLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/layoutRoot"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical" >
  <TableRow>
    <LinearLayout
      android:id="@+id/topleft"
      android:layout_width="0dp"
      android:layout_height="200dp"
      android:layout_weight="1"
      android:background="@drawable/bg" >
      <ImageView
        android:id="@+id/myimage1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />
    </LinearLayout>
    <LinearLayout
      android:id="@+id/topright"
      android:layout_width="0dp"
      android:layout_height="200dp"
      android:layout_weight="1"
      android:background="@drawable/bg" >
      <ImageView
        android:id="@+id/myimage2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />
    </LinearLayout>
  </TableRow>
  <TableRow>
    <LinearLayout
      android:id="@+id/bottomleft"
      android:layout_width="0dp"
      android:layout_height="200dp"
      android:layout_weight="1"
      android:background="@drawable/bg" >
      <ImageView
        android:id="@+id/myimage3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />
    </LinearLayout>
    <LinearLayout
      android:id="@+id/bottomright"
      android:layout_width="0dp"
      android:layout_height="200dp"
      android:layout_weight="1"
      android:background="@drawable/bg" >

      <ImageView
        android:id="@+id/myimage4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />
    </LinearLayout>
  </TableRow>
</TableLayout>
O Layout não tem nada de especial. Os LinearLayouts topleft, topright, bottomleft e bottomright servirão de containers onde poderemos arrastar e soltar as ImageViews myimage1, myimage2, myimage3 e myimage4. Vamos agora para o código da Activity.
public class MainActivity extends Activity 
  implements OnTouchListener, OnDragListener {
 
  Drawable enterShape;
  Drawable normalShape;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  
    enterShape = getResources().
      getDrawable(R.drawable.bg_over);
    normalShape = getResources().
      getDrawable(R.drawable.bg);
  
    findViewById(R.id.myimage1).
      setOnTouchListener(this);
    findViewById(R.id.myimage2).
      setOnTouchListener(this);
    findViewById(R.id.myimage3).
      setOnTouchListener(this);
    findViewById(R.id.myimage4).
      setOnTouchListener(this);
  
    findViewById(R.id.topleft).
      setOnDragListener(this);
    findViewById(R.id.topright).
      setOnDragListener(this);
    findViewById(R.id.bottomleft).
      setOnDragListener(this);
    findViewById(R.id.bottomright).
      setOnDragListener(this);
  }

  public boolean onTouch(View view, MotionEvent me) {
    int action = me.getAction(); 
    if (action == MotionEvent.ACTION_DOWN) {
      ClipData data = ClipData.newPlainText("", "");
      DragShadowBuilder shadowBuilder = 
        new View.DragShadowBuilder(view);

      view.startDrag(data, shadowBuilder, view, 0);
      view.setVisibility(View.INVISIBLE);
      return true;
    }
    return false;
  }

  @Override
  public boolean onDrag(View v, DragEvent event) {
    switch (event.getAction()) {
    case DragEvent.ACTION_DRAG_ENTERED:
      // Ao entrar na área que pode fazer o drop
      v.setBackgroundDrawable(enterShape);
      break;
    case DragEvent.ACTION_DRAG_EXITED:
      // Ao sair da área que pode fazer o drop
      v.setBackgroundDrawable(normalShape);
      break;
    case DragEvent.ACTION_DROP:
      // Ao fazer o drop
      View view = (View) event.getLocalState();
      ViewGroup owner = (ViewGroup) view.getParent();
      owner.removeView(view);
      LinearLayout container = (LinearLayout) v;
      container.addView(view);
      view.setVisibility(View.VISIBLE);
      break;
    case DragEvent.ACTION_DRAG_ENDED:
      // Ao terminar de arrastar
      v.setBackgroundDrawable(normalShape);
      View view2 = (View) event.getLocalState();
      view2.setVisibility(View.VISIBLE);
    default:
      break;
    }
    return true;
  }
}
O código é relativamente simples e vou dar atenção aos principais pontos do drag and drop. Nossa Activity implementa duas interfaces: OnTouchListener e OnDragListener. A primeira tem a implementação no método onTouch e a segunda no onDrag. No onCreate, chamamos setOnTouchListener para as quatro ImageViews declaradas no arquivo de Layout, e logo em seguida chamamos o setOnDragListener para os LinearLayouts que servirão de container para soltarmos a ImageView dentro.

No método onTouch é onde começará o processo de drag (arrastar), nesse momento verificamos se a ação de touch é ACTION_DOWN, o que quer dizer que o dedo está pressionado sobre a ImageView.  Em caso positivo, criamos um objeto ClipData que permite transportar alguma informação junto com o objeto que está sendo arrastado. Já o objeto DragShadowBuilder serve para criar uma cópia da ImageView que será arrastada com o efeito de transparência. Para definitivamente iniciarmos o processo de arrastar, chamamos o método startDrag passando os objetos citados anteriormente juntamente com a View que disparou o evento, que no nosso caso, é uma ImageView. Por fim, deixamos a View invisível.

Já a ação de drop (soltar) é feita no método onDrag que é chamado algumas vezes com ações diferentes. Esse método é chamado enquanto a ImageView está sendo arrastada (e ao ser solta) por sobre o LinearLayout. Ao entrar na área que podemos soltar o ImageView, esse método é chamado com a ação será ACTION_DRAG_ENTERED, nesse caso, apenas mudamos o background do LinearLayout. Fazemos o inverso ao sair da área onde não podemos mais soltar a ImageView, onde a ação será ACTION_DRAG_EXITED. Já o ato de soltar propriamente dito é feito quando a ação é igual a ACTION_DROP, nesse momento realizamos as seguintes tarefas: obtemos a ImageView real que estamos arrastando através do método getLocalState(); pegamos o LinearLayout onde ela está contida com o método getParent(); removemos a ImageView do LinearLayout anterior; adicionamos no novo LinearLayout (o parâmetro v, indica o LinearLayout onde estamos soltando a ImageView); e deixamos a ImageView visível. A ação ACTION_DRAG_ENDED acontece quando todo o processo de Drag&Drop termina, nesse momento redefinimos o background e tornamos a ImageView visível novamente.
Abaixo podemos ver a aplicação em execução.
Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

quarta-feira, 3 de abril de 2013

Jornada Acadêmica Unibratec 2013.1

Olá povo,

Nos últimos dias 2 e 3 de Abril aconteceu mais uma Jornada Acadêmica da Unibratec. Como já é de costume, ministrei uma palestra, dessa vez com o título "Android e iOS: o que eles tem de diferente... ou não" em dois dias de sala lotada. Aproveito para agradecer a presença de todos.
Na oportunidade, falei sobre as diferenças e semelhanças das plataformas do Google e da Apple. Ao final da palestra, desenvolvemos uma aplicação simples em ambas as plataformas.

Os slides da apresentação estão disponíveis abaixo.


4br4ç05,
nglauber