Custom View no Android — essência, desenho e manipulação de eventos

Autor: IT Sectr Publicado: 2026-07-20 Tempo de leitura: 9 min

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 — criado herdando de View ou suas subclasses com sobrescrita de onDraw, onMeasure, onTouchEvent.
  • O desenho é realizado através da Canvas API — formas geométricas, texto, caminhos, Bitmap e animação.
  • onMeasure lida com os tamanhos corretos da View considerando parâmetros de layout e padding.
  • Os atributos XML personalizados são declarados em res/values/attrs.xml e lidos no construtor da View.
  • A manipulação de toques via onTouchEvent permite adicionar suporte a gestos, arrastar e soltar e multitoque.

O que é Custom View no Android?

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.

kotlin
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 e onLayout: Gerenciamento de Tamanhos

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).

kotlin
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 e Canvas API: Desenho

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.

kotlin
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 XML Personalizados em attrs.xml

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.

xml
<!-- 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.

kotlin
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.

Manipulação de Toques com onTouchEvent

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.

kotlin
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.

Otimização de Desempenho do Custom View

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.

Tipo de camada para desenho complexo

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.

Invalidar apenas a área necessária

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.

kotlin
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étodoPropósitoFrequência de chamada
onDrawDesenhar conteúdo da ViewA cada invalidate()
onMeasureDeterminar tamanhos da ViewAo alterar layout
onLayoutPosicionar Views filhasApós onMeasure
onTouchEventManipulação de toquesA cada toque
onSizeChangedReação a mudança de tamanhoNo primeiro layout e mudança

Perguntas Frequentes

Quando usar Custom View em vez de Compose?

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.

É obrigatório sobrescrever onMeasure?

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.

Como animar um Custom View?

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.

Como lidar com multitoque no Custom View?

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.

Pode-se usar Custom View no Jetpack Compose?

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

  • Custom View — um componente Android personalizado criado herdando de View com sobrescrita de onDraw, onMeasure e onTouchEvent para desenho e manipulação personalizados.
  • onDraw e Canvas API — a base do desenho: formas geométricas, texto, Bitmap, caminhos — configurados via Paint para cor, estilo e fonte.
  • onMeasure é importante para o comportamento correto de wrap_content; usa MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) e resolveSize para calcular dimensões.
  • Os atributos personalizados via attrs.xml e TypedArray permitem configurar Custom View a partir de marcação XML com suporte a autocomplete no Android Studio.
  • onTouchEvent com GestureDetector fornece manipulação de toques, gestos e multitoque, enquanto o teste de colisão determina a área de interação.
  • Desempenho: crie objetos uma vez no construtor, use invalidate(Rect) para redesenho parcial e LayerType para efeitos complexos.
  • Custom View integra-se com Jetpack Compose através do AndroidView, permitindo reutilizar componentes prontos em novos projetos Compose.

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.

Discutir o projeto

Leia também