View Lifecycle — qué es, procesos onMeasure onLayout onDraw

Autor: IT Sectr Publicado: 2026-03-05 Tiempo de lectura: 10 min

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 consta de tres fases: onMeasure (tamaños), onLayout (posiciones), onDraw (dibujado) — y se activa con invalidate() o requestLayout().
  • onMeasure calcula el ancho y alto de una View basándose en MeasureSpec (AT_MOST, EXACTLY, UNSPECIFIED).
  • onLayout coloca las Views hijas dentro de una ViewGroup, determinando sus coordenadas left, top, right, bottom.
  • onDraw renderiza el contenido de la View en Canvas: fondo, texto, formas, imágenes.
  • Un View Lifecycle incorrecto es la principal causa de problemas de rendimiento de UI (jank, frames caídos) y problemas de jerarquía.

View Lifecycle — qué es en Android

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:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — determina las dimensiones deseadas de la View. El sistema pasa MeasureSpec — una instrucción sobre qué dimensiones están permitidas (valor exacto, máximo o sin restricciones).
  • onLayout(boolean changed, int left, int top, int right, int bottom) — posiciona la View y sus hijas en pantalla. Para una View, define sus propios límites; para una ViewGroup, posiciona los elementos hijos.
  • onDraw(Canvas canvas) — dibuja el contenido de la View en el Canvas proporcionado. El sistema proporciona un Canvas que traduce comandos a bitmap o GPU.

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: medición de dimensiones de View

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:

ModoConstanteSignificadoEjemplo
EXACTLYMeasureSpec.EXACTLYTamaño exacto establecido por el padre (match_parent o ancho fijo)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTLa View puede tener hasta el tamaño máximo especificado (wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIEDSin restricciones — la View puede tener cualquier tamaño (ScrollView, RecyclerView)ancho ilimitado → MeasureSpec(0, UNSPECIFIED)

La implementación de onMeasure debe:

  • Llamar a setMeasuredDimension(int width, int height) para guardar las dimensiones medidas.
  • Tener en cuenta el padding — restar getPaddingLeft() + getPaddingRight() del ancho disponible.
  • Para ViewGroup — medir todos los hijos mediante measureChild() o measureChildWithMargins().
  • Para wrap_content — calcular el tamaño según el contenido (texto, imagen).
  • No llamar a requestLayout() dentro de onMeasure — esto causará un bucle infinito.

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: colocación de Views en pantalla

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:

java
@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:

  • Iterar por todas las hijas mediante getChildCount() y getChildAt(i).
  • Para cada hija, determinar left, top, right, bottom — coordenadas dentro de la ViewGroup (considerando el padding).
  • Llamar a child.layout(l, t, r, b) para cada hija.
  • Tener en cuenta gravity, margins, alignment.

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: dibujado de contenido en Canvas

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:

  • No crear objetos en onDraw — cada llamada a onDraw debe usar objetos pre-creados (Path, Paint, Rect). Crear objetos en onDraw causa pausas de GC y frames caídos.
  • No llamar a requestLayout() o invalidate() dentro de onDraw — esto desencadenará un bucle infinito de redibujado.
  • No realizar cálculos largos — onDraw se ejecuta en el hilo de UI. Los cálculos complejos deben moverse a un hilo en segundo plano o pre-calcularse.
  • Usar aceleración por hardware — desde API 14+, Canvas puede funcionar mediante GPU. Para gráficos complejos (gradientes, sombras, rotaciones), la aceleración HW proporciona hasta un 300% de mejora de rendimiento.
  • Dibujar solo el área visible — usar 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: cuándo se redibuja una View

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étodoQué haceCuándo usarlo
invalidate()Activa onDraw sin onMeasure/onLayoutSolo cambió la apariencia (color, texto, progreso)
invalidate(Rect)Redibuja solo el área especificadaParte de la View cambió — animación, selección
postInvalidate()Llama a invalidate desde un hilo que no es de UIEl hilo en segundo plano actualizó datos para dibujar
requestLayout()Activa onMeasure → onLayout → onDrawEl tamaño del contenido cambió (texto, imagen)
forceLayout()Marca la View para remedición forzadaEl 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.

Optimización de Views personalizadas: mejores prácticas

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.

  • Pre-calcular todo lo que se pueda calcular — tamaños, coordenadas, ruta, colores de degradado. En onDraw, solo realice el dibujado.
  • Almacenar en caché los resultados de medición — si una View tiene dimensiones fijas, guarde el MeasureSpec y devuelva setMeasuredDimension sin cálculos adicionales.
  • Usar ViewConfiguration — getScaledTouchSlop, getScaledMinimumFlingVelocity — para el manejo de toques.
  • Minimizar el número de Views en la jerarquía — las Views personalizadas que combinan múltiples elementos son siempre más rápidas que una ViewGroup con 3–5 Views anidadas. Google recomienda no más de 10 Views anidadas por pantalla.
  • Usar ConstraintLayout para jerarquía plana — construye una única ViewGroup con rendimiento cercano a RelativeLayout, pero sin anidamiento.
  • Desactivar la capa de hardware después del redibujado — usar setLayerType(LAYER_TYPE_HARDWARE) para Views con animaciones y setLayerType(LAYER_TYPE_NONE) al finalizar.
  • Usar invalidate() con Rect — redibujar solo el área cambiada, no toda la View.
  • Evitar overdraw — usar Profile GPU Rendering en Android Studio para identificar redibujados innecesarios. El overdraw promedio para aplicaciones de Google es 1.5x, el máximo recomendado es 2.5x.

Ejemplos de código View en Kotlin

Ejemplo 1: View personalizada — indicador de progreso

Un indicador de progreso circular simple con implementación correcta de onMeasure, onDraw e invalidate.

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

Ejemplo 2: ViewGroup — FlowLayout simple

Una ViewGroup personalizada que coloca Views hijas en filas (como Flexbox wrap).

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

Ejemplo 3: onDraw con almacenamiento en caché de Path

Una View personalizada dibuja una curva Bezier suave, precalculando el Path y almacenándolo en caché.

kotlin
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

¿En qué se diferencia View Lifecycle de Activity Lifecycle?

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.

¿Qué efecto tiene requestLayout() en el rendimiento?

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

¿Cuándo se llama a onAttachedToWindow?

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.

¿Qué es overdraw y cómo reducirlo?

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

¿Es necesario super.onDraw() en una View personalizada?

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

  • View Lifecycle — tres fases de dibujado: onMeasure (dimensiones), onLayout (posición), onDraw (renderizado).
  • onMeasure procesa MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) y llama a setMeasuredDimension.
  • onLayout en ViewGroup posiciona las Views hijas con coordenadas left/top/right/bottom.
  • onDraw renderiza contenido en Canvas — no cree objetos dentro de este método.
  • invalidate() activa solo onDraw, requestLayout() activa el ciclo completo onMeasure → onLayout → onDraw.
  • Las Views personalizadas aceleran la UI entre un 15–40%, pero requieren una implementación correcta de onMeasure y almacenamiento en caché de objetos en onDraw.
  • Para gráficos complejos, use aceleración por hardware y almacene en caché Path/Bitmap.

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.

Discutir el proyecto

Lea también