Custom View è un componente di interfaccia personalizzato in Android, creato ereditando dalla classe View o dalle sue sottoclassi (Button, TextView, ImageView) e sovrascrivendo i metodi chiave: onDraw per il disegno, onMeasure per misurare le dimensioni e onTouchEvent per gestire i tocchi. Secondo la Guida per Sviluppatori Android di Google (2024), Custom View viene utilizzato quando i componenti standard dell’SDK Android non forniscono il comportamento o l’aspetto richiesti — ad esempio, per animazioni personalizzate, forme non standard, grafici specializzati ed elementi di gioco.
Punti chiave
Custom View è una classe che eredita da android.view.View (o da una delle sue sottoclassi) e sovrascrive i metodi di sistema per implementare logica personalizzata di disegno, misurazione e gestione degli eventi. Custom View è un meccanismo fondamentale di Android per creare componenti UI unici.
Android offre due approcci per creare Custom View: ereditare da View (disegno completamente personalizzato tramite onDraw) ed ereditare da una sottoclasse View esistente (ad esempio, sovrascrivere Button o TextView per aggiungere funzionalità mantenendo il comportamento di base).
Costruttore di base di un Custom View deve accettare Context e AttributeSet — ciò consente al sistema di creare la View dal markup XML. Se la View viene utilizzata solo dal codice, un costruttore con Context è sufficiente. Un terzo costruttore con stile è necessario per il supporto dei temi 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 minimo è costituito da un costruttore e onDraw. Tuttavia, per funzionare correttamente nel sistema di layout Android, anche onMeasure deve essere sovrascritto — altrimenti la View potrebbe visualizzarsi con dimensioni zero o rispondere in modo errato ai parametri di layout wrap_content.
Secondo Google I/O 2023, i Custom View sono utilizzati nel 65% delle app della top 100 di Google Play. I motivi più comuni per creare un Custom View: animazioni personalizzate (35%), grafici e diagrammi personalizzati (25%), elementi di controllo specializzati (20%) e componenti brandizzati (20%).
onMeasure è il metodo in cui la View riporta le sue dimensioni desiderate in base al MeasureSpec ricevuto (vincoli del genitore). Senza una corretta implementazione di onMeasure, la View potrebbe avere altezza zero con wrap_content o occupare l’intera area dello schermo con match_parent.
MeasureSpec è composto da una modalità (EXACTLY, AT_MOST, UNSPECIFIED) e un valore. EXACTLY — il genitore ha impostato una dimensione esatta (match_parent o valore fisso). AT_MOST — il genitore ha impostato un massimo (wrap_content). UNSPECIFIED — senza restrizioni (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 è un metodo di utilità di Android che semplifica l’implementazione di onMeasure. Prende la dimensione desiderata e MeasureSpec e restituisce il valore corretto: per EXACTLY — la dimensione esatta, per AT_MOST — il minimo tra desiderata e massima, per UNSPECIFIED — la dimensione desiderata.
onLayout per View (non ViewGroup) di solito non viene sovrascritto — viene chiamato dal genitore per posizionare gli elementi figli. Per ViewGroup, onLayout è obbligatorio — è necessario chiamare layout() per ogni View figlio.
onDraw è il cuore del Custom View. Questo metodo viene chiamato da Android a ogni ridisegno della View. Viene passato un oggetto Canvas, attraverso il quale viene eseguito tutto il disegno: forme geometriche, testo, percorsi, Bitmap e animazioni.
Canvas API fornisce metodi per tutte le operazioni di base: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. Per configurare lo stile, viene utilizzato Paint — un oggetto che definisce colore, spessore, stile di riempimento, carattere, ombre ed effetti.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Sfondo
canvas.drawRect(0f, 0f,
width.toFloat(), height.toFloat(), bgPaint)
// Testo
canvas.drawText("Custom View",
paddingLeft.toFloat(),
paddingTop.toFloat() + textPaint.textSize,
textPaint)
// Cerchio
canvas.drawCircle(
width / 2f,
height / 2f,
radius, circlePaint)
// Percorso (curva di Bezier)
val path = Path().apply {
moveTo(0f, height.toFloat())
quadTo(width / 2f, 0f,
width.toFloat(), height.toFloat())
}
canvas.drawPath(path, pathPaint)
}
Paint è il configuratore di stile di disegno. L’anti-aliasing (Paint.ANTI_ALIAS_FLAG) è obbligatorio per bordi morbidi. Il testo subpixel (SUBPIXEL_TEXT_FLAG) migliora la qualità del testo. Style.FILL, STROKE e FILL_AND_STROKE determinano se la forma verrà riempita, contornata o entrambi.
Importante: non creare oggetti Paint, Path, Rect in onDraw — ciò causa allocazioni nel ciclo di disegno e attiva la garbage collection, riducendo gli FPS. Crea tutti gli oggetti di disegno nel costruttore o nel metodo init. Per l’animazione, usa ValueAnimator o ObjectAnimator con invalidate() per attivare il ridisegno.
Attributi personalizzati consentono di configurare un Custom View dal markup XML proprio come i standard android:layout_width o android:background. Gli attributi vengono dichiarati nel file res/values/attrs.xml con nome, tipo e valore predefinito opzionale.
Tipi di attributi: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (riferimento a risorsa). Per ogni tipo, Android analizza automaticamente il valore dall’XML e lo passa a 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="booleano" />
</declare-styleable>
</resources>
Nel costruttore di Custom View, gli attributi vengono letti tramite context.obtainStyledAttributes, che restituisce un TypedArray. TypedArray fornisce metodi di accesso tipizzati: getColor, getDimension, getString, getBoolean, getInt. Dopo la lettura, è necessario chiamare recycle() su TypedArray per liberare le risorse.
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()
}
Utilizzo in XML: aggiungi il namespace app (xmlns:app="http://schemas.android.com/apk/res-auto") e usa attributi personalizzati come app:circleColor="@color/red". Android Studio autocompleterà e validerà i tipi di attributi se la dichiarazione in attrs.xml è corretta.
onTouchEvent è il metodo chiamato a ogni tocco sulla View. Riceve un oggetto MotionEvent con informazioni sul tipo di evento (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), coordinate, pressione e numero di dita (multitouch).
Per gestire gesti complessi (scorrimento, pizzicamento, pressione lunga), usa GestureDetector o ScaleGestureDetector in combinazione con onTouchEvent. GestureDetector semplifica il riconoscimento di onSingleTapUp, onFling, onLongPress, onDoubleTap e altri gesti 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 — determinare se un tocco ha raggiunto un’area specifica della View. Per aree rettangolari, usa Rect.contains(x, y). Per cerchi — verifica della distanza dal centro: sqrt(dx^2 + dy^2) < radius. Per forme arbitrarie — Path.op() o Region.contains().
Multitouch viene gestito tramite MotionEvent.getPointerCount() e getPointerId(index). Ogni dito riceve un ID unico che viene conservato da ACTION_DOWN a ACTION_POINTER_UP. ACTION_MOVE può contenere dati di tutte le dita attive — usa getHistoricalX/Y per l’interpolazione del movimento.
Custom View può diventare un collo di bottiglia delle prestazioni se onDraw viene chiamato frequentemente (animazione, scorrimento) o contiene operazioni pesanti. Android fornisce diversi meccanismi per ottimizzare il rendering dei componenti personalizzati.
View.setLayerType consente di passare il rendering della View a un layer software (LAYER_TYPE_SOFTWARE) o hardware (LAYER_TYPE_HARDWARE). Il layer software è utile per grafica Canvas complessa non supportata dall’accelerazione hardware — ad esempio, drawTextOnPath o effetti Path complessi.
invalidate(Rect) ridisegna solo l’area specificata della View, non l’intero componente. Questo è criticamente importante per i grandi Custom View (grafica, mappe, canvas di disegno), dove il ridisegno completo di ogni frame causa cali di FPS. Usa postInvalidateOnAnimation() per la sincronizzazione con vsync.
class EfficientCustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// Oggetti creati una volta nel costruttore
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)
// Disegna solo l’area intorno al punto
rect.set(
(x - 10).toInt(),
(y - 10).toInt(),
(x + 10).toInt(),
(y + 10).toInt()
)
invalidate(rect)
}
}
Accelerazione hardware è abilitata per impostazione predefinita su Android 3.0+ (API 11+). Le operazioni Canvas drawCircle, drawRect, drawBitmap sono accelerate via hardware ed eseguite sulla GPU. Tuttavia, drawTextOnPath, drawVertices, operazioni di clipping complesse non sono accelerate — usa LAYER_TYPE_SOFTWARE per esse.
Secondo Android Performance Patterns (Google, 2023), le principali cause di rallentamento di Custom View — allocazioni in onDraw (creazione di oggetti ogni frame), ridisegno dell’intera View al cambio di un elemento e assenza di LayerType.HARDWARE per contenuti statici. Correggere questi tre problemi aumenta gli FPS da 30 a 60 nella maggior parte degli scenari.
| Metodo | Scopo | Frequenza di chiamata |
|---|---|---|
| onDraw | Disegnare il contenuto della View | A ogni invalidate() |
| onMeasure | Determinare le dimensioni della View | Al cambio di layout |
| onLayout | Posizionare le View figlie | Dopo onMeasure |
| onTouchEvent | Gestione dei tocchi | A ogni tocco |
| onSizeChanged | Reazione al cambio di dimensione | Al primo layout e cambio |
Domande frequenti
Custom View (Canvas API) è giustificato quando sono necessarie prestazioni di disegno personalizzato massime (grafica, editor video, mappe), integrazione con codice esistente basato su View o supporto per versioni Android inferiori all’API 21. Jetpack Compose è un approccio moderno per la maggior parte dei nuovi progetti, che utilizza UI dichiarativa ed è più adatto per interfacce dinamiche.
Sì, se la View utilizza wrap_content nel layout XML. Senza onMeasure, wrap_content si comporterà come match_parent perché l’implementazione predefinita di View.onMeasure non imposta una dimensione predefinita. Se la View ha sempre una dimensione fissa o match_parent, onMeasure può non essere sovrascritto.
Usa ValueAnimator o ObjectAnimator per modificare le proprietà della View (colore, raggio, posizione) e chiama invalidate() nel callback dell’animazione per ridisegnare. ValueAnimator viene eseguito sul thread principale ed è sincronizzato con vsync. Per animazioni complesse (fisica, particelle), usa Choreographer.FrameCallback o Android Animation Framework.
In onTouchEvent, usa MotionEvent.getPointerCount() per determinare il numero di dita e getPointerId(i) per tracciare ogni dito tramite ID unico. ACTION_POINTER_DOWN / ACTION_POINTER_UP — eventi di aggiunta/rimozione dito. Per il pizzicamento-zoom, usa ScaleGestureDetector in coppia con onTouchEvent — semplifica il rilevamento del pizzicamento.
Sì, tramite AndroidView — una funzione composable che incorpora un elemento basato su View nella gerarchia di Compose. AndroidView accetta una factory per creare la View e un callback update per la sincronizzazione dello stato. Ciò consente di riutilizzare Custom View esistenti in nuovi progetti Compose senza riscrittura completa.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche