segunda-feira, 16 de abril de 2012

Animações no Android 3+

Olá povo,

Essa semana, meu colega Felipe Vasconcelos me lembrou de um problema existente na API de animações do Android 2 (que falei nesse post).
Digamos que você tenha um botão em uma posição x/y, e que esse botão seja posicionado em outro lugar através de uma animação (uma TranslateAnimation pra ser mais exato). Se você clicar nesse botão após a animação, você não conseguirá. Isso se deve a uma característica (ou bug?) da API de animação do Android até o 2.x, que só altera o aspecto da View, mas não seu conteúdo interno. Isso quer dizer que, apesar do botão mudar a posição visualmente, ele continua na posição antiga. Ou seja, se você clicar no botão na posição anterior à animação, o evento de clique ocorrerá.

Para corrigir esse problema, o Google introduziu uma nova API de animação no Android 3.0, que traz ainda o benefício de acompanhar cada passo da animação, coisa que não acontecia na antiga API.
Para entender melhor o problema e a nova API, vamos criar um exemplo bem simples. Abaixo temos o arquivo XML que define a tela da nossa aplicação:
<?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">

  <Button
    android:id="@+id/btnAnimacaoAntiga"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onClickAnimarAntigo"
    android:text="Animar 2.x" />

  <Button
    android:id="@+id/btnAnimarNova"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onClickAnimarNovo"
    android:text="Animar Honeycomb" />

</LinearLayout>
A tela é bem simples, apenas dois botões que serão animados até a posição 200 na coordenada Y. O primeiro animará utilizando a API antiga, e o segundo a nova. Abaixo, temos a Activity da aplicação:
public class AnimationHoneycombActivity 
  extends Activity {
 
  private boolean b1Desceu;
  private boolean b2Desceu;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  }
    
  public void onClickAnimarAntigo(View v){
    int yInicial, yFinal;
    if (b1Desceu){
      yInicial = 200;
      yFinal = 0;
    } else {
      yInicial = 0;
      yFinal = 200;
    }
     
    TranslateAnimation animacao = 
      new TranslateAnimation(0, 0, yInicial, yFinal); 
    animacao.setDuration(2000);
    animacao.setFillAfter(true);
    v.startAnimation(animacao);
    b1Desceu = !b1Desceu;
  }
    
  public void onClickAnimarNovo(final View v){
      
    ValueAnimator animacao = 
      ValueAnimator.ofFloat(0, 200);

    animacao.setTarget(v);
    animacao.setDuration(2000);
    animacao.addUpdateListener(
      new ValueAnimator.AnimatorUpdateListener() {
   
        @Override
        public void onAnimationUpdate(
          ValueAnimator animation) {

          Float valor = (Float)
            animation.getAnimatedValue();
          v.setY(valor);
       }
     });

    if (b2Desceu){
      animacao.reverse();
    } else {
      animacao.start();
    }
    b2Desceu = !b2Desceu;
  }
}
No primeiro método, utilizamos a API antiga de animações. Utilizei uma flag para determinar se o botão desceu, para determinar a posição inicial e final na coordenada Y. Em seguida, criei uma TranslateAnimation, defini a duração, e em seguida, com o método setFillAfter determinei que ao terminar a animação, o botão deve permanecer onde a animação terminou. Por fim, mandei animar o botão.
Já no segundo método, estou usando a nova API de animações. A classe ValueAnimator é a chave dessa API, e ela tem basicamente: um valor inicial e final; um target, que é um objeto View; a duração em milissegundos; e um AnimatorUpdateListener. Esse último objeto é chamado N vezes para realizar a animação, onde N depende do valor final e sua respectiva duração. É nesse objeto que devemos alterar a propriedade da View que queremos animar, que no nosso caso, é a propriedade Y do botão.
No final do método vemos se o botão desceu, em caso positivo, solicitamos que a animação seja feita ao contrário com o método reverse; caso contrário, mandamos animar para a posição desejada.
Para observar o "bug" da antiga API de animação, clique no primeiro botão e ele descerá. Em seguida, clique no botão e você notará que nada acontecerá. Agora tente clicar onde o botão estava... Magicamente o botão voltará para a posição original.

Em resumo, se você precisar interagir com uma View que sofreu uma animação, ou ainda precisar obter informações internas sobre elas, você terá que contornar essa limitação da API antiga de animações. Mas se você estiver pensando em desenvolver para tablets Android ou ainda para os novos aparelhos com Android 4 ICS, utilize a nova API de animações.

4br4ç05,
nglauber

sábado, 14 de abril de 2012

iOS: Lendo XML

Olá povo,

