Criando Novos Componentes Gráficos no Android

On 10 de agosto de 2012 by Ricardo Câmara

Introdução

O Android nos oferece um modelo componentizado muito vasto e sofisticado, para a construção de sua interface de usuário. Este modelo tem como base dois conjuntos de classes fundamentais para a formação de layout: View e ViewGroup. No conjunto de Views temos os widgets e no conjunto de ViewGroup temos os layouts.

Em uma lista parcial de widgets disponíveis temos: Button, TextView, EditText, ListView, CheckBox, RadioButton, Spinner e também encontramos outros itens mais específicos, como AutoCompleteTextView, ImageSwitcher e TextSwitcher. Nos Layouts temos o LinearLayout, FrameLayout, RelativeLayout e entre outros.

Se nenhum dos widgets padrão ou layout atendem as suas necessidades, você pode criar sua própria subclasse View, customizando-a para atender suas vontades. Para isso serão necessários apenas pequenos ajustes no widget ou Layout e você poderá criar uma subclasse de widget ou layout e sobrescrever os seus métodos.

Criar sua própria subclasse View lhe dará o controle preciso sobre a aparência e função de um elemento na tela. Aqui estão alguns exemplos do que você pode ser feito:

  • Você pode criar um tipo View completamente customizado como, por exemplo, um controle de volume renderizado usando gráficos 2D que se assemelhe ao controle analógico real.
  • Você pode combinar um grupo de componentes View em um único componente, talvez para fazer algo como uma combobox (uma combinação de lista em popup e um campo text), um controle dual-pane (com painéis do lado esquerdo e direito onde você pode assinalar de qual lista um item faz parte) e assim por diante.
  • Você pode sobrescrever uma forma como um EditText é renderizado na tela.
  • Customizar uma ImageView, desenhando sobra a imagem.

Abordagem básica

Aqui está uma visão geral do que você precisa fazer para começar a criar os seus próprios componentes View customizados:

1 – Extender uma classe View existente ou subclasse em sua própria classe.

2 – Sobrescrever alguns dos métodos da superclasse. Os métodos da superclasse a serem feitos sobrescritos começam com ‘on’. Por exemplo, onDraw(), onMeasure(), onKeyDown().

3 – Uma vez completada, a nova classe pode ser usada no lugar da classe na qual ela é baseada.

Componentes customizados completos

Componentes customizados completos podem ser usados para criar componentes gráficos da maneira como desejar. Talvez um medidor gráfico que se pareça com um medidor analógico ou qualquer outra coisa que você tenha em mente. De qualquer forma, você vai querer algo que os componentes padrão ainda não façam, não importa a maneira como você os combine.

Felizmente, você pode facilmente criar componentes que tem o visual e se comportam da maneira que você quer limitado, talvez, apenas pela sua imaginação, o tamanho da tela e o poder de processamento disponível (lembre-se que sua aplicação vai rodar em um dispositivo que terá muito menos poder de processamento que seu computador onde está criando sua aplicação).

Para criar um componente customizado completo:

1 – A View mais genérica da qual você pode extender é, sem surpresa nenhuma, a View. Então, você vai começar, normalmente, extendendo seu código a partir dessa classe e criar seu novo super componente a partir dela.

2 – Você pode suprir um construtor que poderá receber atributos e parâmetros a partir de um arquivo XML.

3 – Você vai querer, provavelmente, criar seus próprios event listeners, property accessors e modifiers e possivelmente comportamentos mais sofisticados dentro da sua classe de componente.

4 – Você vai certamente querer vai querer sobrescrever o método onMeasure() e também de onDraw() se você quiser que seu componente mostre alguma coisa. Mesmo tendo comportamentos padrão, o onDraw() padrão não faz nada e o onMeasure() padrão vai sempre colocar o tamanho como 100×100.

5 – Obviamente outros métodos on… deverão ser sobrescritos.

Extendendo onDraw() e onMeasure()

 O método onDraw() cria um Canvas sobre o qual você pode implementar qualquer coisa que queira: gráficos 2D, outros componentes padrão ou customizados, texto estilizado ou qualquer coisa que você possa imaginar. Imagine um pintor que vai criar sua obra. Ele pega as tintas e a tela onde vai pintar. O Canvas é a tela onde você poderá criar o que imaginar. Em tecnologia, o termo canvas significa isso: o local onde você poderá criar o que deseja.

