Custom View în Android — desenare și procesare evenimente

Autor: IT Sectr Publicat: 2026-07-20 Timp de citire: 9 min

Custom View — este o componentă de interfață personalizată în Android, creată prin moștenirea clasei View sau a subclaselor sale (Button, TextView, ImageView) și suprascrierea metodelor cheie: onDraw pentru desenare, onMeasure pentru măsurarea dimensiunilor și onTouchEvent pentru procesarea atingerilor. Conform Google Android Developer Guide (2024), Custom View este utilizat când componentele standard Android SDK nu asigură comportamentul sau aspectul necesar — de exemplu, pentru animații personalizate, forme nestandard, grafice specializate și elemente de joc.

Principalele puncte

  • Custom View — se creează prin moștenirea lui View sau a subclaselor sale cu suprascrierea onDraw, onMeasure, onTouchEvent.
  • Desenarea se realizează prin Canvas API — figuri geometrice, text, căi, Bitmap și animații.
  • onMeasure răspunde de dimensiunile corecte ale View-ului ținând cont de parametrii layout și padding.
  • Atributele XML personalizate se declară în res/values/attrs.xml și se citesc în constructorul View-ului.
  • Procesarea atingerilor onTouchEvent permite adăugarea suportului pentru gesturi, drag-and-drop și multitouch.

Ce este Custom View în Android?

Custom View — este o clasă care moștenește android.view.View (sau una dintre subclasele sale) și suprascrie metodele sistemului pentru implementarea propriei logici de desenare, măsurare și procesare a evenimentelor. Custom View este mecanismul fundamental al Android pentru crearea componentelor UI unice.

Android oferă două abordări pentru crearea Custom View: moștenirea de la View (desenare complet personalizată prin onDraw) și moștenirea de la o subclasă View existentă (de exemplu, suprascrierea Button sau TextView pentru a adăuga funcționalitate păstrând comportamentul de bază).

Constructorul de bază al Custom View trebuie să accepte Context și AttributeSet — aceasta permite sistemului să creeze View din cod XML. Dacă View va fi folosit doar din cod, constructorul cu Context este suficient. Al treilea constructor cu stil este necesar pentru suportul temelor 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 minim constă din constructor și onDraw. Totuși, pentru funcționarea corectă în sistemul Layout Android este necesară și suprascrierea onMeasure — altfel View se poate afișa cu dimensiuni zero sau poate reacționa incorect la parametrii layout wrap_content.

Conform Google I/O 2023, View-urile personalizate sunt utilizate în 65% din aplicațiile din top 100 Google Play. Cele mai frecvente motive pentru crearea Custom View: animații personalizate (35%), grafice și diagrame personalizate (25%), elemente de control specializate (20%) și componente branding (20%).

onMeasure și onLayout: gestionarea dimensiunilor

onMeasure — este metoda în care View își comunică sistemului dimensiunile dorite pe baza MeasureSpec-ului primit (constrângerile părintelui). Fără o implementare corectă a onMeasure, View poate avea înălțime zero la wrap_content sau poate ocupa întreaga suprafață a ecranului la match_parent.

