terça-feira, 31 de janeiro de 2012

Fragmentos para todos

Olá povo,

Uma das novidades do Android 3 (Honeycomb), foi a adição do conceito de Fragments. Com essa versão foi escrita exclusivamente para tablets, os "fragmentos" permitem a separação de uma Activity em pedaços de modo a aproveitar os grande tamanho de tela desse tipo de dispositivo. Os Fragments funcionam como sub-activities (dentro de uma Activity), e têm seu próprio ciclo de vida, tratando também os eventos dos componentes nele contidos.

Você pode estar se perguntando: ótimo, mas isso é só pro Android 3 ou superior, então não roda no 2.x. Você estaria certo se a Google não tivesse disponibilizado uma API de compatibilidade, que permite usufruir de alguns recursos do Honeycomb em aparelhos 1.6 (Donut) ou superior. Essa biblioteca está disponível no Android SDK Manager, o mesmo lugar onde você baixa os pacotes necessários para o desenvolvimento Android. Essa API é basicamente um JAR que deve ser adicionado no seu projeto Android.

A Figura abaixo mostra a opção para baixar a API de compatibilidade no Android SDK Manager.

EDITADO EM 10/08/2012
Na versão atual do plugin, essa lib é adicionada automaticamente ao projeto.

Uma vez baixado, o JAR vá até a pasta do SDK e procure pelo arquivo android-support-v4.jar. Ele pode estar na pasta extras/android/compatibility/v4 ou android-compatibility/v4 (dependendo do S.O.).
Crie um novo projeto Android selecionando a versão 1.6 ou superior, e na raiz do projeto, crie uma pasta lib. Copie o JAR para essa pasta e depois adicione-o ao Build-Path do projeto (botão direito sobre JAR, Build Path > Add to build path). Em seguida, clique com o botão direito sobre o projeto e clique em propriedades. Por fim, selecione Java Build Path e na aba Order and Export, marque o android-support-v4.jar.

Antes de começarmos a codificar, vou explicar como será o exemplo. Será uma aplicação clássica de Fragments, onde teremos uma lista, e ao clicar em um dos elementos dessa lista, os detalhes daquele item serão exibidos. Como exemplo, listarei as versões do Android e ao clicar, exibir os detalhes da respectiva versão.

Vou começar criando o layout principal da aplicação. Crie o arquivo res/layout/titles.xml e deixe-o conforme abaixo:
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="horizontal" >

  <fragment
    android:id="@+id/titles"
    android:layout_width="0px"
    android:layout_height="fill_parent" 
    android:layout_weight="1"
    class="ngvl.android.fragments2.TitlesFragment" />

  <FrameLayout
    android:id="@+id/details"
    android:layout_width="0px"
    android:layout_height="fill_parent"
    android:layout_weight="1" />
</LinearLayout>

Aqui temos um LinearLayout horizontal com um Fragment que está apontando para a classe TitlesFragment e está ocupando metade da tela (determinado pela propriedade layout_weight). E um FrameLayout para exibir os detalhes da opção selecionada.

O arquivo acima dará erro porque não criamos a classe TitlesFragment. Ela é mostrada abaixo:
public class TitlesFragment extends ListFragment {

  private final String EXTRA_INDEX = "index";
  private int selectedIndex;
 
  @Override
  public void onActivityCreated(
    Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
  
    ListView lv = getListView();
  
    setListAdapter(
      new ArrayAdapter<String>(
      getActivity(),
      android.R.layout.simple_list_item_checked,
      Dados.titles));
  
    lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
  
    if (savedInstanceState != null){
      selectedIndex = 
        savedInstanceState.getInt(EXTRA_INDEX);
      lv.setSelection(selectedIndex);
    }

    if (isDualPane()){
      showDetails(selectedIndex);
    } else {
      getActivity().findViewById(
        R.id.details).setVisibility(View.GONE);
    }
  
    lv.setItemChecked(selectedIndex, true);  
  }

  private void showDetails(int index) {
    selectedIndex = index;
  
    if (isDualPane()) {  
      DetailsFragment details = 
        (DetailsFragment)getFragmentManager()
          .findFragmentById(R.id.details);
   
      if (details == null || 
        details.getShowIndex() != index){

        details = DetailsFragment.newInstance(index);
    
        FragmentTransaction fragTrans =
          getFragmentManager().beginTransaction();
        fragTrans.replace(R.id.details, details);
        fragTrans.setTransition(
          FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        fragTrans.commit();
      }
    } else {
      Intent it = new Intent(getActivity(), 
        DetailsActivity.class);

      it.putExtra(EXTRA_INDEX, index);
      startActivity(it);
    }
  }
 
  @Override
  public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt(EXTRA_INDEX, selectedIndex);
  }
 
  @Override
  public void onListItemClick(ListView l, 
    View v, int position, long id) {
    super.onListItemClick(l, v, position, id);
    showDetails(position);
  }
 
  private boolean isDualPane(){
    Configuration config =
      getActivity().getResources().getConfiguration();

    return config.orientation == 
      Configuration.ORIENTATION_LANDSCAPE;
  }
}

Nosso fragmento herda de ListFragment, que é bem similar a classe ListActivity, e exibe informações em formato de lista. Então, no método onActivityCreated (o nome por sí só já diz o que ele faz :) obtemos a referência para a ListView e setamos um Adapter para ela. Os dados que são passados para esse Adapter vêm de um array de Strings contendo as versões do Android definidos na classe Dados. Parte dessa classe é mostrada abaixo:
public class Dados {
  static public String[] titles = {
    "Android 1.5",
    "Android 1.6",
    // Mais itens aqui
  }

  static public String[] description = {
    "Primeira versão do Android, Cupcake",
    "Donut já suporta Fragments",
    // Mais descrições aqui
  }
}

Em seguida, verificamos se existe um estado salvo da nossa Activity. Quando entramos a primeira vez, esse estado estará vazio, mas quando girarmos o aparelho, o método onSaveInstance será chamado e nós salvaremos a posição da lista que está checada. Se já existir um estado saldo, marcamos a opção que estava selecionada antes de girar o aparelho e fazemos o scroll até ela.
Logo após, verificamos se a aplicação é dualPane (dois painéis) esse método (que está no fim da classe) verifica se a orientação do aparelho está como landscape. Em caso positivo exibiremos a lista e os detalhes no mesmo momento, caso contrário exibiremos apenas a lista, então ocultamos o FrameLayout de detalhes.
O método showDetails verifica se a tela é dualPane, em caso positivo, instancia o fragmento de detalhes e o exibe utilizando a class FragmentTransition. Caso não seja dualPane, ou seja, está em Portrait, exibimos a Activity de detalhes.

Com a utilização de Fragments, a Activity passa a controlar o Fragment, e o Fragment trata eventos de UI. Com isso a Activity fica bem simples como podemos ver abaixo:
public class TitleActivity extends FragmentActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.titles);
  }
}

O único detalhe é que essa clase herda de FragmentActivity. Lembrando que ela é uma classe da API de compatibilidade, no Android 3.x podemos continuar usando Activity mesmo. Agora vamos analisar o fragmento de detalhes
public class DetailsFragment extends Fragment {

  private static final String EXTRA_INDEX = "index";
 
  public static DetailsFragment newInstance(int index) {
    DetailsFragment fragment = new DetailsFragment();
  
    Bundle params = new Bundle();
    params.putInt(EXTRA_INDEX, index);
    fragment.setArguments(params);
  
    return fragment;
  }
 
  public int getShowIndex() {
    return getArguments().getInt(EXTRA_INDEX);
  }

  @Override
  public View onCreateView(LayoutInflater inflater,
    ViewGroup container, Bundle savedInstanceState) {
  
    TextView txt = new TextView(getActivity());
    txt.setText(Dados.description[getShowIndex()]);
  
    return txt;
  }
}

A classe tem um Factory Method (chamado newInstance) que cria um DetailsFragment já passando os "extras" necessários. O método onCreateView define o que será exibido no Fragment, para simplificar, criamos apenas um TextView, mas você poderia usar o parâmetro inflater para carregar um arquivo de Layout que desejasse.
Por fim, vamos mostrar a Activity de detalhes.
public class DetailsActivity extends FragmentActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  
    if (isDualPane()){
      finish();
      return;
   
    } else if (savedInstanceState == null) {
  
      DetailsFragment details = new DetailsFragment();
      details.setArguments(getIntent().getExtras());
   
      getSupportFragmentManager()
        .beginTransaction()
        .add(android.R.id.content, details)
        .commit();
    }
  }
 
  private boolean isDualPane(){
    Configuration config =
      getResources().getConfiguration();

    return config.orientation ==
      Configuration.ORIENTATION_LANDSCAPE;
  }
}

Nessa Activity, basicamente verificamos se a orientação é landscape, em caso positivo, essa Activity é fechada, e consequentemente a TitlesActivity é exibida mostrando os títulos e os detalhes do mesmo. Já se Activity não é dual pane (ou seja, é portrait) e foi passado o índice para ela, carregamos o Fragment e o adicionamos à tela.

A imagem abaixo mostra nossa aplicação rodando em landscape.

Esse artigo foi baseado nesse post do Blog do Android Developers.

Qualquer dúvida, deixem seus comentários,

4br4ç05,
nglauber

sábado, 14 de janeiro de 2012

Android + iOS = Adobe Air

Olá povo,

Atualmente a disputa entre as plataformas para dispositivos móveis está bastante acirrada. Android e iOS travam uma verdadeira batalha pela preferência dos usuários, tanto no mercado de tablets quanto de smartphones. E nós, como desenvolvedores, temos de estar atentos a essa peleja.
Imagine que você recebe uma demanda de um projeto em que um dos requisitos, é rodar em iOS e Android. Nesse caso, o primeiro pensamento normalmente é: "putz! vou ter que fazer duas aplicações?".
Quem me conhece, sabe que meu projeto de mestrado foi o desenvolvimento de uma ferramenta que traduz aplicações iOS para Android. Mas infelizmente ela ainda está longe de ficar pronta :( Mesmo assim, durante as pesquisas do mestrado, analisei algumas ferramentas "concorrentes" da minha como o PhoneGap e Titanium. Entretanto, dentre todas, a que gostei mais foi o Adobe Air. Entre os pontos positivos da tecnologia estão: a produtividade, utilizando uma ferramenta RAD com diversos recursos; a linguagem de programação é dinâmica e bastante fácil de aprender; e o aspecto visual que se mantém igual no Android e no iOS.

A aplicação é desenvolvida em Flex e/ou ActionScrpit com o Flash Builder (ou Flash Professional CS5). Nesse post, vou mostrar como iniciar o desenvolvimento e fazer o bom e velho "Hello World". Para começar, você precisa fazer o download do Flash Builder 4.6 no site da Adobe. Ele é uma IDE baseada em Eclipse, onde você pode baixar um trial de 60 dias, e após esse tempo deve ser adquirido. Faça o download e instale o Flash Builder Trial no seu computador e vamos começar a brincadeira :)

Acesse o menu File > New > Flex Mobile Project. Na janela exibida abaixo, digite o nome do projeto e o local onde o projeto será salvo e clique em Next.

Na próxima janela do assistente, selecionamos para quais plataformas iremos desenvolver: iOS, Android e/ou BlackBerry. Deixei aqui Android e iOS. Na aba "Application Template" selecionamos o formato de aplicação que devemos desenvolver, aqui deixei uma aplicação Blank. As outras opções permitem criar aplicações que naveguem entre telas ou com abas respectivamente.
Na aba "Permission", selecionamos as permissões necessárias para acessar recursos de aplicações Android (para iOS ela não é usada). E em "Platform Settings", selecionamos para quais aparelhos iOS iremos desenvolver (iPad, iPod, iPhone).
Na seção "Application settings", marcamos se a aplicação deve mudar de orientação (portrait e landscape) automaticamente; se será tela cheia; e se a aplicação será esticada para se ajustar a telas de diferentes densidades. Deixe as configurações como abaixo e clique em Next.

Na próxima tela poderíamos selecionar se estamos utilizando alguma tecnologia servidor. Mas como não é o caso, clique em Next.
Na última tela do assistente, vamos deixar as opções padrão, exceto pelo campo Application ID, onde preencheremos com o esquema similar ao que usamos em Java. ex:"ngvl.air.exemploblog". Outro campo interessante dessa tela, é o "Main Application File" onde definimos o arquivo que será o ponto de partida da nossa aplicação. Aqui, deixamos HelloWorldBlog.mxml e clicamos em Finish.

Ao iniciar o projeto, o arquivo HelloWorldBlog.mxml é aberto em modo texto, mas vamos primeiro edita-lo no modo design. Para tal, clique no botão na parte superior do editor de código, e no combo "Device" selecione um aparelho de sua preferência.

Na paleta de componentes, localizada do lado esquerdo, arraste para a tela de design: um Label, um TextInput e um Button. De um duplo-clique nos componentes Label e Button para alterar o texto. E deixe conforme abaixo:

Agora vá para o modo Source e deixe conforme abaixo;
<?xml version="1.0" encoding="utf-8"?>
<s:Application
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
applicationDPI="160">

<fx:Script>
<![CDATA[
protected function botaoFilhaoClick(
event:MouseEvent):void
{
lblTexto.text = edtTexto.text;
}
]]>
</fx:Script>
<fx:Declarations>
</fx:Declarations>
<s:Label
id="lblTexto"
x="39" y="48"
text="Olá mundo Adobe Air"/>
<s:TextInput
id="edtTexto"
x="39" y="71"/>
<s:Button
x="39" y="112"
label="Vai filhão!"
click="botaoFilhaoClick(event)"/>
</s:Application>

Em seguida, clique com o botão direito sobre o projeto e selecione Run As... > Mobile Application. Aparecerá a janela abaixo. Selecione a plataforma que deseja testar (aqui usei Android) e em Launch Method você pode escolher entre rodar em um simulador ou em um dispositivo real. Aqui, selecionei para rodar no Desktop, e então o emulador com o tamanho de tela do Motorola Defy +. Clique em Run.

A figura abaixo mostra nossa aplicação em execução no simulador do Adobe Air. Ao clicar no botão, o texto digitado na caixa de texto é exibido no Label.


Bem povo, esse é apenas um pontapé inicial. Espero colocar mais posts sobre Adobe Air aqui no blog. Aproveito para agradecer ao meu grande amigo Eric Cavalcanti(@ericoc), com quem tive aulas de Adobe Air e me ajudou a iniciar nessa tecnologia.

Dúvidas? Deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 2 de janeiro de 2012

Widgets + Service

Olá povo,

Eu já coloquei aqui no blog dois posts sobre widgets (veja aqui e aqui), mas para quem não os viu, um AppWidget é uma aplicação que roda na HomeScreen dos aparelhos Android. E um dos grandes problemas ao se desenvolver esse tipo de aplicação é que o tratamento dos eventos que são disparados por ele. Eles podem ser feitos com uma subclasse de AppWidgetProvider (que herda de BroadcastReceiver), entretanto eles não mantêm estado. Por ser um BroadcastReceiver, o seu programa só interage com ele durante a execução do método onReceive.

Alguns colegas de trabalho (que não vou citar nomes :) usam classes com atributos estáticos para manter os estado dos AppWidgets. Mas digamos que essa não é a maneira mais elegante :) Por isso, hoje vou mostrar como integrá-los com o componente Service do Android.

O componente Service permite a sua execução em segundo plano mesmo que não haja nenhuma tela da aplicação aberta. Seu ciclo de vida inicia quando é disparada uma Intent para a classe do serviço utilizando o método startService. Caso o serviço ainda não esteja sendo executado, o mesmo será iniciado e ficará ativo até que o método stopService seja chamado ou que o próprio serviço chame o método stopSelf.

Tendo em vista essa característica do serviço, vou utilizá-lo nesse exemplo para guardar o estado do widget, que ficará fazendo chamadas ao serviço para obter informações e se atualizar. Nosso exemplo será um widget que mostrará alguns links favoritos que nós poderemos passar clicando nos botões de navegação.

Vamos começar pelo código do Service, que está na classe MeuServico

public class MeuServico extends Service {

// Sites favoritos
private String[] sites = {
"nglauber.blogspot.com",
"developer.android.com",
"www.unibratec.edu.br",
"www.especializa.com.br",
"www.cesar.edu.br" };

// índice do site que deve ser mostrado no widget
private int indice;

@Override
public int onStartCommand(Intent intent, int flags,
int startId) {
// O Widget vai chamar esse método cada vez que
// clicarmos no botão
// Obtendo o botão clicado (próx. ou ant.)
String acao = intent.getStringExtra(
MeuWidget.EXTRA_ACAO);

// Se foi próximo, incrementa o índice
if (MeuWidget.ACAO_PROXIMO.equals(acao)) {
indice++;

if (indice > sites.length - 1) {
indice = 0;
}

// Se foi anterior, decrementa o índice
}else if(MeuWidget.ACAO_ANTERIOR.equals(acao)){
indice--;
if (indice < 0) {
indice = sites.length - 1;
}
}
// Se veio alguma ação, atualize o widget
if (acao != null) {
RemoteViews views = new RemoteViews(
this.getPackageName(), R.layout.main);

// Atualizando TextView pra exibir o site
views.setTextViewText(
R.id.txtSite, sites[indice]);

// ID do widget que deve ser atualizado
int appWidgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);

// Obtendo a instância do AppWidgetManager
// para atualizar o Widget
AppWidgetManager appWidgetManager =
AppWidgetManager.getInstance(this);

// atualiza o widget
appWidgetManager.updateAppWidget(
appWidgetId, views);
}

return super.onStartCommand(
intent, flags, startId);
}

@Override
public IBinder onBind(Intent arg0) {
// Não usado aqui
return null;
}
}

O código acima, cria um serviço que tem uma lista de links favoritos, e um índice que controlará qual link deve ser exibido. O widget então fará requisições ao serviço, que serão tratadas pelo método onStartCommand, que mostrará um link da lista baseado na posição do array.
É importante ter em mente que o widget não roda no processo da nossa aplicação, e sim da HomeScreen, então para ter acesso aos componentes gráficos usamos a classe RemoteViews e atualizamos o seu conteúdo com a classe AppWidgetManager.

Uma vez que o Service está pronto, vamos começar a criar o widget. Assim como uma Activity, ele terá um arquivo de layout. Utilizei o próprio res/layout/main.xml que é criado com o projeto. Ele ficou conforme abaixo:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal"
android:background="#000000"
android:layout_margin="20dp">
<Button
android:id="@+id/btnAnterior"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:text="<" />
<TextView
android:id="@+id/txtSite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:layout_weight="1"
android:text="Clique para navegar nos favoritos"/>
<Button
android:id="@+id/btnProximo"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:text=">" />
</LinearLayout>

Nada de especial no Layout acima, então vamos ver como fica o código do Widget.

public class MeuWidget extends AppWidgetProvider {
public static final String EXTRA_ACAO = "acao";
public static final String ACAO_ANTERIOR="anterior";
public static final String ACAO_PROXIMO = "proximo";

@Override
public void onUpdate(Context context,
AppWidgetManager appWidgetManager,
int[] appWidgetIds) {

super.onUpdate(context,
appWidgetManager, appWidgetIds);

// As view estão na aplicação Home e não na
// nossa. Por isso, para obter as referências
// dos componentes é usada a classe RemoteViews
RemoteViews views = new RemoteViews(
context.getPackageName(), R.layout.main);

// Esse método recebe a lista dos widgets que
// devem ser atualizados, pois podem haver várias
// instâncias do mesmo na Home.
// Então devemos atualizar todos.
for (int i = 0; i < appWidgetIds.length; i++){
// Configurando evento dos botões
setButtonClicks(context, appWidgetIds[i], views);
}
appWidgetManager.updateAppWidget(
appWidgetIds, views);
}

private void setButtonClicks(
Context context, int appWidgetId,
RemoteViews views) {
// Os eventos de click só podem disparar
// PendingIntents. No nosso caso,
// dispararemos PendingIntents para o nosso
// serviço. Para setar o evento, passa o id
// do componentes e a PendingIntent
views.setOnClickPendingIntent(
R.id.btnProximo,
servicePendingIntent(
context, ACAO_PROXIMO, appWidgetId));

views.setOnClickPendingIntent(
R.id.btnAnterior,
servicePendingIntent(
context, ACAO_ANTERIOR, appWidgetId));
}

private PendingIntent servicePendingIntent(
Context ctx, String acao, int appWidgetId){

// Criando a Intent para chamar o serviço
Intent serviceIntent = new Intent(
ctx, MeuServico.class);

// Passando a ação (proximo ou anterior)
serviceIntent.putExtra(EXTRA_ACAO, acao);

// Passando o ID do widget que clicou
serviceIntent.putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetId);

// O requestId da PendingIntent deve ser único,
// então gero um número aleatório
int requestId = new Random().nextInt();

// criando a PendingIntent para o serviço
PendingIntent pit = PendingIntent.getService(
ctx, requestId, serviceIntent, 0);
return pit;
}
}

No código acima, o método onUpdate é chamado quando o widget é adicionado à tela. Note que a referência aos componentes é feita através da classe RemoteViews, este é outro conceito que não deve ser esquecido: o widget roda na aplicação Home, logo, essas Views lá. Outro detalhe interessante é que os componentes só podem disparar PendingIntents ao serem clicados. E no nosso caso, essas PendingIntents estão chamando o Service, mudando apenas os parâmetros que são passados (próximo e anterior).

Agora vamos criar o arquivo de configuração do widget. Na pasta res, crie a pasta xml (minúsculo) e adicione o arquivo meuwidget.xml e coloque o conteúdo abaixo:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/main"
android:minHeight="72dp"
android:minWidth="294dp" >
</appwidget-provider>

Assim como todo BroadcastReceiver ele deve ser declarado no AndroidManifest.xml, só que ele deve tratar a ação APPWIDGET_UPDATE e deve ter uma tag meta-data referenciando o arquivo de configuração que criamos anteriormente.

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

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

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

<receiver
android:name="MeuWidget"
android:label="Meu Widget">
<!-- IntentFilter obrigatória
para todo widget -->
<intent-filter>
<action
android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<!-- Arquivo de configuração do widget -->
<meta-data android:resource="@xml/meuwidget"
android:name="android.appwidget.provider"/>
</receiver>

<!-- Meu Serviço -->
<service android:name="MeuServico"/>
</application>
</manifest>

Mande rodar a aplicação. Nada irá aparecer, pois você não tem nenhuma Activity no projeto. Para ver se o Widget está funcionando, dê um clique-longo na Home ou clique na tecla "Menu" e em seguida, selecione "Add". No pop-up que for exibido, selecione "Widgets", e depois "Meu Widget". A mensagem inicial do widget será exibida, depois é clicar nos botões para ver o resultado.

Como vocês observaram dexei o código comentado, mas quem tiver dúvidas, deixem seus comentários.

4br4ç0s,
nglauber

sexta-feira, 23 de dezembro de 2011

Agradecimento

Olá povo,

Esse será o último post de 2011. Um ano fantástico para mim, Graças a Deus! Consegui atingir vários objetivos, e por isso resolvi agradecer a todos que acreditaram em mim e no meu trabalho.

Primeiramente ao CESAR, onde trabalho desde 2006, e me dá a oportunidade de aprender muito todos os dias. Mexer com diversas tecnologias e em um ambiente diferenciado, são coisas que me faz gostar e querer continuar nesse time fantástico.

Um agradecimento Especial a Especializa :) onde dei os primeiros paços como instrutor. Foi uma grande escola, e continua sendo uma satisfação ministrar cursos em uma instituição que tem como meta, formar especialistas com cursos de qualidade.


A LinuxFi me permitiu conhecer a bela capital da Paraíba. E tive a satisfação de ministrar o primeiro curso de Android de João Pessoa. Depois tive a oportunidade de voltar, e espero voltar mais vezes :)


Após terminar o mestrado no CESAR.edu, tive a felicidade de iniciar minha carreira como professor universitário na própria instituição. Dando aulas de Java ME e Android na pós-graduação em Tecnologias para Desenvolvimento de Aplicações Móveis, aprendi muito com meus alunos, e isso gerou muitos posts aqui do blog :)


Esse é o meu mais novo desafio. Esse ano eu comecei a ministrar a disciplina de Programação Móvel no curso de Análise e Desenvolvimento de Software (ADS) na Unibratec. Totalmente diferente de cursos isolados e de pós-graduação, está sendo bem gratificante e desafiador.

Venho escrevendo matérias para as revistas Java Magazine e Mobile Magazine da DevMedia. Essa parceria tem me ajudado a escrever melhor (inclusive aqui pro blog) e falar melhor também durante as aulas. Além é claro de me forçar a estudar para enriquecer as aulas.

Para finalizar, agradeço a todos vocês. Colegas de trabalho, Alunos, Leitores do Blog e/ou dos meus artigos. Espero contar com todos vocês em 2012.

8045 F35745!

4br4ç05,
nglauber

segunda-feira, 12 de dezembro de 2011

Google Maps: Traçando Rotas

[EDITADO EM 03/04/2013: atualizado para Google Api v2]

Olá povo,

A API do Google Maps disponível para Android é muito bacana, e incorporar esse recurso na sua aplicação pode ser um bom atrativo. Em um dos projetos que trabalhei, precisava mostrar a rota entre dois pontos no mapa, e a Google disponibilizou em 27/07/12 uma API Web que retorna a rota entre duas coordenadas geográficas no formato JSON (ou XML se preferir).

Nesse arquivo, temos uma lista com as coordenadas geográficas que devem ser percorridas desde o ponto de partida até o destino. Com essas coordenadas lidas do JSON, criamos uma lista de LatLng que devem ser desenhadas no objeto GoogleMap através de uma PolyLine.

Não vou detalhar muito a configuração do Google Maps v2 no Android pois já falei nesse post aqui. Também não vou entrar em detalhes sobre leitura de JSON pois falei nesse post aqui. E se você tiver dúvidas sobre AsyncTask, dê uma olhada nesse post.

Vou começar pelo código da Activity mostrado abaixo. No onCreate pegamos a instância de GoogleMap e definimos a posição inicial no mapa e um zoom padrão. Em seguida iniciamos a AsyncTask passando a GoogleMap no construtor e informando a latitude e longitude de origem e destino.
public class TesteRotaMapaActivity 
  extends FragmentActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  
    SupportMapFragment fragment = 
      (SupportMapFragment)
        getSupportFragmentManager()
          .findFragmentById(R.id.map);

    GoogleMap map = fragment.getMap();

    LatLng latLng = new LatLng(-8.102739,-34.89917);
  
    map.animateCamera(
      CameraUpdateFactory.newLatLngZoom(latLng, 12.0f));
  
    new RotaAsyncTask(this, map).execute(
      // Latitude, Logintude de Origem
      latLng.latitude, latLng.longitude,    
      // Latitude, Longitude de Destino
      -23.604262,-46.676513);  
    }
}

A classe RotaAsyncTask é quem faz o trabalho de acessar o site do Google Maps, baixar o JSON com a informação da rota.
public class RotaAsyncTask extends 
  AsyncTask<Double, Void, Void>{

  private ProgressDialog dialog;
  private GoogleMap mapView;
  private Context context;
  private Route rota;

  public RotaAsyncTask(Context ctx, GoogleMap mapa) {
    mapView = mapa;
    context = ctx;
  }
 
  @Override
  protected void onPreExecute() {
    super.onPreExecute();
    dialog = ProgressDialog.show(
      mapView.getContext(), "Aguarde", 
      "Calculando rota");
  }
 
  @Override
  protected Void doInBackground(Double... params) {

    rota = directions(
      new LatLng(params[0], params[1]), 
      new LatLng(params[2], params[3]));
    return null;
  }
 
  @Override
  protected void onPostExecute(Void result) {
    super.onPostExecute(result);
    PolylineOptions options = new PolylineOptions()
      .width(5)
      .color(Color.RED)
      .visible(true);

    for (LatLng latlng : rota.getPoints()) {
      options.add(latlng);
    }

    mapView.addPolyline(options);
    dialog.dismiss();
  }
 
  private Route directions(
    final LatLng start, final LatLng dest) {
  
    // Formatando a URL com a latitude e longitude  
    // de origem e destino.  
    String urlRota = String.format(Locale.US,
      "http://maps.googleapis.com/maps/api/"+
      "directions/json?origin=%f,%f&"+
      "destination=%f,%f&" +
      "sensor=true&mode=driving",   
      start.latitude, 
      start.longitude, 
      dest.latitude, 
      dest.longitude); 
  
    GoogleParser parser;
    parser = new GoogleParser(urlRota);
    return parser.parse();
  }
}

Quem viu o post anterior pode notar que houveram algumas modificações. Delegamos o parse do JSON para a classe GoogleParser e exibimos as rotas utilizando a classe PoluLineOptions (da API v2) com ajuda da classe Route. Essas novas classes foram extraídas desse post aqui. Graças ao comentário do Hadson Gomes.
Eu peguei essas classes e deixei apenas o necessário. Vejamos elas abaixo.
public class Route {
  private final List<LatLng> points;
  private String polyline;

  public Route() {
    points = new ArrayList<LatLng>();
  }

  public void addPoints(final List<LatLng> points) {
    this.points.addAll(points);
  }

  public List<LatLng> getPoints() {
    return points;
  }

  public void setPolyline(String polyline) {
    this.polyline = polyline;
  }

  public String getPolyline() {
    return polyline;
  }
}
Note que a classe acima tem uma propriedade chamada polyline. Nesse JSON, a rota retorna algumas coordenadas "macro" digamos assim. Por isso os comentários abaixo informando que eram traçadas linhas retas no caminho. A rota com as coordenadas mas precisas estão codificadas em base64 no atributo polyline. A classe GoogleParser faz essa conversão de base64 para GeoPoint conforme abaixo.
public class GoogleParser {

  protected URL feedUrl;

  public GoogleParser(String feedUrl) {
    try {
      this.feedUrl = new URL(feedUrl);
    } catch (MalformedURLException e) {
    }
  }
 
  public Route parse() {
    // Cria uma rota vazia
    final Route route = new Route();
    try {
      // Obtém a String do JSON
      final String result = 
        convertStreamToString(
          feedUrl.openConnection()
            .getInputStream());

      // Transforma a string em JSON
      JSONObject json = new JSONObject(result);
      // Pega a primeira rota retornada
      JSONObject jsonRoute = 
        json.getJSONArray("routes")
          .getJSONObject(0);
      JSONObject leg = jsonRoute
        .getJSONArray("legs").getJSONObject(0);

      // Obtém os passos do caminho
      JSONArray steps = leg.getJSONArray("steps");

      final int numSteps = steps.length();
      /*
       * Itera através dos passos, decodificando 
       * a polyline e adicionando à rota.
       */
      JSONObject step;
      for (int i = 0; i < numSteps; i++) {
        // Obtém o passo corrente
        step = steps.getJSONObject(i);
        // Decodifica a polyline e adiciona à rota
        route.addPoints(
          decodePolyLine(
            step.getJSONObject("polyline")
              .getString("points")));
      }
    } catch (Exception e) {
    }
    return route;
  }

  private String convertStreamToString(
    final InputStream input) {

    final BufferedReader reader = 
      new BufferedReader(
        new InputStreamReader(input));
    final StringBuilder sBuf = new StringBuilder();

    String line = null;
    try {
      while ((line = reader.readLine()) != null) {
        sBuf.append(line);
      }
    } catch (IOException e) {
    } finally {
      try {
        input.close();
      } catch (IOException e) {
      }
    }
    return sBuf.toString();
  }

  private List<LatLng> decodePolyLine(final String poly) {
    int len = poly.length();
    int index = 0;
    List<LatLng> decoded = 
      new ArrayList<LatLng>();

    int lat = 0;
    int lng = 0;

    while (index < len) {
      int b;
      int shift = 0;
      int result = 0;
      do {
        b = poly.charAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);
      int dlat = 
        ((result & 1) != 0 ? 
          ~(result >> 1) : 
          (result >> 1));
      lat += dlat;

      shift = 0;
      result = 0;
      do {
        b = poly.charAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);

      int dlng = ((result & 1) != 0 ? 
        ~(result >> 1) : 
        (result >> 1));
      lng += dlng;

      decoded.add(
        new LatLng(
          (float)(lat / 1E5),
          (float)(lng / 1E5)));
    }
    return decoded;
  }
}
Quem viu o post anterior deve lembrar que a classe RouteOverlay não existe mais. Quem faz seu papel é a classe PolyLine da nova versão da Google Maps Api.
A imagem abaixo mostra nossa aplicação em execução.



Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 5 de dezembro de 2011

Resoluções de Tela

Olá povo,

Frequentemente nas aulas de Android o pessoal tem dificuldade de lembrar as diversas resoluções de tela disponíveis para os smartphones e tablets atuais. Hoje meu colega de trabalho André mandou uma imagem com os tamanhos e siglas que as representam.

No Android, a grande maioria dos dispositivos utilizam as resoluções: QVGA, HVGA, WVGA e WXGA.

4br4ç05,
nglauber

Fonte: http://samuelvarela.wordpress.com/2009/03/20/resolucao-de-tela-vga-qvga-etc/

segunda-feira, 28 de novembro de 2011

Lendo RSS no Android

Olá povo,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

4br4ç05,
nglauber