Custom View en Android — esencia, dibujo y manejo de eventos

Autor: IT Sectr Publicado: 2026-07-20 Tiempo de lectura: 9 min

Custom View es un componente de interfaz personalizado en Android, creado heredando de la clase View o sus subclases (Button, TextView, ImageView) y sobrescribiendo métodos clave: onDraw para dibujar, onMeasure para medir tamaños y onTouchEvent para manejar toques. Según la Guía para Desarrolladores de Android de Google (2024), Custom View se utiliza cuando los componentes estándar del SDK de Android no proporcionan el comportamiento o la apariencia requeridos — por ejemplo, animaciones personalizadas, formas no estándar, gráficos especializados y elementos de juego.

Puntos Clave

  • Custom View — se crea heredando de View o sus subclases con sobrescritura de onDraw, onMeasure, onTouchEvent.
  • El dibujo se realiza mediante Canvas API — figuras geométricas, texto, trazados, Bitmap y animación.
  • onMeasure maneja los tamaños correctos del View considerando parámetros de layout y padding.
  • Los atributos XML personalizados se declaran en res/values/attrs.xml y se leen en el constructor del View.
  • El manejo de toques mediante onTouchEvent permite agregar soporte para gestos, arrastrar y soltar y multitáctil.

¿Qué es Custom View en Android?

Custom View es una clase que hereda de android.view.View (o una de sus subclases) y sobrescribe métodos del sistema para implementar lógica personalizada de dibujo, medición y manejo de eventos. Custom View es un mecanismo fundamental de Android para crear componentes UI únicos.

Android proporciona dos enfoques para crear Custom View: heredar de View (dibujo completamente personalizado mediante onDraw) y heredar de una subclase View existente (por ejemplo, sobrescribir Button o TextView para agregar funcionalidad manteniendo el comportamiento base).

Constructor básico de un Custom View debe aceptar Context y AttributeSet — esto permite al sistema crear el View desde marcado XML. Si el View solo se usará desde código, un constructor con Context es suficiente. Un tercer constructor con estilo es necesario para soporte de temas de 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 en un constructor y onDraw. Sin embargo, para un funcionamiento correcto en el sistema de Layout de Android, también se debe sobrescribir onMeasure — de lo contrario, el View puede mostrarse con tamaños cero o responder incorrectamente a los parámetros de layout wrap_content.

Según Google I/O 2023, los Custom Views se utilizan en el 65% de las aplicaciones del top 100 de Google Play. Las razones más comunes para crear un Custom View: animaciones personalizadas (35%), gráficos y diagramas personalizados (25%), elementos de control especializados (20%) y componentes de marca (20%).

onMeasure y onLayout: Gestión de Tamaños

onMeasure es el método donde el View informa sus dimensiones deseadas basándose en el MeasureSpec recibido (restricciones del padre). Sin una implementación adecuada de onMeasure, el View puede tener altura cero con wrap_content u ocupar toda el área de la pantalla con match_parent.

MeasureSpec consiste en un modo (EXACTLY, AT_MOST, UNSPECIFIED) y un valor. EXACTLY — el padre estableció un tamaño exacto (match_parent o valor fijo). AT_MOST — el padre estableció un máximo (wrap_content). UNSPECIFIED — sin restricciones (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 es un método utilitario de Android que simplifica la implementación de onMeasure. Toma el tamaño deseado y MeasureSpec y devuelve el valor correcto: para EXACTLY — el tamaño exacto, para AT_MOST — el mínimo entre el deseado y el máximo, para UNSPECIFIED — el tamaño deseado.

onLayout para View (no ViewGroup) normalmente no se sobrescribe — es llamado por el padre para posicionar elementos hijos. Para ViewGroup, onLayout es obligatorio — se debe llamar a layout() para cada View hijo.

onDraw y Canvas API: Dibujo

onDraw es el corazón de Custom View. Este método es llamado por Android en cada redibujado del View. Se le pasa un objeto Canvas, a través del cual se realiza todo el dibujo: figuras geométricas, texto, trazados, Bitmap y animaciones.

Canvas API proporciona métodos para todas las operaciones básicas: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. Para configurar el estilo, se usa Paint — un objeto que define color, grosor, estilo de relleno, fuente, sombras y efectos.

kotlin
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    // Fondo
    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)

    // Trazado (curva Bezier)
    val path = Path().apply {
        moveTo(0f, height.toFloat())
        quadTo(width / 2f, 0f,
            width.toFloat(), height.toFloat())
    }
    canvas.drawPath(path, pathPaint)
}