O Canvas não se aplica a gráficos 3D. Se quiser usar gráficos dessa natureza, você deve extender SurfaceView ao invés da View, e desenhar o gráfico em uma thread separada. Você pode checar por GLSurfaceViewActivity para mais detalhes.

Já o método onMeasure() é um pouco mais complexo. onMeasure() é uma peça crítica para o contrato de renderização do seu componente e o seu container. O onMeasure() deve ser sobrescrito para eficientemente e corretamente reportar as medidas de seu container para que a renderização seja perfeita na tela. O onMeasure() foi feito para ser mais complexo por conta dos requerimentos de limites dos componentes pais onde eles estão posicionados e pelo requerimento de chamar o método setMeasuredDimension() com a medida em largura e altura que foi calculada. Se você falhar em chamar esse método de um onMeasure() que foi sobrescrito, o resultado é que uma exceção será chamada.

Implementar o onMeasure() seria algo assim:

  1. O método onMeasure() que foi sobrescreito é chamado com as especificações de largura e altura (parâmetros widthMeasureSpec e heightMeasureSpec, ambos com valores em inteiro representando as dimensões do componente pai onde ele está sendo criado) que deve ser tratado como um requerimento para as restrições de largura e altura a serem produzidas.
  2. O método onMeasure() do componente calcula as medidas de largura e altura necessárias para renderizar o componente. Ele vai tentar se manter dentro das especificações enviadas.
  3. Uma vez que a largura e altura foram calculadas, o método setMeasuredDimension(int width, int height) deve ser chamado com as medidas calculadas. Se houver uma falha em conseguir um resultado e uma exceção será lançada.
Categoria Métodos Descrição
Criação Construtores Existe uma forma do construtor que é chamado quando a View é criada no código ou quando a View é inflada a partir de um arquivo de layout. A segunda forma passa quaisquer atributos definidos no arquivo de layout.
onFinishInflate() Chamado após a view e todos os itens filhos serem inflados da XML.
Layout onMeasure(int,int) Chamado para determinar os requerimentos de tamanho para  a View e todos os seus filhos.
onLayout(boolean, int, int, int, int) Chamado quando a View deve assinalar o tamanho e posição de todos os seus filhos.
onSizeChanged(int, int, int, int) Chamado quando o tamanho da View foi mudado.
Desenhando onDraw(Canvas) Chamado quando uma View deve renderizar seu conteúdo.
Processamento de eventos onKeyDown(int, KeyEvent) Chamado quando um novo evento de tecla ocorre.
onKeyUp(int, KeyEvent) Chamado quando um novo evento key up ocorre.
onTrackballEvent(MotionEvent) Chamado quando um evento de movimentação do trackball ocorre.
onTouchEvent(MotionEvent) Chamado quando um evento touch screen ocorre.
Foco onFocusChanged(boolean, int, Rect) Chamado quando uma View ganha ou perde o foco.
onWindowFocusChanged(boolean) Chamado quando uma janela contendo views ganha ou perde foco.
Anexamento onAttachedToWindow() Chamado quando uma View é anexada a uma janela.
onDetachedFromWindow() Chamado quando uma View é desanexada de uma janela.
onWindowVisibilityChanged(int) Chamado quando a visibiliadde da janela contendo a View é modificada.

Exemplo de componente customizado

O componente customizado que darei como exemplo será um velocímetro em 2D. A ideia é bom simples, criarei uma classe (ExemploPonteiro),  que herdará da classe ImageView. O fundo do velocímetro será um drawable comum, atribuído como em um ImageView normal. O ponteiro será desenhado na tela usando a classe Canvas. E ficará da seguinte maneira:

O exemplo está dividido em duas partes, a primeira parte mostra como desenhar o ponteiro de forma estática, para mostrar somente como desenhar na tela, na segunda parte farei com que o ponteiro possa girar, para passar uma ideia de como você pode animar o seu componente.

Primeira parte

Nesta primeira parte a activity não tem nenhuma alteração, pois irei somente mostrar como desenhar o ponteiro na tela, então a novidade aqui será criar a nova classe e coloca-la no xml.

MainActivity

Então o código da activity não possui nenhuma alteração ficando como código a seguir.

package com.exemplocomponentecustomizado;
 
import android.app.Activity;
import android.os.Bundle;
 
public class MainActivity extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

Classe ExemploPonteiro

A seguir temos o código do componente customizado. Aqui mostrarei como desenhar o ponteiro estático na tela. Temos aqui três classes novas que são a classes Path e Paint. Com a classe Path você pode criar os “objetos” que serão desenhados na tela, no nosso exemplo criarei um retângulo com o Path, este retângulo será o corpo principal do ponteiro. Já a classe Paint é como um pincel na mão do programador com ela você decide a cor que vai desenhar e também se o objeto a ser desenhado é translúcido ou não, entre outras coisas.

Criamos a classe ExemploPonteiro herdando de ImageView. No construtor da classe iniciamos o paint e o path, atribuímos vermelho para o paint e também atribuímos true para o AntiAlias, o AntiAlias serve para melhorar a qualidade do desenho.

No onDraw() é onde desenhamos o objeto na tela, então criamos o retângulo que forma o ponteiro. O método moveTo() passa o início do Path para o ponto passado como parâmetro. E os métodos lineTo() criam as linhas de ponto em ponto, quando chegamos no último ponto usamos o close() para voltar ao ponto inicial fechando assim o retângulo. Os métodos this.getWidth() e this.getHeight() retornam a largura e a altura do canvas respectivamente.

Para desenhar utilizamos a classe Canvas, o método drawCircle() desenha o círculo tendo como parâmetro as coordenadas do ponto, o raio e o paint a ser utilizado. O método drawPath() recebe um path e um paint como parâmetro e desenha o path usando os atributos do paint.

Veja o seguinte código.

package com.exemplocomponentecustomizado;
 
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.widget.ImageView;
 
public class ExemploPonteiro extends ImageView
{	
	Path ponteiro;
	Paint paint;
 
	public ExemploPonteiro(Context context, AttributeSet attrs)
	{
		super(context, attrs);
 
		paint = new Paint();
		ponteiro = new Path();
 
		paint.setColor(Color.RED);
		paint.setAntiAlias(true);
	}
 
	@Override
	protected void onDraw(Canvas canvas)
	{
		super.onDraw(canvas);
 
		ponteiro.moveTo((this.getWidth()/2)-8, this.getHeight()/2);
		ponteiro.lineTo((this.getWidth()/2)+8, this.getHeight()/2);
		ponteiro.lineTo((this.getWidth()/2)+8, (this.getHeight()/14)*12);
		ponteiro.lineTo((this.getWidth()/2)-8, (this.getHeight()/14)*12);
		ponteiro.close();
 
		canvas.drawCircle(this.getWidth()/2, this.getHeight()/2, this.getHeight()/20, paint);
		canvas.drawPath(ponteiro, paint);
 
	}
}

                Arquivo XML

No arquivo xml a única mudança que temos é a forma de declarar o componente, para declarar o componente onde colocaríamos <ImageView /> colocamos <”EndereçoCompletoDoPacote”.NomedaClasse />, conforme o código a seguir.

 

Animando o ponteiro

Agora mostrarei uma das formas de como animar o seu componente. A ideia é bem simples criarei duas variáveis a mais, boolean primeiraVez e float ângulo. O booleano irá controlar para que o path seja criado somente a primeira vez que o onDraw() for invocado, e a variável ângulo controlara a posição que o ponteiro será desenhado.

Classe ExemploPonteiro

No construtor atribuímos true para a variável primeiraVez e iniciamos ângulo com o valor zero. No método onDraw() primeiramente salvamos o estado do canvas com canvas.save(), isso implica que podemos alterar qualquer coisa no canvas (nesse caso será o ângulo) e depois voltarmos ao estado em que foi salvo com canvas.restore(). Criamos o path somente uma vez para economizar processamento. Usamos canvas.rotate() para girar o canvas o tanto que a variável ângulo (passado como parâmetro) definir, girando em relação ao centro do canvas (ponto também passado como parâmetro). Desenhamos o ponteiro e restauramos o canvas, nesse caso a única diferença será voltar o canvas ao ângulo inicial. O método desenhar() recebe o ângulo como parâmetro, seta o ângulo em que o ponteiro será desenhado.  E logo após depois chama o método invalidate() que invalida a view inteira sendo assim o método onDraw() é chamado automaticamente redesenhando o ponteiro no novo ângulo. O método criaPath() simplesmente cria o path como no exemplo anterior, só coloquei a criação do path fora do onDraw() para uma melhor visualização do código. Veja abaixo o código do componente.