MeasureSpec constă din modul (EXACTLY, AT_MOST, UNSPECIFIED) și valoare. EXACTLY — părintele a stabilit dimensiunea exactă (match_parent sau valoare fixă). AT_MOST — părintele a stabilit maximul (wrap_content). UNSPECIFIED — fără restricții (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 — este o metodă utilitară Android care simplifică implementarea onMeasure. Primește dimensiunea dorită și MeasureSpec și returnează valoarea corectă: pentru EXACTLY — dimensiunea exactă, pentru AT_MOST — minimul dintre cea dorită și maxim, pentru UNSPECIFIED — dimensiunea dorită.

onLayout pentru View (nu ViewGroup) de obicei nu se suprascrie — este apelat de părinte pentru poziționarea elementelor copil. Pentru ViewGroup onLayout este obligatoriu — în el trebuie apelat layout() pentru fiecare View copil.

onDraw și Canvas API: desenarea

onDraw — inima Custom View. Această metodă este apelată de Android la fiecare redesenare a View-ului. Primește un obiect Canvas prin care se realizează toată desenarea: figuri geometrice, text, căi, Bitmap și animații.

Canvas API oferă metode pentru toate operațiile de bază: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. Pentru configurarea stilului se folosește Paint — obiectul care definește culoarea, grosimea, stilul de umplere, fontul, umbrele și efectele.

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

    // Fundal
    canvas.drawRect(0f, 0f,
        width.toFloat(), height.toFloat(), bgPaint)

    // Text
    canvas.drawText("Vizualizare personalizată",
        paddingLeft.toFloat(),
        paddingTop.toFloat() + textPaint.textSize,
        textPaint)

    // Cerc
    canvas.drawCircle(
        width / 2f,
        height / 2f,
        radius, circlePaint)

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

Paint — este configuratorul stilului de desenare. Anti-aliasing (Paint.ANTI_ALIAS_FLAG) este obligatoriu pentru margini netede. Subpixel text (SUBPIXEL_TEXT_FLAG) îmbunătățește calitatea textului. Style.FILL, STROKE și FILL_AND_STROKE determină dacă figura va fi umplută, conturată sau ambele.

Important: nu creați obiecte Paint, Path, Rect în onDraw — aceasta duce la alocări în bucla de desenare și declanșează garbage collection, ceea ce reduce FPS. Creați toate obiectele de desenare în constructor sau în metoda init. Pentru animație folosiți ValueAnimator sau ObjectAnimator cu invalidate() pentru a declanșa redesenarea.

Atribute XML personalizate în attrs.xml

Atributele personalizate permit configurarea Custom View din cod XML la fel ca android:layout_width sau android:background standard. Atributele se declară în fișierul res/values/attrs.xml cu specificarea numelui, tipului și valorii implicite opționale.

Tipuri de atribute: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (referință la resursă). Pentru fiecare tip, Android parsează automat valoarea din XML și o transmite în 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="boolean" />
    </declare-styleable>
</resources>

În constructorul Custom View, atributele se citesc prin context.obtainStyledAttributes, care returnează un TypedArray. TypedArray oferă metode de acces tipizate: getColor, getDimension, getString, getBoolean, getInt. După citire, trebuie apelat recycle() pe TypedArray pentru eliberarea resurselor.

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

Utilizarea în XML: adăugați spațiul de nume app (xmlns:app="http://schemas.android.com/apk/res-auto") și folosiți atributele personalizate ca app:circleColor="@color/red". Android Studio va autocompleta și verifica tipurile atributelor dacă declarația în attrs.xml este corectă.

Procesarea atingerilor cu onTouchEvent

onTouchEvent — metoda apelată la fiecare atingere a View-ului. Primește un obiect MotionEvent cu informații despre tipul evenimentului (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), coordonate, presiune și numărul de degete (multitouch).

Pentru procesarea gesturilor complexe (swipe, scalare, apăsare lungă) folosiți GestureDetector sau ScaleGestureDetector în combinație cu onTouchEvent. GestureDetector simplifică recunoașterea onSingleTapUp, onFling, onLongPress, onDoubleTap și a altor gesturi standard.

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

Hit-testing — determinarea dacă o atingere a nimerit într-o anumită zonă a View-ului. Pentru zone dreptunghiulare folosiți Rect.contains(x, y). Pentru cercuri — verificarea distanței de la centru: sqrt(dx^2 + dy^2) < radius. Pentru forme arbitrare — Path.op() sau Region.contains().

Multitouch se procesează prin MotionEvent.getPointerCount() și getPointerId(index). Fiecare deget primește un ID unic care se păstrează de la ACTION_DOWN până la ACTION_POINTER_UP. ACTION_MOVE poate conține date pentru toate degetele active — folosiți getHistoricalX/Y pentru interpolarea mișcării.

Optimizarea performanței Custom View

Custom View poate deveni un blocaj de performanță dacă onDraw este apelat frecvent (animație, scroll) sau conține operații grele. Android oferă mai multe mecanisme pentru optimizarea desenării componentelor personalizate.

Layer type pentru desenare complexă

View.setLayerType permite comutarea randării View pe un strat software (LAYER_TYPE_SOFTWARE) sau hardware (LAYER_TYPE_HARDWARE). Software layer este util pentru grafica Canvas complexă care nu este suportată de accelerarea hardware — de exemplu, drawTextOnPath sau efecte Path complexe.

invalidate doar zona necesară

invalidate(Rect) — redesenează doar zona specificată a View-ului, nu întregul component. Acest lucru este critic pentru Custom View-uri mari (grafice, hărți, canavase de desenare), unde redesenarea completă a fiecărui cadru cauzează scăderea FPS. Folosiți postInvalidateOnAnimation() pentru sincronizarea cu vsync.

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

    // Obiecte create o dată în 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)
        // Redesenează doar zona din jurul punctului
        rect.set(
            (x - 10).toInt(),
            (y - 10).toInt(),
            (x + 10).toInt(),
            (y + 10).toInt()
        )
        invalidate(rect)
    }
}

Accelerarea hardware este activată implicit pe Android 3.0+ (API 11+). Operațiile Canvas drawCircle, drawRect, drawBitmap sunt accelerate hardware și executate pe GPU. Totuși, drawTextOnPath, drawVertices, operațiile complexe de clipping nu sunt accelerate — pentru ele folosiți LAYER_TYPE_SOFTWARE.

Conform Android Performance Patterns (Google, 2023), principalele cauze ale lag-ului în Custom View sunt alocările în onDraw (crearea de obiecte în fiecare cadru), redesenarea întregului View la schimbarea unui singur element și absența LayerType.HARDWARE pentru conținut static. Remedierea acestor trei probleme oferă o creștere a FPS de la 30 la 60 în majoritatea scenariilor.

MetodăDestinațieFrecvența apelării
onDrawDesenarea conținutului ViewLa fiecare invalidate()
onMeasureDeterminarea dimensiunilor ViewLa schimbarea layout-ului
onLayoutPoziționarea View-urilor copilDupă onMeasure
onTouchEventProcesarea atingerilorLa fiecare atingere
onSizeChangedReacția la schimbarea dimensiuniiLa primul layout și la schimbare

Întrebări frecvente

Când să folosim Custom View în loc de Compose?

Custom View (Canvas API) este justificat când este necesară performanța maximă a desenării personalizate (grafice, editoare video, hărți), integrarea cu codul View-based existent sau suportul pentru versiunile Android sub API 21. Jetpack Compose este abordarea modernă pentru majoritatea proiectelor noi, folosind UI declarativ și fiind mai potrivit pentru interfețe dinamice.

Este obligatorie suprascrierea onMeasure?

Da, dacă View folosește wrap_content în codul XML. Fără onMeasure, wrap_content va funcționa ca match_parent, deoarece implementarea standard View.onMeasure nu stabilește o dimensiune implicită. Dacă View are întotdeauna o dimensiune fixă sau match_parent, onMeasure poate fi omis.

Cum să animăm un Custom View?

Folosiți ValueAnimator sau ObjectAnimator pentru modificarea proprietăților View (culoare, rază, poziție) și apelați invalidate() în callback-ul animației pentru redesenare. ValueAnimator rulează pe firul principal și este sincronizat cu vsync. Pentru animații complexe (fizică, particule) folosiți Choreographer.FrameCallback sau Android Animation Framework.

Cum să procesăm multitouch în Custom View?

În onTouchEvent folosiți MotionEvent.getPointerCount() pentru determinarea numărului de degete și getPointerId(i) pentru urmărirea fiecărui deget după un ID unic. ACTION_POINTER_DOWN / ACTION_POINTER_UP — evenimentele de adăugare/eliminare a degetului. Pentru scalare folosiți ScaleGestureDetector împreună cu onTouchEvent — simplifică detectarea pinch-to-zoom.

Se poate folosi Custom View în Jetpack Compose?

Da, prin AndroidView — o funcție composable care inserează un element View-based în ierarhia Compose. AndroidView primește o fabrică pentru crearea View-ului și un callback update pentru sincronizarea stării. Aceasta permite reutilizarea Custom View-urilor existente în proiecte noi Compose fără rescrierea completă.

Rezumat

  • Custom View — componentă Android personalizată, creată prin moștenirea View cu suprascrierea onDraw, onMeasure și onTouchEvent pentru desenare și procesare personalizată.
  • onDraw și Canvas API — baza desenării: figuri geometrice, text, Bitmap, căi — cu configurare prin Paint pentru culoare, stil și font.
  • onMeasure este important pentru funcționarea corectă a wrap_content; folosește MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) și resolveSize pentru calcularea dimensiunilor.
  • Atributele personalizate prin attrs.xml și TypedArray permit configurarea Custom View din cod XML cu suport autocomplete în Android Studio.
  • onTouchEvent cu GestureDetector asigură procesarea atingerilor, gesturilor și multitouch, iar hit-testing determină zona de interacțiune.
  • Performanța: creați obiecte o singură dată în constructor, folosiți invalidate(Rect) pentru redesenare parțială și LayerType pentru efecte complexe.
  • Custom View se integrează cu Jetpack Compose prin AndroidView, ceea ce permite reutilizarea componentelor gata făcute în proiecte noi Compose.

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și