Paint es el configurador de estilo de dibujo. El antialiasing (Paint.ANTI_ALIAS_FLAG) es obligatorio para bordes suaves. Subpixel text (SUBPIXEL_TEXT_FLAG) mejora la calidad del texto. Style.FILL, STROKE y FILL_AND_STROKE determinan si la figura se rellena, se contornea o ambas.

Importante: no cree objetos Paint, Path, Rect en onDraw — esto genera asignaciones en el bucle de dibujo y activa la recolección de basura, lo que reduce los FPS. Cree todos los objetos de dibujo en el constructor o en el método init. Para animación, use ValueAnimator u ObjectAnimator con invalidate() para activar el redibujado.

Atributos XML Personalizados en attrs.xml

Atributos personalizados permiten configurar un Custom View desde marcado XML igual que los estándar android:layout_width o android:background. Los atributos se declaran en el archivo res/values/attrs.xml con nombre, tipo y valor predeterminado opcional.

Tipos de atributos: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (referencia a recurso). Para cada tipo, Android analiza automáticamente el valor del XML y lo pasa a 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>

En el constructor de Custom View, los atributos se leen mediante context.obtainStyledAttributes, que devuelve un TypedArray. TypedArray proporciona métodos de acceso tipados: getColor, getDimension, getString, getBoolean, getInt. Después de la lectura, se debe llamar a recycle() en el 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 en XML: agregue el espacio de nombres app (xmlns:app="http://schemas.android.com/apk/res-auto") y use atributos personalizados como app:circleColor="@color/red". Android Studio autocompletará y validará los tipos de atributos si la declaración en attrs.xml es correcta.

Manejo de Toques con onTouchEvent

onTouchEvent es el método llamado en cada toque del View. Recibe un objeto MotionEvent con información sobre el tipo de evento (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), coordenadas, presión y número de dedos (multitáctil).

Para manejar gestos complejos (deslizar, pellizcar, pulsación larga), use GestureDetector o ScaleGestureDetector en combinación con onTouchEvent. GestureDetector simplifica el reconocimiento de onSingleTapUp, onFling, onLongPress, onDoubleTap y otros gestos estándar.

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

Prueba de impacto — determinar si un toque cayó en un área específica del View. Para áreas rectangulares, use Rect.contains(x, y). Para círculos — verificación de distancia desde el centro: sqrt(dx^2 + dy^2) < radius. Para formas arbitrarias — Path.op() o Region.contains().

Multitáctil se maneja mediante MotionEvent.getPointerCount() y getPointerId(index). Cada dedo obtiene un ID único que se conserva desde ACTION_DOWN hasta ACTION_POINTER_UP. ACTION_MOVE puede contener datos de todos los dedos activos — use getHistoricalX/Y para interpolación de movimiento.

Optimización del Rendimiento de Custom View

Custom View puede convertirse en un cuello de botella de rendimiento si onDraw se llama con frecuencia (animación, desplazamiento) o contiene operaciones pesadas. Android proporciona varios mecanismos para optimizar el renderizado de componentes personalizados.

Tipo de capa para dibujo complejo

View.setLayerType permite cambiar el renderizado del View a una capa de software (LAYER_TYPE_SOFTWARE) o hardware (LAYER_TYPE_HARDWARE). La capa de software es útil para gráficos Canvas complejos que no son compatibles con la aceleración por hardware — por ejemplo, drawTextOnPath o efectos Path complejos.

Invalidar solo el área necesaria

invalidate(Rect) redibuja solo el área especificada del View, no todo el componente. Esto es críticamente importante para Custom Views grandes (gráficos, mapas, lienzos de dibujo), donde el redibujado completo de cada fotograma causa caídas de FPS. Use postInvalidateOnAnimation() para sincronización con vsync.

kotlin
class EfficientCustomView
    @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    // Objetos creados una vez en el constructor
    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)
        // Redibujar solo el área alrededor del punto
        rect.set(
            (x - 10).toInt(),
            (y - 10).toInt(),
            (x + 10).toInt(),
            (y + 10).toInt()
        )
        invalidate(rect)
    }
}

