View Lifecycle — a sequência de métodos que o Android chama para desenhar e redesenhar um elemento de interface do usuário (View) na tela. Ao contrário de Activity ou Fragment, View é um componente leve que não possui um ciclo de vida estendido, mas passa por um processo rigoroso de três fases: onMeasure (medição), onLayout (posicionamento), onDraw (desenho). Entender o View Lifecycle é necessário para criar Views personalizadas, otimizar o desempenho e resolver problemas de desenho. De acordo com o Google, Views personalizadas aceleram a UI em 15–40% em comparação com uma combinação de ViewGroups aninhados padrão quando implementadas corretamente. Documentação do Android sobre Views personalizadas descreve onMeasure, onLayout e onDraw como os três pilares do View Lifecycle.
Pontos principais
View Lifecycle é o processo pelo qual uma View do Android (e ViewGroup) passa para se exibir na tela. Ao contrário de Activity ou Fragment, View não tem onStart/onStop/onDestroy — sua “vida” consiste em um processo cíclico de medição, posicionamento e desenho. Este ciclo é acionado sempre que uma View precisa ser exibida ou redesenhada.
As três fases do View Lifecycle:
O ciclo completo do View Lifecycle também inclui métodos relacionados à anexação de uma View a uma janela: onAttachedToWindow (View anexada a uma janela, tem aceleração HW) e onDetachedFromWindow (View desanexada, recursos são liberados). Esses métodos são chamados uma vez por vida da View e são importantes para registrar/cancelar animações e sensores.
De acordo com o Android Performance Blog, 65% dos problemas de desempenho da UI (jank, quadros perdidos) estão relacionados à implementação incorreta de onMeasure e onDraw: substituição excessiva, chamada desnecessária de requestLayout(), criação de objetos em onDraw.
onMeasure — a fase mais importante e mais complexa do View Lifecycle. Nesta etapa, o Android determina quanto espaço a View ocupará na tela. O sistema passa MeasureSpec — instruções empacotadas em int que consistem em um modo e um tamanho.
Os três modos de MeasureSpec:
| Modo | Constante | Significado | Exemplo |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Tamanho exato definido pelo pai (match_parent ou largura fixa) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View pode ter até o tamanho máximo especificado (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Sem restrições — View pode ter qualquer tamanho (ScrollView, RecyclerView) | largura ilimitada → MeasureSpec(0, UNSPECIFIED) |
A implementação do onMeasure deve:
setMeasuredDimension(int width, int height) para salvar as dimensões medidas.getPaddingLeft() + getPaddingRight() da largura disponível.measureChild() ou measureChildWithMargins().Erro típico: não considerar MeasureSpec ao usar wrap_content. Se uma View estiver definida como wrap_content, mas onMeasure não lidar com AT_MOST e retornar um tamanho fixo, a View será cortada ou ocupará mais espaço do que o necessário.
onLayout — a fase em que uma View ou ViewGroup organiza seus filhos dentro de seus limites. Para uma View comum (não ViewGroup), onLayout não é necessário — o sistema chama layout() com os parâmetros passados pelo pai. Para uma ViewGroup, onLayout é obrigatório — sem ele, as Views filhas não serão posicionadas.
Assinatura do onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// organizando Views filhas
}
O parâmetro changed indica se a posição ou o tamanho da View mudou em comparação com o layout anterior. Se for false, a View pode pular o recálculo das posições dos filhos para otimização.
Para ViewGroup, onLayout deve:
getChildCount() e getChildAt(i).child.layout(l, t, r, b) para cada filho.onLayout é chamado após onMeasure — as dimensões medidas estão disponíveis via getMeasuredWidth()/getMeasuredHeight(). Se uma View filha tiver dimensões reais diferentes após layout(), requestLayout() será chamado para remedir. Isso é chamado de “passe de layout” e pode desencadear uma reação em cadeia de recálculos.
onDraw — a fase em que uma View se desenha no Canvas. Esta é a única fase que pode ser chamada várias vezes sem onMeasure e onLayout — se a View estiver marcada como invalidate(). O Canvas fornece API de desenho: drawLine, drawRect, drawCircle, drawText, drawBitmap e drawPath.
Regras do onDraw:
canvas.clipRect() para recortar partes invisíveis.Ordem de desenho em ViewGroup: fundo (setBackgroundDrawable) → onDraw (conteúdo) → dispatchDraw (Views filhas) → onDrawForeground (primeiro plano). dispatchDraw chama onDraw de cada filho. Sobrescrever dispatchDraw é usado para aplicar efeitos sobre os elementos filhos.
De acordo com as estatísticas do Android Vitals, as causas mais comuns de quadros perdidos em onDraw são criar objetos dentro do método (48%), chamar decodeResource (22%) e operações complexas com Path sem cache (15%).
Invalidação — o mecanismo que aciona o redesenho da View. Chamar invalidate() marca a View como “suja” e agenda a chamada de onDraw no próximo ciclo de desenho. Chamar requestLayout() é uma operação mais “pesada”, acionando o ciclo completo: onMeasure → onLayout → onDraw.
| Método | O que faz | Quando usar |
|---|---|---|
| invalidate() | Aciona onDraw sem onMeasure/onLayout | Apenas a aparência mudou (cor, texto, progresso) |
| invalidate(Rect) | Redesenha apenas a área especificada | Parte da View mudou — animação, seleção |
| postInvalidate() | Chama invalidate de uma thread não-UI | Thread em segundo plano atualizou dados para desenho |
| requestLayout() | Aciona onMeasure → onLayout → onDraw | O tamanho do conteúdo mudou (texto, imagem) |
| forceLayout() | Marca a View para remedição forçada | Estado interno mudou, o tamanho pode ter mudado |
Animações e View Lifecycle: ViewPropertyAnimator e ValueAnimator chamam invalidate() em cada quadro de animação. ObjectAnimator chama um setter na View, que se o setter mudar o tamanho (largura/altura), automaticamente chama requestLayout(). Isso pode ser caro para ViewGroups complexas: cada requestLayout aciona a hierarquia completa até a view raiz.
Regra de otimização: invalidate() em vez de requestLayout() em todos os lugares onde apenas a aparência muda (cor, transparência, rotação sem alteração de tamanho). Use requestLayout apenas ao alterar tamanhos ou conteúdo que afete o tamanho.
Views personalizadas são uma ferramenta poderosa para criar UI única, mas exigem adesão estrita às regras de desempenho. Aqui estão as principais recomendações do Google para otimização do View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) para Views com animações e setLayerType(LAYER_TYPE_NONE) após a conclusão.Um indicador de progresso circular simples com implementação correta de onMeasure, onDraw e invalidate.
class CircularProgressView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
style = Paint.Style.STROKE
strokeWidth = 8f
strokeCap = Paint.Cap.ROUND
}
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.LTGRAY
style = Paint.Style.STROKE
strokeWidth = 8f
}
private var progress = 0f
private var viewWidth = 0
private var viewHeight = 0
fun setProgress(value: Float) {
progress = value.coerceIn(0f, 100f)
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 100 * resources.displayMetrics.density.toInt()
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
val size = minOf(width, height).coerceAtLeast(desiredSize)
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val padding = progressPaint.strokeWidth / 2
val radius = (minOf(viewWidth, viewHeight) - padding) / 2
val cx = viewWidth / 2f
val cy = viewHeight / 2f
canvas.drawCircle(cx, cy, radius, backgroundPaint)
val sweepAngle = (progress / 100f) * 360f
canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
-90f, sweepAngle, false, progressPaint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
}
}
Barra de progresso circular: onMeasure retorna um tamanho quadrado com base em MeasureSpec, onSizeChanged lembra as dimensões, onDraw desenha o fundo e o arco de progresso. Invalidate é chamado quando o progresso muda — onMeasure/onLayout não são afetados. Paint é criado uma vez no construtor, não em onDraw.
Uma ViewGroup personalizada que organiza Views filhas em linhas (como Flexbox wrap).
class FlowLayout constructor(
context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {
private val horizontalSpacing = 8.dpToPx(resources)
private val verticalSpacing = 8.dpToPx(resources)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = MeasureSpec.getSize(widthMeasureSpec)
var totalHeight = paddingTop + paddingBottom
var rowWidth = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
if (rowWidth + child.measuredWidth > width - paddingRight) {
totalHeight += rowHeight + verticalSpacing
rowWidth = paddingLeft
rowHeight = 0
}
rowWidth += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
totalHeight += rowHeight
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean,
l: Int, t: Int, r: Int, b: Int) {
var rowTop = paddingTop
var rowLeft = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (rowLeft + child.measuredWidth > r - paddingRight) {
rowTop += rowHeight + verticalSpacing
rowLeft = paddingLeft
rowHeight = 0
}
child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
rowLeft += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}
FlowLayout sobrescreve onMeasure: mede cada filho, quebra para uma nova linha quando a largura é excedida, calcula a altura total. onLayout posiciona os filhos por coordenadas considerando as quebras de linha. generateLayoutParams retorna MarginLayoutParams para suportar margem em Views filhas.
Uma View personalizada desenha uma curva Bezier suave, pré-calculando o Path e armazenando-o em cache.
class WaveView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#4A90D9")
style = Paint.Style.FILL
}
private val wavePath = Path()
private var isPathDirty = true
private var viewWidth = 0
private var viewHeight = 0
fun refreshWave() {
isPathDirty = true
invalidate()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
isPathDirty = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (isPathDirty) {
wavePath.reset()
val amplitude = viewHeight * 0.1f
wavePath.moveTo(0f, viewHeight * 0.5f)
for (x in 0..viewWidth step 4) {
val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
wavePath.lineTo(x.toFloat(), y)
}
wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
wavePath.lineTo(0f, viewHeight.toFloat())
wavePath.close()
isPathDirty = false
}
canvas.drawPath(wavePath, wavePaint)
}
}
Cache de Path: isPathDirty = true apenas quando as dimensões da View mudam ou refreshWave() é chamado. Em onDraw, o Path é recalculado apenas se estiver “ sujo”. Isso evita recalcular a curva Bezier a cada quadro de animação, economizando CPU.
Perguntas frequentes
View Lifecycle é um processo cíclico de desenho (onMeasure → onLayout → onDraw) independente da criação/destruição da Activity. View não tem onStart/onStop — ou está visível (anexada a uma janela) ou não. Activity Lifecycle gerencia o estado do componente do aplicativo, View Lifecycle gerencia o desenho da UI.
requestLayout() aciona o ciclo completo onMeasure → onLayout → onDraw para toda a árvore de Views a partir da raiz. Se requestLayout() for chamado com frequência (por exemplo, a cada quadro de animação), causa jank e quadros perdidos. De acordo com o Google, um requestLayout leva em média 2–5 ms em uma ViewGroup de 10 elementos. Para animações, use invalidate().
onAttachedToWindow é chamado quando uma View é anexada a uma Window — torna-se parte da hierarquia visível. Neste momento, a View recebe aceleração HW e acesso aos recursos da Window (WindowManager, Display). onAttachedToWindow é o lugar certo para registrar listeners de animação e BroadcastReceivers que vivem enquanto a View está visível.
Overdraw é uma situação em que um pixel é desenhado várias vezes em um único quadro. Cada passagem extra desperdiça tempo de GPU. Métodos de redução: defina windowBackground no tema (não desenhe fundo no layout), use canvas.clipRect(), evite mesclar fundos aninhados, use ConstraintLayout em vez de LinearLayout aninhado. Android Studio → Profile GPU Rendering → Overdraw mostra um mapa de cores do overdraw (azul = 1x, vermelho = 3x+).
Sim, se a View tiver um fundo. super.onDraw() desenha o fundo da View. Se sua View personalizada não tiver fundo ou você desenhar seu próprio fundo, super.onDraw() pode ser omitido — isso economiza uma passagem de desenho. Para ViewGroup, super.dispatchDraw() é obrigatório — desenha as Views filhas.
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