quinta-feira, 18 de outubro de 2012

Customizando Views

Olá pessoal,

Este é o primeiro de 3 artigos que irei escrever sobre como trabalhar com as views que a API do Android nos proporciona. Se você leu os artigos anteriores irá saber que as views são os componentes que fazem a interface de interação com o usuário, são como os tão conhecidos "controles" presentes em linguagens como Visual Basic e C#.

Hoje vou escrever sobre como customizar uma View já existente, já os próximos dois posts serão sobre como criar views compostas e como criar novas views.

Não é minha intenção descrever todas as views da API do Android mas como é importante que você saiba que elas existem, e são muitas, segue o link oficial com a definição de todas as Views.

http://developer.android.com/reference/android/view/View.html

No post de hoje vamos ver como funciona a customização de uma View. Vamos neste exemplo vamos criar uma View que será uma caixa de texto que muda de cor ao receber o foco e retorna ao seu estado original quando o perde. É um exemplo simples, mas que nos trará entendimento sobre conceitos interessantes e sobre este processo.

O processo de customizar uma View é feito através da estensão de uma classe para o tipo desejado. No nosso exemplo estamos customizando uma View do tipo EditText, então temos que criar uma classe que estenda-a, isso faz com que possamos ter uma View com todas as funcionalidades da View que queremos customizar.

public class FocusableText extends EditText

Depois de ter a classe estendida, temos que configurar os seus construtores e o seu método onDraw. Repare no código abaixo que eu criei também uma função chamada Init, que será usada para inicializar variáveis de memória logo após o construtor ser chamado.


public FocusableText(Context context, AttributeSet ats, int ds) {
super(context, ats, ds);
init();
}

public FocusableText(Context context) {
super(context);
init();
}

public FocusableText(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

private void init() {
this.setFocusable(true);
this.setFocusableInTouchMode(true);



myResources = getResources();
borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

marginPaint = new Paint(Paint.ANTI_ALIAS_FLAG);


margin = myResources.getDimension(R.dimen.marginFocusableText);
this.setBackgroundColor( myResources.getColor(R.color.defaultTextBackcolor));

}




É importante saber que todos os métodos que estendemos chamam a rotina principal da classe estendida.

Agora é o momento onde a customização visual da View acontece. No método onDraw é onde definimos o layout da nossa View.


@Override
public void onDraw(Canvas canvas) {

borderPaint.setColor(myResources.getColor(R.color.focusableTextBorderColor));

marginPaint.setColor( myResources.getColor(R.color.focusableTextBorderColor));


canvas.drawLine(0, 0, getMeasuredWidth(), 0, borderPaint);
canvas.drawLine(0, 0, 0, getMeasuredHeight(), borderPaint);
canvas.drawLine(0, getMeasuredHeight(),getMeasuredWidth(), getMeasuredHeight(), borderPaint);
canvas.drawLine(getMeasuredWidth(),getMeasuredHeight(),getMeasuredWidth(), 0, borderPaint);
canvas.drawLine(0, 0, getMeasuredWidth(), 0, borderPaint);


canvas.save();
canvas.translate(margin, 0);

super.onDraw(canvas);
canvas.restore();
}
Este código a principio parece complicado, mas é de simples entendimento. Aqui estamos definindo a borda do controle, a margem, escrevendo as linhas que irão formar a borda através do canvas.drawLine, salvando a área do controle, chamado aqui de canvas e chamando o método onDraw da classe estendida. Neste momento a interface do controle é construida e aplicada.

Não é necessário entrar em maiores detalhes sobre cores ou medidas, pois tudo aqui está vindo dos arquivos de Resources.

O objetivo principal do nosso controle é feito pela simples ação de sobrecarregar o método onFocusChanged da classe estendida. 

@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {

if (focused)
this.setBackgroundColor(getResources().getColor(R.color.focusableTextBackcolor));
else
this.setBackgroundColor(getResources().getColor(R.color.defaultTextBackcolor));

// TODO Auto-generated method stub
super.onFocusChanged(focused, direction, previouslyFocusedRect);

}

Repare que sempre será verificado o foco e depois será chamada a função da classe estendida.

Quando você for utilizar a View basta abrir uma tag dentro do arquivo de layout começando pelo nome do package do seu aplicativo e finalizando com o nome da sua classe criada. Veja no exemplo abaixo:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" 
    android:background="@color/linearLayoutBackcolor"
    >

    <com.example.customizandoviews.FocusableText
        android:id="@+id/focusableText1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/hintFirstName"
        android:singleLine="true"
        android:layout_margin="@dimen/marginFocusableText" />

    <com.example.customizandoviews.FocusableText
        android:id="@+id/focusableText2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/hintLastName"
        android:singleLine="true"
        android:layout_margin="@dimen/marginFocusableText" />

    <com.example.customizandoviews.FocusableText
        android:id="@+id/focusableText3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/hintAge"
        android:inputType="number"
        android:layout_margin="@dimen/marginFocusableText" />



</LinearLayout>


Atente há alguns detalhes que citei no começo do artigo e são comprovados aqui. Quando estendi a classe herdei todas as funcionalidades da classe pai e estou fazendo uso delas, note a utilização do hint, singleline e até mesmo do inputType.

Depois de pronto temos esta visualização, o campo focado sempre assume outra cor.



Abraço e boa sorte!

sábado, 13 de outubro de 2012

Fragments

Olá pessoal,

Conforme prometido há algumas semanas atrás venho falar hoje de um recurso muito interessante chamado Fragments.

O recurso Fragments foi adicionado a API do Android a partir da versão 3.0 (Honeycomb) e também está disponível nas bibliotecas de suporte, o que permite ao desenvolvedor utilizar estes recursos a partir da versão 1.6 do Android.

Os Fragments permitem ao desenvolvedor encapsular código criando componentes reutilizáveis que possuem seu próprio ciclo de vida e sua própria interface de usuário. Este recurso se assemelha muito aos User Controls presentes nas linguagens de desenvolvimento .NET da Microsoft.

Uma grande vantagem desse recurso é que ele permite ao desenvolvedor criar aplicações que suportam múltiplos layouts, onde você pode definir o posicionamento dos objetos de acordo com o tamanho da tela do usuário. Imagine um aplicativo que irá exibir uma lista de imagens e quando o usuário selecionar uma delas esta imagem será mostrada, simples não? Agora vamos pensar em otimizar este cenário, condicionando a tela de nosso aplicativo de acordo com a tela do dispositivo do usuário. Em um smartphone comum, onde a visualização é geralmente em retrato, nosso aplicativo irá mostrar somente a lista de imagens, e quando o usuário selecionar uma imagem, o nosso aplicativo irá limpar tela e mostrar a imagem, já em um tablet a tela pode ser melhor aproveitada, mostrando do lado esquerdo a lista de imagens e do lado direto a imagem selecionada pelo usuário. Esta situação pode ser resolvida utilizando os Fragments e combinando o layout. A aplicação disponível para download deste artigo faz exatamente isso. Veja abaixo a visualização do mesmo aplicativo em duas telas diferentes.

Normal

Tablet

Esta aplicação tem dois tipos de Fragments, um que possui layout definido nos resources (PictureViewFragment) e o outro (ListPictureFragment) que não tem layout definido mas é representado normalmente na tela. Como isso é possível? O ListPictureFragment é uma extensão do ListView, por isso ele já tem a sua definição de layout. Já o PictureViewFragment é composto somente por um ImageView.

public class ListPictureFragment extends ListFragment

Vamos pensar em layout neste momento. Na pasta de resources, temos layout para landscape, grande e normal. Como já escrevi neste blog, os nomes das pastas definem os layouts de acordo com a situação. Vamos analisar o layout normal do Activity principal comparando com o layout large da mesma Activity.

Normal

<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <FrameLayout
        android:id="@id/fragment_container"
        android:name="com.example.fragments.PictureViewFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</GridLayout>


Large

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <fragment
        android:layout_weight="1"
        android:id="@id/ListPictureFragmentId"
        android:name="com.example.fragments.ListPictureFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent" />

    <fragment
        android:layout_weight="2"
        android:id="@id/PictureViewFragmentId"
        android:name="com.example.fragments.PictureViewFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent" />

</LinearLayout>


Repare que é muito diferente um layout do outro. O primeiro tem apenas um FrameLayout e o outro possui os dois Fragments que serão mostrados na tela. A principio, você deve ter ficado com a mesma dúvida que eu tive quando vi isso. Mas há uma explicação muito lógica para isso. Na visualização normal, este FrameLayout colocado na tela serve como um ponto de transição entre os objetos que serão mostrado dentro da Activity, isso faz com que você possa trabalhar com a troca de Fragments dentro da mesma Activity. Já o segundo caso, mostra que os dois Fragments já estão na tela e neste caso não haverá transição de objetos. Mesmo sabendo deste detalhe, ainda há partes que ficam nebulosas. Então vamos ver este código e entender como a Activity trata a visualização do layout quando é criada.


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

// se tiver o container de fragments...
if (findViewById(R.id.fragment_container) != null) {

// se tiver restaurando de um estado anterior não necessita de
// mudanças...
if (savedInstanceState != null)
return;

ListPictureFragment listPictureFragment = new ListPictureFragment();

// cria um novo fragmento e adiciona pois se não encontrou, este é a visualização Normal
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, listPictureFragment).commit();

}

}

Os dois pontos chaves deste código estão destacados. O primeiro é a condição que verifica o tipo de layout de acordo com os objetos presentes na tela. O segundo é utilizado se o FrameLayout foi encontrado na tela, indica que este layout será o normal, então irá adicionar o Fragment a tela. Você pode reparar também que o objeto FragmentManager é o responsável pela manipulação dos Fragments, o objeto FragmentManager é retornado pelo método getSupportFragmentManager. Ainda pensando no formato de tela de um smartphone comum, se o usuário clicar em um item da lista, a imagem será mostrada em outro Fragment, como mostrado no código abaixo:


public void onPictureItemClicked(int position) {

PictureViewFragment pictureViewFragment = (PictureViewFragment) getSupportFragmentManager().findFragmentById(R.id.PictureViewFragmentId);

if (pictureViewFragment != null) {

pictureViewFragment.ShowImage(position);

} else {

PictureViewFragment newFragment = new PictureViewFragment();
Bundle args = new Bundle();
args.putInt(PictureViewFragment.ARG_POSITION, position);
newFragment.setArguments(args);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
transaction.commit();
}
}

Quando o usuário clica no item é verificado se há o PictureViewFragment na tela, se tiver (tela grande) irá mostrar a imagem usando o método ShowImage(), se não tiver (tela normal) irá criar o Fragment, definir a posição inicial atráves de argumentos e em seguida utiliza uma transaction para substituir o FrameLayout pelo novo Fragment. O ponto em destaque é de extrema importância, pois através desta chamada o usuário pode utilizar o botão Voltar para retornar a tela anterior.

Toda a transação de troca de um Fragment deve estar entre o beginTransaction e o commit do objeto FragmentTransaction. 

Voltando um pouco na explicação sobre os Fragments, é importante saber que as classes que criamos com o intuito de ser um Fragment deve ser herdada da classe Fragment ou de uma classe que seja herdada de Fragment.

public class ListPictureFragment extends ListFragment

public class PictureViewFragment extends Fragment

Observe que a ListPictureFragment herda de ListFragment que é uma classe com operações de lista para ser implementada como um Fragment. Além da ListFragment, há também a WebViewFragment e a DialogFragment que serão discutidas mais para frente aqui mesmo neste blog.

É importante conhecer o ciclo de vida do Fragment que é um pouco diferente do ciclo de vida da atividade. Pelo diagrama abaixo você pode verificar que há alguns outros estados. 


No nosso aplicativo, utilizamos o método onAttach do Fragment para definir o callback da nossa aplicação para a classe da Activity principal.

Espero que este post tenha sido útil para vocês. 


*se você fizer o teste em um smartphone, tente colocar a tela em landscape, você irá utilizar o segundo layout, no entanto há um bug neste ponto que não consegui resolver a tempo de escrever este post, mas vou procurar a correção resolver este problema.

Abraço a todos.