Custom View — egy egyedi felületi komponens Androidban, amely a View osztály vagy alosztályai (Button, TextView, ImageView) örökítésével és a kulcsfontosságú metódusok felülírásával jön létre: onDraw a rajzoláshoz, onMeasure a méretek meghatározásához és onTouchEvent az érintések kezeléséhez. A Google Android Developer Guide (2024) szerint a Custom View akkor használatos, amikor a szabványos Android SDK komponensek nem biztosítják a kívánt viselkedést vagy megjelenést — például egyedi animációkhoz, nem szabványos formákhoz, speciális diagramokhoz és játékelemekhez.
Főbb pontok
Custom View — egy osztály, amely az android.view.View (vagy egyik alosztálya) osztályból örököl, és felülírja a rendszermetódusokat saját rajzolási, mérési és eseménykezelési logika megvalósításához. A Custom View az Android alapvető mechanizmusa egyedi UI-komponensek létrehozásához.
Az Android két megközelítést kínál a Custom View létrehozásához: öröklés a View-ből (teljesen egyedi rajzolás onDraw segítségével) és öröklés egy meglévő View-alosztályból (például Button vagy TextView felülírása funkcionalitás hozzáadásához az alapviselkedés megtartása mellett).
Az alapkonstruktor Custom View esetén Context és AttributeSet paramétereket kell fogadnia — ez lehetővé teszi a rendszer számára, hogy View-t hozzon létre XML-kódból. Ha a View csak kódból lesz használva, a Context paraméteres konstruktor elegendő. A harmadik, stílusos konstruktor az Android témák támogatásához szükséges.
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)
}
}
Minimális Custom View egy konstruktorból és az onDraw-ból áll. Az Android Layout rendszerben való helyes működéshez azonban az onMeasure-t is felül kell írni — különben a View nulla méretekkel jelenhet meg, vagy helytelenül reagálhat a wrap_content layout paraméterekre.
A Google I/O 2023 szerint az egyedi View-k a Google Play top 100 alkalmazásának 65%-ában használatosak. A Custom View létrehozásának leggyakoribb okai: egyedi animációk (35%), egyedi grafikonok és diagramok (25%), speciális vezérlőelemek (20%) és márkázott komponensek (20%).
onMeasure — az a metódus, amelyben a View közli a rendszerrel a kívánt méreteit a kapott MeasureSpec (szülői korlátozások) alapján. Az onMeasure helyes megvalósítása nélkül a View nulla magasságú lehet wrap_content esetén, vagy a teljes képernyőt elfoglalhatja match_parent esetén.
MeasureSpec egy módból (EXACTLY, AT_MOST, UNSPECIFIED) és egy értékből áll. EXACTLY — a szülő pontos méretet adott meg (match_parent vagy rögzített érték). AT_MOST — a szülő maximumot adott meg (wrap_content). UNSPECIFIED — korlátozás nélkül (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 — egy hasznos Android segédmetódus, amely leegyszerűsíti az onMeasure megvalósítását. Fogadja a kívánt méretet és a MeasureSpec-et, és visszaadja a helyes értéket: EXACTLY esetén — pontos méret, AT_MOST esetén — a kívánt és a maximum közötti minimum, UNSPECIFIED esetén — kívánt méret.
onLayout View (nem ViewGroup) esetén általában nem kerül felülírásra — a szülő hívja meg a gyermekelemek pozicionálásához. ViewGroup esetén az onLayout kötelező — ebben kell meghívni a layout()-ot minden gyermek View-ra.
onDraw — a Custom View szíve. Ezt a metódust az Android hívja meg a View minden újrarajzolásakor. Egy Canvas objektumot kap, amelyen keresztül az összes rajzolás történik: geometriai alakzatok, szöveg, útvonalak, Bitmap és animációk.
Canvas API metódusokat biztosít az összes alapvető művelethez: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. A stílus beállításához a Paint objektumot használjuk — amely meghatározza a színt, vastagságot, kitöltési stílust, betűtípust, árnyékokat és effekteket.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Háttér
canvas.drawRect(0f, 0f,
width.toFloat(), height.toFloat(), bgPaint)
// Szöveg
canvas.drawText("Egyedi nézet",
paddingLeft.toFloat(),
paddingTop.toFloat() + textPaint.textSize,
textPaint)
// Kör
canvas.drawCircle(
width / 2f,
height / 2f,
radius, circlePaint)
// Útvonal (Bezier-görbe)
val path = Path().apply {
moveTo(0f, height.toFloat())
quadTo(width / 2f, 0f,
width.toFloat(), height.toFloat())
}
canvas.drawPath(path, pathPaint)
}
Paint — a rajzolási stílus konfigurátora. Az Anti-aliasing (Paint.ANTI_ALIAS_FLAG) kötelező a sima élekhez. A Subpixel text (SUBPIXEL_TEXT_FLAG) javítja a szöveg minőségét. A Style.FILL, STROKE és FILL_AND_STROKE határozzák meg, hogy az alakzat kitöltött, körvonalazott vagy mindkettő legyen.
Fontos: ne hozzon létre Paint, Path, Rect objektumokat az onDraw-ban — ez allokációkhoz vezet a rajzolási ciklusban, és garbage collection-t indít el, ami csökkenti az FPS-t. Hozzon létre minden rajzolási objektumot a konstruktorban vagy az init metódusban. Animációhoz használjon ValueAnimator-t vagy ObjectAnimator-t invalidate()-val az újrarajzolás elindításához.
Egyedi attribútumok lehetővé teszik a Custom View konfigurálását XML-kódból, akárcsak a szabványos android:layout_width vagy android:background attribútumok. Az attribútumok a res/values/attrs.xml fájlban deklarálódnak névvel, típussal és opcionális alapértékkel.
Attribútumtípusok: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (hivatkozás erőforrásra). Minden típus esetén az Android automatikusan elemzi az értéket az XML-ből és továbbítja a TypedArray-be.
<!-- 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>
A Custom View konstruktorában az attribútumok a context.obtainStyledAttributes segítségével olvashatók be, amely egy TypedArray-t ad vissza. A TypedArray típusos hozzáférési metódusokat biztosít: getColor, getDimension, getString, getBoolean, getInt. Olvasás után a TypedArray-n recycle()-t kell hívni az erőforrások felszabadításához.
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()
}
Használat XML-ben: adja hozzá az app névteret (xmlns:app="http://schemas.android.com/apk/res-auto") és használja az egyedi attribútumokat app:circleColor="@color/red" formában. Az Android Studio automatikusan kiegészíti és ellenőrzi az attribútumtípusokat, ha a deklaráció az attrs.xml-ben helyes.
onTouchEvent — a metódus, amely a View minden érintésekor meghívódik. Egy MotionEvent objektumot kap az esemény típusáról (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), koordinátákról, nyomásról és ujjak számáról (multitouch).
Az összetett gesztusok (swipe, nagyítás, hosszú lenyomás) kezeléséhez használjon GestureDetector-t vagy ScaleGestureDetector-t az onTouchEvent-tel kombinálva. A GestureDetector leegyszerűsíti az onSingleTapUp, onFling, onLongPress, onDoubleTap és más szabványos gesztusok felismerését.
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 — annak meghatározása, hogy az érintés a View egy adott területére esett-e. Téglalap alakú területekhez használjon Rect.contains(x, y) metódust. Körökhöz — távolság ellenőrzése a középponttól: sqrt(dx^2 + dy^2) < radius. Tetszőleges alakzatokhoz — Path.op() vagy Region.contains().
Multitouch a MotionEvent.getPointerCount() és getPointerId(index) segítségével kezelhető. Minden ujj egy egyedi ID-t kap, amely ACTION_DOWN-tól ACTION_POINTER_UP-ig megmarad. Az ACTION_MOVE az összes aktív ujj adatait tartalmazhatja — használjon getHistoricalX/Y-t a mozgás interpolálásához.
Custom View teljesítménybeli szűk keresztmetszetté válhat, ha az onDraw gyakran kerül meghívásra (animáció, görgetés) vagy nehéz műveleteket tartalmaz. Az Android több mechanizmust kínál az egyedi komponensek rajzolásának optimalizálásához.
View.setLayerType lehetővé teszi a View renderelésének átkapcsolását szoftveres (LAYER_TYPE_SOFTWARE) vagy hardveres (LAYER_TYPE_HARDWARE) rétegre. A Software layer hasznos olyan összetett Canvas grafikához, amelyet a hardveres gyorsítás nem támogat — például drawTextOnPath vagy összetett Path effektek.
invalidate(Rect) — csak a View megadott területét rajzolja újra, nem a teljes komponenst. Ez kritikus fontosságú nagy Custom View-k (grafikák, térképek, rajzvásznak) esetén, ahol minden egyes képkocka teljes újrarajzolása FPS-csökkenést okoz. A vsync-el való szinkronizáláshoz használjon postInvalidateOnAnimation()-t.
class EfficientCustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// Objektumok egyszer létrehozva a konstruktorban
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)
// Csak a pont körüli terület újrarajzolása
rect.set(
(x - 10).toInt(),
(y - 10).toInt(),
(x + 10).toInt(),
(y + 10).toInt()
)
invalidate(rect)
}
}
Hardveres gyorsítás alapértelmezetten be van kapcsolva Android 3.0+ (API 11+) verziókon. A drawCircle, drawRect, drawBitmap Canvas műveletek hardveresen gyorsítottak és a GPU-n futnak. Azonban a drawTextOnPath, drawVertices, összetett clipping műveletek nem gyorsítottak — ezekhez használjon LAYER_TYPE_SOFTWARE-t.
Az Android Performance Patterns (Google, 2023) szerint a Custom View lassúságának fő okai: allokációk az onDraw-ban (objektumok létrehozása minden képkockában), a teljes View újrarajzolása egyetlen elem változásakor és a LayerType.HARDWARE hiánya statikus tartalom esetén. E három probléma kijavítása a legtöbb forgatókönyvben 30-ról 60-ra növeli az FPS-t.
| Metódus | Rendeltetés | Hívási gyakoriság |
|---|---|---|
| onDraw | View tartalmának rajzolása | Minden invalidate() híváskor |
| onMeasure | View méreteinek meghatározása | A layout változásakor |
| onLayout | Gyermek View-k pozicionálása | onMeasure után |
| onTouchEvent | Érintések kezelése | Minden érintéskor |
| onSizeChanged | Reakció méretváltozásra | Az első layoutnál és változásnál |
Gyakran ismételt kérdések
Custom View (Canvas API) akkor indokolt, ha maximális egyedi rajzolási teljesítményre van szükség (grafikák, videószerkesztők, térképek), integrációra meglévő View-alapú kóddal, vagy API 21 alatti Android verziók támogatására. A Jetpack Compose modern megközelítés a legtöbb új projekthez, deklaratív UI-t használ, és jobban megfelel dinamikus interfészekhez.
Igen, ha a View wrap_content-et használ az XML-kódban. Az onMeasure nélkül a wrap_content match_parent-ként fog működni, mivel a View.onMeasure alapértelmezett megvalósítása nem állít be alapértelmezett méretet. Ha a View mindig rögzített méretű vagy match_parent, az onMeasure kihagyható.
Használjon ValueAnimator-t vagy ObjectAnimator-t a View tulajdonságainak (szín, sugár, pozíció) módosításához, és hívja meg az invalidate()-t az animáció callback-jében az újrarajzoláshoz. A ValueAnimator a főszálon működik és szinkronizálva van a vsync-el. Összetett animációkhoz (fizika, részecskék) használjon Choreographer.FrameCallback-t vagy Android Animation Framework-öt.
Az onTouchEvent-ben használjon MotionEvent.getPointerCount()-et az ujjak számának meghatározásához és getPointerId(i)-t az egyes ujjak egyedi ID alapján történő követéséhez. ACTION_POINTER_DOWN / ACTION_POINTER_UP — az ujj hozzáadásának/eltávolításának eseményei. Nagyításhoz használjon ScaleGestureDetector-t az onTouchEvent-tel párban — leegyszerűsíti a pinch-to-zoom érzékelését.
Igen, az AndroidView segítségével — egy composable függvény, amely View-alapú elemet illeszt a Compose hierarchiába. Az AndroidView fogad egy gyárat a View létrehozásához és egy update callback-et az állapot szinkronizálásához. Ez lehetővé teszi a meglévő Custom View-k újrafelhasználását új Compose projektekben teljes újraírás nélkül.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is