View Lifecycle — la secuencia de métodos que Android llama para dibujar y redibujar un elemento de interfaz de usuario (View) en pantalla. A diferencia de Activity o Fragment, View es un componente ligero que no tiene un ciclo de vida extendido, pero pasa por un estricto proceso trifásico: onMeasure (medición), onLayout (posicionamiento), onDraw (dibujado). Comprender View Lifecycle es necesario para crear Views personalizadas, optimizar el rendimiento y resolver problemas de dibujado. Según Google, las Views personalizadas aceleran la UI entre un 15–40% en comparación con una combinación de ViewGroups anidados estándar cuando se implementan correctamente. Documentación de Android sobre Views personalizadas describe onMeasure, onLayout y onDraw como los tres pilares de View Lifecycle.
Puntos clave
View Lifecycle es el proceso que una View de Android (y ViewGroup) sigue para mostrarse en pantalla. A diferencia de Activity o Fragment, View no tiene onStart/onStop/onDestroy — su “vida” consiste en un proceso cíclico de medición, posicionamiento y dibujado. Este ciclo se activa cada vez que una View necesita mostrarse o redibujarse.
Las tres fases de View Lifecycle:
El ciclo completo de View Lifecycle también incluye métodos relacionados con adjuntar una View a una ventana: onAttachedToWindow (la View se adjunta a una ventana, tiene aceleración HW) y onDetachedFromWindow (la View se desadjunta, los recursos se liberan). Estos métodos se llaman una vez por vida de la View y son importantes para registrar/cancelar animaciones y sensores.
Según Android Performance Blog, 65% de los problemas de rendimiento de UI (jank, frames caídos) están relacionados con una implementación incorrecta de onMeasure y onDraw: sobrescritura excesiva, llamada a requestLayout() innecesaria, creación de objetos en onDraw.
onMeasure — la fase más importante y más compleja de View Lifecycle. En esta etapa, Android determina cuánto espacio ocupará la View en pantalla. El sistema pasa MeasureSpec — instrucciones empaquetadas en int que consisten en un modo y un tamaño.
Los tres modos de MeasureSpec:
| Modo | Constante | Significado | Ejemplo |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Tamaño exacto establecido por el padre (match_parent o ancho fijo) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | La View puede tener hasta el tamaño máximo especificado (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Sin restricciones — la View puede tener cualquier tamaño (ScrollView, RecyclerView) | ancho ilimitado → MeasureSpec(0, UNSPECIFIED) |
La implementación de onMeasure debe:
setMeasuredDimension(int width, int height) para guardar las dimensiones medidas.getPaddingLeft() + getPaddingRight() del ancho disponible.measureChild() o measureChildWithMargins().Error típico: no tener en cuenta MeasureSpec al usar wrap_content. Si una View está configurada como wrap_content, pero onMeasure no maneja AT_MOST y devuelve un tamaño fijo, la View se recortará u ocupará más espacio del necesario.
onLayout — la fase en la que una View o ViewGroup coloca a sus hijas dentro de sus límites. Para una View normal (no ViewGroup), onLayout no es necesario — el sistema llama a layout() con los parámetros pasados del padre. Para una ViewGroup, onLayout es obligatorio — sin él, las Views hijas no se colocarán.
Firma de onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// colocación de Views hijas
}
El parámetro changed indica si la posición o el tamaño de la View ha cambiado respecto al layout anterior. Si es false, la View puede omitir el recálculo de posiciones de las hijas para optimizar.
Para ViewGroup, onLayout debe:
getChildCount() y getChildAt(i).child.layout(l, t, r, b) para cada hija.onLayout se llama después de onMeasure — las dimensiones medidas están disponibles mediante getMeasuredWidth()/getMeasuredHeight(). Si una View hija tiene dimensiones reales diferentes después de layout(), se llamará a requestLayout() para volver a medir. Esto se denomina “pase de layout” y puede desencadenar una reacción en cadena de recálculos.
onDraw — la fase en la que una View se dibuja a sí misma en Canvas. Es la única fase que puede llamarse múltiples veces sin onMeasure y onLayout — si la View está marcada como invalidate(). Canvas proporciona API de dibujado: drawLine, drawRect, drawCircle, drawText, drawBitmap y drawPath.
Reglas de onDraw:
canvas.clipRect() para recortar partes invisibles.Orden de dibujado en ViewGroup: fondo (setBackgroundDrawable) → onDraw (contenido) → dispatchDraw (Views hijas) → onDrawForeground (primer plano). dispatchDraw llama a onDraw de cada hija. Sobrescribir dispatchDraw se usa para aplicar efectos sobre los elementos hijos.
Según estadísticas de Android Vitals, las causas más comunes de frames caídos en onDraw son crear objetos dentro del método (48%), llamar a decodeResource (22%) y operaciones complejas con Path sin almacenamiento en caché (15%).
Invalidation — el mecanismo que desencadena el redibujado de la View. Llamar a invalidate() marca la View como “sucia” y programa la llamada a onDraw en el próximo ciclo de dibujado. Llamar a requestLayout() es una operación más “pesada”, que desencadena el ciclo completo: onMeasure → onLayout → onDraw.
| Método | Qué hace | Cuándo usarlo |
|---|---|---|
| invalidate() | Activa onDraw sin onMeasure/onLayout | Solo cambió la apariencia (color, texto, progreso) |
| invalidate(Rect) | Redibuja solo el área especificada | Parte de la View cambió — animación, selección |
| postInvalidate() | Llama a invalidate desde un hilo que no es de UI | El hilo en segundo plano actualizó datos para dibujar |
| requestLayout() | Activa onMeasure → onLayout → onDraw | El tamaño del contenido cambió (texto, imagen) |
| forceLayout() | Marca la View para remedición forzada | El estado interno cambió, el tamaño pudo haber cambiado |
Animaciones y View Lifecycle: ViewPropertyAnimator y ValueAnimator llaman a invalidate() en cada frame de animación. ObjectAnimator llama a un setter en la View, que si el setter cambia el tamaño (ancho/alto), automáticamente llama a requestLayout(). Esto puede ser costoso para ViewGroups complejas: cada requestLayout desencadena la jerarquía completa hasta la vista raíz.
Regla de optimización: invalidate() en lugar de requestLayout() en todos los casos donde solo cambia la apariencia (color, transparencia, rotación sin cambio de tamaño). Use requestLayout solo al cambiar tamaños o contenido que afecte el tamaño.
Las Views personalizadas son una herramienta poderosa para crear UI única, pero requieren un cumplimiento estricto de las reglas de rendimiento. Aquí están las recomendaciones clave de Google para la optimización de View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) para Views con animaciones y setLayerType(LAYER_TYPE_NONE) al finalizar.Un indicador de progreso circular simple con implementación correcta 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 progreso circular: onMeasure devuelve un tamaño cuadrado basado en MeasureSpec, onSizeChanged recuerda las dimensiones, onDraw dibuja el fondo y el arco de progreso. Se llama a Invalidate cuando cambia el progreso — onMeasure/onLayout no se ven afectados. Paint se crea una vez en el constructor, no en onDraw.
Una ViewGroup personalizada que coloca Views hijas en filas (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 sobrescribe onMeasure: mide cada hija, pasa a una nueva línea cuando se supera el ancho, calcula la altura total. onLayout posiciona a las hijas por coordenadas considerando los saltos de línea. generateLayoutParams devuelve MarginLayoutParams para soportar margen en las Views hijas.
Una View personalizada dibuja una curva Bezier suave, precalculando el Path y almacenándolo en caché.
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)
}
}
Almacenamiento en caché de Path: isPathDirty = true solo cuando cambian las dimensiones de la View o se llama a refreshWave(). En onDraw, Path se recalcula solo si está “sucio”. Esto evita recalcular la curva Bezier en cada frame de animación, ahorrando CPU.
Preguntas frecuentes
View Lifecycle es un proceso cíclico de dibujado (onMeasure → onLayout → onDraw) independiente de la creación/destrucción de Activity. View no tiene onStart/onStop — o es visible (adjunta a una ventana) o no. Activity Lifecycle gestiona el estado del componente de la aplicación, View Lifecycle gestiona el dibujado de la UI.
requestLayout() desencadena el ciclo completo onMeasure → onLayout → onDraw para todo el árbol de Views desde la raíz. Si se llama a requestLayout() con frecuencia (por ejemplo, cada frame de animación), causa jank y frames caídos. Según Google, una requestLayout toma en promedio 2–5 ms en una ViewGroup de 10 elementos. Para animaciones, use invalidate().
onAttachedToWindow se llama cuando una View se adjunta a una Window — se vuelve parte de la jerarquía visible. En ese momento, la View recibe aceleración HW y acceso a recursos de Window (WindowManager, Display). onAttachedToWindow es el lugar adecuado para registrar listeners de animaciones y BroadcastReceivers que viven mientras la View es visible.
Overdraw es una situación en la que un píxel se dibuja varias veces en un solo frame. Cada paso extra desperdicia tiempo de GPU. Métodos de reducción: establecer windowBackground en el tema (no dibujar fondo en layout), usar canvas.clipRect(), evitar fusionar fondos anidados, usar ConstraintLayout en lugar de LinearLayout anidado. Android Studio → Profile GPU Rendering → Overdraw muestra un mapa de color de overdraw (azul = 1x, rojo = 3x+).
Sí, si la View tiene un fondo. super.onDraw() dibuja el fondo de la View. Si su View personalizada no tiene fondo o dibuja su propio fondo, se puede omitir super.onDraw() — esto ahorra un pase de dibujado. Para ViewGroup, super.dispatchDraw() es obligatorio — dibuja las Views hijas.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también