Neste post vou mostrar como ler arquivos XML em aplicações iOS. O XML que estou lendo, eu baixei no site do livro Google Android do Ricardo Lecheta (eu tento, mas não deixo o Android :)
O modelo do estrutura do XML que iremos ler é apresentado abaixo:
<?xml version="1.0" encoding="utf-8"?>
<carros>
  <carro>
    <nome>Modelo do Carro</nome>
    <desc>Descrição do Carro</desc>
    <url_info>http://www.ferrari.com/</url_info>
    <url_foto>http://www.livroandroid.com.br/</url_foto>
  </carro>
  ...
</carros>
Crie um novo projeto no Xcode utilizando o template "Single View Application". Agora acesse o site do livro e baixe o XML, em seguida adicione-o ao seu projeto no Xcode na pasta "Supporting Files". A estrutura do projeto (no final) deve ficar conforme abaixo:

Como você deve ter observado, o carro descrito no XML tem os atributos nome, desc, url_info e url_foto. Vamos criar uma classe que representará esse objeto em código.
// Carro.h ---------------------------------------
#import <Foundation/Foundation.h>

@interface Carro : NSObject

@property (strong, nonatomic) UIImage *image;
@property (strong, nonatomic) NSString *nome;
@property (strong, nonatomic) NSString *descricao;
@property (strong, nonatomic) NSString *urlInfo;
@property (strong, nonatomic) NSString *urlFoto;

@end
// Carro.m ---------------------------------------
#import "Carro.h"

@implementation Carro

@synthesize nome, descricao, urlFoto, urlInfo, image;

@end
Vou alterar o AppDelegate da aplicação para ficar desta forma:
// NGAppDelegate.h -------------------------------
#import <UIKit/UIKit.h>

@class NGViewController;

@interface NGAppDelegate : 
  UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) 
  UIWindow *window;
@property (strong, nonatomic) 
  UINavigationController *navigationController;

@end
// NGAppDelegate.m -------------------------------
#import "NGAppDelegate.h"
#import "NGViewController.h"

@implementation NGAppDelegate

@synthesize window = _window;
@synthesize navigationController;

- (BOOL)application:
   (UIApplication *)application
     didFinishLaunchingWithOptions:
   (NSDictionary *)launchOptions{

  self.window = [[UIWindow alloc]
     initWithFrame:[[UIScreen mainScreen] bounds]];

  NGViewController *viewController =
     [[NGViewController alloc] initWithNibName:
       @"NGViewController" bundle:nil];

  navigationController =
     [[UINavigationController alloc]
       initWithRootViewController:viewController];

  self.window.rootViewController =
     self.navigationController;

  [self.window makeKeyAndVisible];
  return YES;
}
// Outros métodos não alterados
@end
Nesse template, a aplicação é criada com uma UIViewController simples. Vamos utilizar uma UITableViewController que listará os dados lidos através de um objeto da class NSXMLParser. Sua utilização é bem similar ao SAX do Java, e teremos que implementar um protocolo NSXMLParserDelegate, e a medida que as tags são lidas os métodos desse protocolo são chamados.
#import <UIKit/UIKit.h>
#import "Carro.h"

@interface NGViewController : UITableViewController
   <NSXMLParserDelegate>{

  NSMutableArray *carros;
  Carro *carro;
  NSString *currTag;
}

@end
Um NSMutableArray armazenará os objetos Carro criados a partir da leitura do XML. Durante o parser do XML, precisamos saber que tag está sendo lida, para tal setarmos o atributo currTag com tag que corresponde a propriedade da classe Carro. Por exemplo, quando o parser encontrar uma tag "carro", criamos um objeto Carro, já quando encontramos uma tag "nome", devemos definir o a propriedade nome do objeto carro. Por fim, ao encontrar a tag "/carro" devemos adicionar o objeto carro na lista.
Vamos agora a implementação da tela (lembrando que só estamos mostrando os métodos alterados):
#import "NGViewController.h"
#import "Carro.h"

@implementation NGViewController

- (void)viewDidLoad{
  [super viewDidLoad];
  self.title = @"Carros";
      carros = [[NSMutableArray alloc] init];
  // Pega o caminho do arquivo XML
  NSString *xmlPath = [[NSBundle mainBundle]
     pathForResource:@"carros" ofType:@"xml"];
  // Obtém os bytes do arquivo
  NSData *xmlData =
     [NSData dataWithContentsOfFile:xmlPath];
  // Cria o objeto que fará o parser do XML
  NSXMLParser *xmlParser =
     [[NSXMLParser alloc] initWithData:xmlData];
  [xmlParser setDelegate:self];
  [xmlParser parse];
}

