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 — 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.
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 — 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).
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 — 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.
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.
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.
<!-- 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.
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ă.
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.
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.
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.
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(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.
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ție | Frecvența apelării |
|---|---|---|
| onDraw | Desenarea conținutului View | La fiecare invalidate() |
| onMeasure | Determinarea dimensiunilor View | La schimbarea layout-ului |
| onLayout | Poziționarea View-urilor copil | După onMeasure |
| onTouchEvent | Procesarea atingerilor | La fiecare atingere |
| onSizeChanged | Reacția la schimbarea dimensiunii | La primul layout și la schimbare |
Întrebări frecvente
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.
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.
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.
Î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.
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
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.
Citiți și