package com.exemplocomponentecustomizado;
 
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.widget.ImageView;
 
public class ExemploPonteiro extends ImageView
{	
	Path ponteiro;
	Paint paint;
 
	boolean primeiraVez;
 
	float angulo;
 
	public ExemploPonteiro(Context context, AttributeSet attrs)
	{
		super(context, attrs);
 
		paint = new Paint();
		ponteiro = new Path();
 
		paint.setColor(Color.RED);
		paint.setAntiAlias(true);
 
		primeiraVez = true;
 
		angulo = 0;
	}
 
	@Override
	protected void onDraw(Canvas canvas)
	{
		canvas.save();
 
		super.onDraw(canvas);
 
		if(primeiraVez)
		{
			criaPath();
			primeiraVez = false;
		}
 
		canvas.rotate(angulo,this.getWidth()/2, this.getHeight()/2);
 
		canvas.drawCircle(this.getWidth()/2, this.getHeight()/2, this.getHeight()/20, paint);
		canvas.drawPath(ponteiro, paint);
 
		canvas.restore();
	}
 
	public void desenhar(float a)
	{
		angulo = a;
		invalidate();
	}
 
	public void criaPath()
	{
		Ponteiro = new Path();
 
		ponteiro.moveTo((this.getWidth()/2)-8, this.getHeight()/2);
		ponteiro.lineTo((this.getWidth()/2)+8, this.getHeight()/2);
		ponteiro.lineTo((this.getWidth()/2)+8, (this.getHeight()/14)*12);
		ponteiro.lineTo((this.getWidth()/2)-8, (this.getHeight()/14)*12);
		ponteiro.close();
	}
}

MainActivity

Aqui criei uma thread e um handler para fazer a animação do componente. A thread é responsável por criar os valores do ângulo do ponteiro e o handler chama a função que desenha o ponteiro na tela. Todas alterações na interface dessa natureza devem ser feitas usando handler. Veja como ficou o código.

package com.exemplocomponentecustomizado;
 
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
 
public class MainActivity extends Activity
{
	ExemploPonteiro ponteiro;
 
	Thread thread;
	Handler handler = new Handler();
 
	float angulo = 0;
 
	boolean crescendo = true;
 
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        ponteiro = (ExemploPonteiro) findViewById(R.id.ponteiro);
    }
 
    @Override
    protected void onStart()
    {
    	super.onStart();
 
        thread = new Thread(rodando);
        thread.start();
    };
 
    Runnable rodando = new Runnable()
    {
		@Override
		public void run()
		{
			while(true)	
			{	
				if(crescendo)
				{
					angulo++;
 
					if(angulo == 360)
					{
						crescendo = false;
					}
				}
				else
				{
					angulo--;
 
					if(angulo == 0)
					{
						crescendo = true;
					}
				}
 
				handler.post(runHandler);
 
				try
				{
					Thread.sleep(25);
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
			}
		}
    };
 
    Runnable runHandler = new Runnable()
    {
		@Override
		public void run()
		{
			ponteiro.desenhar(angulo);
		}	
    };
}

A imagem usada no exemplo é a imagem a baixo:

5 Responses to “Criando Novos Componentes Gráficos no Android”

  • Queridos, precisando implantar um velocímetro parecido esse! Não cossegui acompanhando esse tutorial! Falta o xml! Pode me ajudar! Se puder me mandar o projeto , agradeço !

  • olá, testei seu código e não deu erro, mas não fez nada.

    poderia me mandar seu código funcionando, por favor.
    se quiser posso te mandar o meu também.

  • Bom dia, Ricardo!

    Tudo bem?

    Estou começando a desenvolver um aplicativo onde preciso criar componentes customizados.

    Não sei por onde começar.

    Pode me indicar algum curso ou tutorial mais detalhado, por favor?

    Obrigado!

    Marco Aurélio

Deixe um comentário

O seu endereço de e-mail não será publicado.