// Métodos chamado quando NSXMLParser
// inicia a leitura de uma TAG
-(void)parser:(NSXMLParser *)parser
   didStartElement:(NSString *)elementName
   namespaceURI:(NSString *)namespaceURI
   qualifiedName:(NSString *)qName
   attributes:(NSDictionary *)attributeDict {

  currTag = elementName;      
  // Se achou a TAG carro, crie um novo objeto
  if ([currTag compare:@"carro"] == NSOrderedSame){
    carro = [[Carro alloc] init];  
  }
}

// Método chamado quando vai ler o 
// conteúdo da TAG
-(void)parser:(NSXMLParser *)parser
    foundCharacters:(NSString *)string {

  if ([currTag compare:@"nome"] ==
     NSOrderedSame && carro.nome == nil){
    carro.nome = string;
  } else if ([currTag compare:@"desc"] ==
     NSOrderedSame && carro.descricao == nil){
    carro.descricao = [string
       stringByTrimmingCharactersInSet:
        [NSCharacterSet
           whitespaceAndNewlineCharacterSet]];
  } else if ([currTag compare:@"url_info"] ==
     NSOrderedSame && carro.urlInfo == nil){
    carro.urlInfo = [string
       stringByTrimmingCharactersInSet:
        [NSCharacterSet
            whitespaceAndNewlineCharacterSet]];
  } else if ([currTag compare:@"url_foto"] ==
     NSOrderedSame && carro.urlFoto == nil){
    carro.urlFoto = [string
       stringByTrimmingCharactersInSet:
        [NSCharacterSet
           whitespaceAndNewlineCharacterSet]];
  }
}

// Método chamado quando termina de ler uma tag
-(void)parser:(NSXMLParser *)parser
   didEndElement:(NSString *)elementName
   namespaceURI:(NSString *)namespaceURI
   qualifiedName:(NSString *)qName {
  // Se a tag lida for "carro", adiciona à lista
  if ([elementName compare:@"carro"]==NSOrderedSame){
    [carros addObject:carro];
  }
}

// Método de UITableViewController
-(NSInteger)tableView:(UITableView *)tableView
   numberOfRowsInSection:(NSInteger)section {
  return [carros count];
}

-(NSInteger)numberOfSectionsInTableView:
  (UITableView *)tableView {
  return 1;
}

-(UITableViewCell *)tableView:(UITableView *)
  tableView cellForRowAtIndexPath:
   (NSIndexPath *)indexPath {
  static NSString *CellIdentifier = @"Cell";
  UITableViewCell *cell = [tableView
    dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil) {
    cell = [[UITableViewCell alloc]
       initWithStyle:UITableViewCellStyleSubtitle
       reuseIdentifier:CellIdentifier];
  }
  Carro *_carro = [carros objectAtIndex:indexPath.row];
  cell.textLabel.text = _carro.nome;
  cell.detailTextLabel.text = _carro.urlInfo;
  return cell;
}

-(void)tableView:(UITableView *)tableView
   didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
  // Obtém o carro da linha selecionada
  Carro *_carro = [carros objectAtIndex:indexPath.row];
  // Abre o site do carro selecionado  
  NSURL *url = [NSURL URLWithString:_carro.urlInfo];
  [[UIApplication sharedApplication] openURL:url];
}
@end
O código acima está comentado e parte relacionada com a UITableViwController foi detalhada nesse post aqui.

Feito isso, precisamos fazer alguns ajustes no NIB. Abra o NGViewController.xib e remova a UIView e adicione uma UITableViewController. Ligue esse componente ao Outlet da propriedade view, e o dataSource e delegate.
Além disso, alterei também a propriedade "Top Bar" para "Navigation Bar". Feito isso, podemos rodar nossa aplicação. O resultado é apresentado abaixo.
Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

domingo, 8 de abril de 2012

Artigo "Android 4: Ice Cream Sandwich" (parte 2)

Olá povo,

A revista Java Magazine, traz em sua edição de número 102, a segunda (e última) parte da matéria sobre a nova versão da plataforma Android. O texto foi escrito por mim e pelo meu colega Bruno Vinícius.

Nesta segunda parte do artigo, apresentamos mais recursos da nova versão do Android. Abordamos a integração com serviços de agendamento de tarefas, novos componentes de interface gráfica e a vasta gama de possibilidades multimídia e de comunicação trazidas por essa nova versão da plataforma.

Espero que vocês gostem.

4br4ç05,
nglauber

terça-feira, 3 de abril de 2012

iOS: Internacionalização

Olá povo,

Aqui vai o primeiro vídeo-post aqui do blog: Internacionalização no iOS.


Espero que vocês gostem. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber