Custom View é um componente de interface personalizado no Android, criado herdando da classe View ou suas subclasses (Button, TextView, ImageView) e sobrescrevendo métodos-chave: onDraw para desenho, onMeasure para medir tamanhos e onTouchEvent para manipular toques. De acordo com o Google Android Developer Guide (2024), Custom View é usado quando os componentes padrão do Android SDK não fornecem o comportamento ou aparência necessários — por exemplo, animações personalizadas, formas não padronizadas, gráficos especializados e elementos de jogo.
Pontos Principais
Custom View é uma classe que herda de android.view.View (ou uma de suas subclasses) e sobrescreve métodos do sistema para implementar lógica personalizada de desenho, medição e manipulação de eventos. Custom View é um mecanismo fundamental do Android para criar componentes de UI únicos.
O Android fornece duas abordagens para criar Custom View: herdar de View (desenho totalmente personalizado via onDraw) e herdar de uma subclasse View existente (por exemplo, sobrescrever Button ou TextView para adicionar funcionalidade mantendo o comportamento base).
Construtor básico de um Custom View deve aceitar Context e AttributeSet — isso permite que o sistema crie a View a partir de marcação XML. Se a View for usada apenas a partir de código, um construtor com Context é suficiente. Um terceiro construtor com estilo é necessário para suporte a temas do Android.
class CustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
strokeWidth = 4f
style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2f,
height / 2f,
50f, paint)
}
}
Custom View mínimo consiste em um construtor e onDraw. No entanto, para funcionar corretamente no sistema de Layout do Android, onMeasure também deve ser sobrescrito — caso contrário, a View pode exibir tamanhos zero ou responder incorretamente aos parâmetros de layout wrap_content.
De acordo com Google I/O 2023, Custom Views são usados em 65% dos aplicativos do top 100 do Google Play. As razões mais comuns para criar um Custom View: animações personalizadas (35%), gráficos e diagramas personalizados (25%), elementos de controle especializados (20%) e componentes de marca (20%).
onMeasure é o método onde a View informa suas dimensões desejadas com base no MeasureSpec recebido (restrições do pai). Sem uma implementação adequada de onMeasure, a View pode ter altura zero com wrap_content ou ocupar toda a área da tela com match_parent.
MeasureSpec consiste em um modo (EXACTLY, AT_MOST, UNSPECIFIED) e um valor. EXACTLY — o pai definiu um tamanho exato (match_parent ou valor fixo). AT_MOST — o pai definiu um máximo (wrap_content). UNSPECIFIED — sem restrições (ScrollView, ListView).
override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int
) {
val desiredWidth = paddingLeft + paddingRight
+ DEFAULT_WIDTH
val desiredHeight = paddingTop + paddingBottom
+ DEFAULT_HEIGHT
val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
val modeWidth = MeasureSpec.getMode(widthMeasureSpec)
val resultWidth = when (modeWidth) {
MeasureSpec.EXACTLY -> measuredWidth
MeasureSpec.AT_MOST ->
desiredWidth.coerceAtMost(measuredWidth)
else -> desiredWidth
}
val resultHeight = resolveSize(desiredHeight,
heightMeasureSpec)
setMeasuredDimension(resultWidth, resultHeight)
}
resolveSize é um método utilitário do Android que simplifica a implementação de onMeasure. Ele pega o tamanho desejado e MeasureSpec e retorna o valor correto: para EXACTLY — o tamanho exato, para AT_MOST — o mínimo entre o desejado e o máximo, para UNSPECIFIED — o tamanho desejado.
onLayout para View (não ViewGroup) geralmente não é sobrescrito — é chamado pelo pai para posicionar elementos filhos. Para ViewGroup, onLayout é obrigatório — você precisa chamar layout() para cada View filho.
onDraw é o coração do Custom View. Este método é chamado pelo Android a cada redesenho da View. Um objeto Canvas é passado para ele, através do qual todo o desenho é realizado: formas geométricas, texto, caminhos, Bitmap e animações.
Canvas API fornece métodos para todas as operações básicas: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. Para configurar o estilo, Paint é usado — um objeto que define cor, espessura, estilo de preenchimento, fonte, sombras e efeitos.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Fundo
canvas.drawRect(0f, 0f,
width.toFloat(), height.toFloat(), bgPaint)
// Texto
canvas.drawText("Custom View",
paddingLeft.toFloat(),
paddingTop.toFloat() + textPaint.textSize,
textPaint)
// Círculo
canvas.drawCircle(
width / 2f,
height / 2f,
radius, circlePaint)
// Caminho (curva Bezier)
val path = Path().apply {
moveTo(0f, height.toFloat())
quadTo(width / 2f, 0f,
width.toFloat(), height.toFloat())
}
canvas.drawPath(path, pathPaint)
}
Paint é o configurador de estilo de desenho. Anti-aliasing (Paint.ANTI_ALIAS_FLAG) é obrigatório para bordas suaves. Texto subpixel (SUBPIXEL_TEXT_FLAG) melhora a qualidade do texto. Style.FILL, STROKE e FILL_AND_STROKE determinam se a forma será preenchida, contornada ou ambos.
Importante: não crie objetos Paint, Path, Rect no onDraw — isso causa alocações no loop de desenho e ativa a coleta de lixo, reduzindo o FPS. Crie todos os objetos de desenho no construtor ou no método init. Para animação, use ValueAnimator ou ObjectAnimator com invalidate() para acionar o redesenho.
Atributos personalizados permitem configurar um Custom View a partir de marcação XML, assim como android:layout_width ou android:background padrão. Os atributos são declarados no arquivo res/values/attrs.xml com nome, tipo e valor padrão opcional.
Tipos de atributos: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (referência a recurso). Para cada tipo, o Android analisa automaticamente o valor do XML e o passa para TypedArray.
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CustomView">
<attr name="circleColor"
format="color" />
<attr name="circleRadius"
format="dimension" />
<attr name="labelText"
format="string" />
<attr name="showAnimation"
format="booleano" />
</declare-styleable>
</resources>
No construtor do Custom View, os atributos são lidos através de context.obtainStyledAttributes, que retorna um TypedArray. TypedArray fornece métodos de acesso tipados: getColor, getDimension, getString, getBoolean, getInt. Após a leitura, recycle() deve ser chamado no TypedArray para liberar recursos.
init {
val typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.CustomView
)
circleColor = typedArray.getColor(
R.styleable.CustomView_circleColor,
Color.BLUE
)
circleRadius = typedArray.getDimension(
R.styleable.CustomView_circleRadius,
50f
)
labelText = typedArray.getString(
R.styleable.CustomView_labelText
) ?: "Default"
typedArray.recycle()
}
Uso em XML: adicione o namespace app (xmlns:app="http://schemas.android.com/apk/res-auto") e use atributos personalizados como app:circleColor="@color/red". O Android Studio autocompletará e validará os tipos de atributos se a declaração em attrs.xml estiver correta.
onTouchEvent é o método chamado a cada toque na View. Ele recebe um objeto MotionEvent com informações sobre o tipo de evento (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), coordenadas, pressão e número de dedos (multitoque).
Para lidar com gestos complexos (deslizar, pinçar, pressionar longo), use GestureDetector ou ScaleGestureDetector em combinação com onTouchEvent. GestureDetector simplifica o reconhecimento de onSingleTapUp, onFling, onLongPress, onDoubleTap e outros gestos padrão.
private val gestureDetector = GestureDetector(
context, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
handleTap(e.x, e.y)
return true
}
})
override fun onTouchEvent(event: MotionEvent): Boolean {
val handled = gestureDetector.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_MOVE -> {
currentX = event.x
currentY = event.y
invalidate()
return true
}
}
return handled || super.onTouchEvent(event)
}
Teste de colisão — determinar se um toque atingiu uma área específica da View. Para áreas retangulares, use Rect.contains(x, y). Para círculos — verificação de distância do centro: sqrt(dx^2 + dy^2) < radius. Para formas arbitrárias — Path.op() ou Region.contains().
Multitoque é tratado através de MotionEvent.getPointerCount() e getPointerId(index). Cada dedo recebe um ID único que é preservado de ACTION_DOWN até ACTION_POINTER_UP. ACTION_MOVE pode conter dados de todos os dedos ativos — use getHistoricalX/Y para interpolação de movimento.
Custom View pode se tornar um gargalo de desempenho se onDraw for chamado com frequência (animação, rolagem) ou contiver operações pesadas. O Android fornece vários mecanismos para otimizar a renderização de componentes personalizados.
View.setLayerType permite alternar a renderização da View para uma camada de software (LAYER_TYPE_SOFTWARE) ou hardware (LAYER_TYPE_HARDWARE). A camada de software é útil para gráficos Canvas complexos que não são suportados por aceleração de hardware — por exemplo, drawTextOnPath ou efeitos Path complexos.
invalidate(Rect) redesenha apenas a área especificada da View, não o componente inteiro. Isso é criticamente importante para Custom Views grandes (gráficos, mapas, telas de desenho), onde o redesenho completo de cada quadro causa quedas de FPS. Use postInvalidateOnAnimation() para sincronização com vsync.
class EfficientCustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// Objetos criados uma vez no construtor
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rect = Rect()
private val position = PointF()
fun updatePosition(x: Float, y: Float) {
position.set(x, y)
// Redesenhar apenas a área ao redor do ponto
rect.set(
(x - 10).toInt(),
(y - 10).toInt(),
(x + 10).toInt(),
(y + 10).toInt()
)
invalidate(rect)
}
}
Aceleração de hardware está habilitada por padrão no Android 3.0+ (API 11+). As operações de Canvas drawCircle, drawRect, drawBitmap são aceleradas por hardware e executadas na GPU. No entanto, drawTextOnPath, drawVertices, operações de clipping complexas não são aceleradas — use LAYER_TYPE_SOFTWARE para elas.
De acordo com Android Performance Patterns (Google, 2023), as principais causas de lentidão do Custom View — alocações no onDraw (criação de objetos a cada quadro), redesenho de toda a View ao alterar um elemento e ausência de LayerType.HARDWARE para conteúdo estático. Corrigir esses três problemas aumenta o FPS de 30 para 60 na maioria dos cenários.
| Método | Propósito | Frequência de chamada |
|---|---|---|
| onDraw | Desenhar conteúdo da View | A cada invalidate() |
| onMeasure | Determinar tamanhos da View | Ao alterar layout |
| onLayout | Posicionar Views filhas | Após onMeasure |
| onTouchEvent | Manipulação de toques | A cada toque |
| onSizeChanged | Reação a mudança de tamanho | No primeiro layout e mudança |
Perguntas Frequentes
Custom View (Canvas API) é justificado quando é necessário o máximo desempenho de desenho personalizado (gráficos, editores de vídeo, mapas), integração com código existente baseado em View ou suporte para versões do Android abaixo da API 21. Jetpack Compose é uma abordagem moderna para a maioria dos novos projetos, usando UI declarativa e mais adequado para interfaces dinâmicas.
Sim, se a View usar wrap_content no layout XML. Sem onMeasure, wrap_content se comportará como match_parent porque a implementação padrão de View.onMeasure não define um tamanho padrão. Se a View sempre tiver um tamanho fixo ou match_parent, onMeasure pode não ser sobrescrito.
Use ValueAnimator ou ObjectAnimator para alterar propriedades da View (cor, raio, posição) e chame invalidate() no callback da animação para redesenhar. ValueAnimator é executado na thread principal e está sincronizado com vsync. Para animações complexas (física, partículas), use Choreographer.FrameCallback ou Android Animation Framework.
Em onTouchEvent, use MotionEvent.getPointerCount() para determinar o número de dedos e getPointerId(i) para rastrear cada dedo por ID único. ACTION_POINTER_DOWN / ACTION_POINTER_UP — eventos de adicionar/remover dedo. Para pinçar-zoom, use ScaleGestureDetector junto com onTouchEvent — simplifica a detecção de pinça.
Sim, através do AndroidView — uma função composable que incorpora um elemento baseado em View na hierarquia do Compose. AndroidView aceita uma fábrica para criar a View e um callback update para sincronização de estado. Isso permite reutilizar Custom Views existentes em novos projetos Compose sem reescrita completa.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também