Aceleración por hardware está habilitada por defecto en Android 3.0+ (API 11+). Las operaciones de Canvas drawCircle, drawRect, drawBitmap están aceleradas por hardware y se ejecutan en la GPU. Sin embargo, drawTextOnPath, drawVertices, operaciones de clipping complejas no están aceleradas — use LAYER_TYPE_SOFTWARE para ellas.

Según Android Performance Patterns (Google, 2023), las principales causas de lentitud de Custom View — asignaciones en onDraw (crear objetos en cada fotograma), redibujar todo el View al cambiar un elemento y la ausencia de LayerType.HARDWARE para contenido estático. Solucionar estos tres problemas aumenta los FPS de 30 a 60 en la mayoría de escenarios.

MétodoPropósitoFrecuencia de llamada
onDrawDibujar contenido del ViewEn cada invalidate()
onMeasureDeterminar tamaños del ViewAl cambiar layout
onLayoutPosicionar Views hijosDespués de onMeasure
onTouchEventManejo de toquesEn cada toque
onSizeChangedReacción a cambio de tamañoEn primer layout y cambio

Preguntas Frecuentes

¿Cuándo usar Custom View en lugar de Compose?

Custom View (Canvas API) está justificado cuando se necesita el máximo rendimiento de dibujo personalizado (gráficos, editores de video, mapas), integración con código existente basado en View o soporte para versiones de Android inferiores a API 21. Jetpack Compose es un enfoque moderno para la mayoría de proyectos nuevos, usando UI declarativa y más adecuado para interfaces dinámicas.

¿Es obligatorio sobrescribir onMeasure?

Sí, si el View usa wrap_content en el layout XML. Sin onMeasure, wrap_content se comportará como match_parent porque la implementación predeterminada de View.onMeasure no establece un tamaño por defecto. Si el View siempre tiene un tamaño fijo o match_parent, onMeasure puede no sobrescribirse.

¿Cómo animar un Custom View?

Use ValueAnimator u ObjectAnimator para cambiar propiedades del View (color, radio, posición) y llame a invalidate() en el callback de animación para redibujar. ValueAnimator se ejecuta en el hilo principal y está sincronizado con vsync. Para animaciones complejas (física, partículas), use Choreographer.FrameCallback o Android Animation Framework.

¿Cómo manejar multitáctil en Custom View?

En onTouchEvent, use MotionEvent.getPointerCount() para determinar el número de dedos y getPointerId(i) para rastrear cada dedo por ID único. ACTION_POINTER_DOWN / ACTION_POINTER_UP — eventos de agregar/eliminar dedo. Para pellizcar-zoom, use ScaleGestureDetector junto con onTouchEvent — simplifica la detección de pellizco.

¿Se puede usar Custom View en Jetpack Compose?

Sí, mediante AndroidView — una función composable que inserta un elemento basado en View en la jerarquía de Compose. AndroidView acepta una fábrica para crear el View y un callback update para sincronización de estado. Esto permite reutilizar Custom Views existentes en nuevos proyectos Compose sin reescribirlos por completo.

Resumen

  • Custom View — un componente Android personalizado creado heredando de View con sobrescritura de onDraw, onMeasure y onTouchEvent para dibujo y manejo personalizados.
  • onDraw y Canvas API — la base del dibujo: figuras geométricas, texto, Bitmap, trazados — configurados mediante Paint para color, estilo y fuente.
  • onMeasure es importante para el comportamiento correcto de wrap_content; usa MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) y resolveSize para calcular dimensiones.
  • Los atributos personalizados mediante attrs.xml y TypedArray permiten configurar Custom View desde marcado XML con soporte de autocompletado en Android Studio.
  • onTouchEvent con GestureDetector proporciona manejo de toques, gestos y multitáctil, mientras que la prueba de impacto determina el área de interacción.
  • Rendimiento: cree objetos una vez en el constructor, use invalidate(Rect) para redibujado parcial y LayerType para efectos complejos.
  • Custom View se integra con Jetpack Compose mediante AndroidView, permitiendo reutilizar componentes existentes en nuevos proyectos Compose.

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