View Lifecycle — azoknak a metódusoknak a sorozata, amelyeket az Android meghív a felhasználói felület elemének (View) megjelenítéséhez és újramegjelenítéséhez a képernyőn. Az Activity-től vagy Fragment-től eltérően a View egy könnyű komponens, amelynek nincs kiterjesztett életciklusa, de szigorú háromfázisú folyamaton megy keresztül: onMeasure (mérés), onLayout (elhelyezés), onDraw (rajzolás). A View Lifecycle megértése szükséges az egyedi View-ok létrehozásához, a teljesítmény optimalizálásához és a megjelenítési problémák megoldásához. A Google adatai szerint az egyedi View-ok 15–40%-kal gyorsítják az UI-t a szabványos beágyazott ViewGroup kombinációhoz képest helyes implementáció esetén. Az Android dokumentációja az egyedi View-okról az onMeasure, onLayout és onDraw metódusokat írja le a View Lifecycle három pilléreként.
Főbb pontok
View Lifecycle — az a folyamat, amelyen az Android View (és ViewGroup) átesik, hogy megjelenítse magát a képernyőn. Az Activity-től vagy Fragment-től eltérően a View-nak nincs onStart/onStop/onDestroy — az „élete” a mérés, elhelyezés és rajzolás ciklikus folyamatából áll. Ez a ciklus minden alkalommal elindul, amikor a View-t meg kell jeleníteni vagy újra kell rajzolni.
A View Lifecycle három fázisa:
A View Lifecycle teljes ciklusa tartalmazza a View ablakhoz csatolásával kapcsolatos metódusokat is: onAttachedToWindow (a View az ablakhoz van csatolva, HW gyorsítással rendelkezik) és onDetachedFromWindow (a View le van választva, az erőforrások felszabadulnak). Ezek a metódusok egyszer hívódnak meg a View élete során, és fontosak az animációk, érzékelők regisztrálásához/lemondásához.
Az Android Performance Blog szerint az UI teljesítményproblémák (jank, képkocka kihagyás) 65%-a az onMeasure és onDraw helytelen megvalósításához kapcsolódik: túlzott felülírás, szükségtelen requestLayout() hívás, objektumok létrehozása az onDraw-ban.
onMeasure — a View Lifecycle legfontosabb és legösszetettebb fázisa. Ebben a szakaszban az Android meghatározza, mennyi helyet foglal el a View a képernyőn. A rendszer átadja a MeasureSpec-et — int-be csomagolt utasításokat, amelyek módból és méretből állnak.
A MeasureSpec három módja:
| Mód | Konstans | Jelentés | Példa |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Pontos méret, amelyet a szülő határozott meg (match_parent vagy fix szélesség) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | A View a megadott maximumig terjedő méretű lehet (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Korlátozás nélkül — a View bármekkora lehet (ScrollView, RecyclerView) | szélesség korlátlan → MeasureSpec(0, UNSPECIFIED) |
Az onMeasure megvalósításának:
setMeasuredDimension(int width, int height) metódust a mért méretek mentéséhez.getPaddingLeft() + getPaddingRight() értéket a rendelkezésre álló szélességből.measureChild() vagy measureChildWithMargins() segítségével.Tipikus hiba: a MeasureSpec figyelmen kívül hagyása wrap_content esetén. Ha a View wrap_content-re van állítva, de az onMeasure nem dolgozza fel az AT_MOST értéket és fix méretet ad vissza, a View vagy levágódik, vagy több helyet foglal el, mint amennyi szükséges.
onLayout — az a fázis, amelyben a View vagy ViewGroup elrendezi leszármazottait a saját határain belül. Egy szokásos View (nem ViewGroup) esetén az onLayout nem szükséges — a rendszer maga hívja meg a layout()-ot a szülőtől kapott paraméterekkel. ViewGroup esetén az onLayout kötelező — nélküle a gyermek View-ok nem lesznek elrendezve.
Az onLayout szignatúrája:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// gyermek View-ok elrendezése
}
A changed paraméter jelzi, hogy a View pozíciója vagy mérete megváltozott-e az előző layout-hoz képest. Ha false — a View kihagyhatja a leszármazottak pozícióinak újraszámítását optimalizálás céljából.
ViewGroup esetén az onLayout-nak:
getChildCount() és getChildAt(i) segítségével.child.layout(l, t, r, b) metódust minden leszármazott számára.Az onLayout az onMeasure után hívódik meg — a mért méretek a getMeasuredWidth()/getMeasuredHeight() segítségével érhetők el. Ha egy gyermek View-nak a layout() után eltérő valós méretei vannak, a requestLayout() meghívódik az újraméréshez. Ezt „layout pass”-nak nevezik, és az újraszámítások láncreakcióját okozhatja.
onDraw — az a fázis, amelyben a View lerajzolja magát a Canvas-ra. Ez az egyetlen fázis, amely többször is meghívható onMeasure és onLayout nélkül — ha a View invalidate()-ként van megjelölve. A Canvas API-t biztosít a rajzoláshoz: drawLine, drawRect, drawCircle, drawText, drawBitmap és drawPath.
Az onDraw szabályai:
canvas.clipRect() metódust a nem látható részek levágásához.A rajzolás sorrendje ViewGroup-ban: háttér (setBackgroundDrawable) → onDraw (tartalom) → dispatchDraw (gyermek View-ok) → onDrawForeground (előtér). A dispatchDraw meghívja minden leszármazott onDraw-ját. A dispatchDraw felülírását arra használják, hogy effekteket alkalmazzanak a gyermek elemek felett.
Az Android Vitals statisztikái szerint a kieső képkockák leggyakoribb okai az onDraw-ban — objektumok létrehozása a metóduson belül (48%), a decodeResource hívása (22%), és összetett Path műveletek gyorsítótár nélkül (15%).
Invalidation — a View újrarajzolását elindító mechanizmus. A invalidate() hívása „piszkosnak” jelöli a View-t, és ütemezi az onDraw meghívását a következő rajzolási ciklusban. A requestLayout() hívása — egy nehezebb művelet, amely elindítja a teljes ciklust: onMeasure → onLayout → onDraw.
| Metódus | Mit csinál | Mikor használja |
|---|---|---|
| invalidate() | Meghívja az onDraw-t onMeasure/onLayout nélkül | Csak a megjelenés változott (szín, szöveg, folyamat) |
| invalidate(Rect) | Csak a megadott területet rajzolja újra | A View egy része változott — animáció, kijelölés |
| postInvalidate() | Meghívja az invalidate-et nem UI szálból | A háttérszál frissítette a rajzolási adatokat |
| requestLayout() | Elindítja az onMeasure → onLayout → onDraw ciklust | A tartalom mérete változott (szöveg, kép) |
| forceLayout() | Kényszerített újramérésre jelöli a View-t | A belső állapot változott, a méret változhatott |
Animációk és View Lifecycle: A ViewPropertyAnimator és a ValueAnimator minden animációs képkockán meghívja az invalidate()-et. Az ObjectAnimator meghívja a setter-t a View-n, amely ha a setter megváltoztatja a méretet (width/height), automatikusan meghívja a requestLayout()-ot. Ez költséges lehet összetett ViewGroup-ok esetén: minden requestLayout elindítja a teljes hierarchiát a gyökér view-ig.
Optimalizálási szabály: invalidate() a requestLayout() helyett mindenhol, ahol csak a megjelenés változik (szín, átlátszóság, méretváltozás nélküli forgatás). A requestLayout-ot csak a méretek vagy a méretet befolyásoló tartalom változásakor használja.
Az egyedi View-ok — hatékony eszköz egyedi UI létrehozásához, de szigorúan be kell tartaniuk a teljesítményre vonatkozó szabályokat. Íme a Google legfontosabb ajánlásai a View Lifecycle optimalizálásához.
setLayerType(LAYER_TYPE_HARDWARE) beállítást animációkkal rendelkező View-okhoz, és a setLayerType(LAYER_TYPE_NONE) beállítást a befejezés után.Egyszerű kör alakú folyamatjelző az onMeasure, onDraw és invalidate helyes megvalósításával.
class CircularProgressView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
style = Paint.Style.STROKE
strokeWidth = 8f
strokeCap = Paint.Cap.ROUND
}
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.LTGRAY
style = Paint.Style.STROKE
strokeWidth = 8f
}
private var progress = 0f
private var viewWidth = 0
private var viewHeight = 0
fun setProgress(value: Float) {
progress = value.coerceIn(0f, 100f)
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 100 * resources.displayMetrics.density.toInt()
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
val size = minOf(width, height).coerceAtLeast(desiredSize)
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val padding = progressPaint.strokeWidth / 2
val radius = (minOf(viewWidth, viewHeight) - padding) / 2
val cx = viewWidth / 2f
val cy = viewHeight / 2f
canvas.drawCircle(cx, cy, radius, backgroundPaint)
val sweepAngle = (progress / 100f) * 360f
canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
-90f, sweepAngle, false, progressPaint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
}
}
Kör alakú folyamatjelző sáv: az onMeasure négyzet alakú méretet ad vissza a MeasureSpec alapján, az onSizeChanged megjegyzi a méreteket, az onDraw megrajzolja a hátteret és a folyamatívet. Az Invalidate a folyamat változásakor hívódik meg — az onMeasure/onLayout nem érintett. A Paint egyszer, a konstruktorban jön létre, nem az onDraw-ban.
Egyedi ViewGroup, amely a gyermek View-okat sorokba rendezi (mint a Flexbox wrap).
class FlowLayout constructor(
context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {
private val horizontalSpacing = 8.dpToPx(resources)
private val verticalSpacing = 8.dpToPx(resources)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = MeasureSpec.getSize(widthMeasureSpec)
var totalHeight = paddingTop + paddingBottom
var rowWidth = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
if (rowWidth + child.measuredWidth > width - paddingRight) {
totalHeight += rowHeight + verticalSpacing
rowWidth = paddingLeft
rowHeight = 0
}
rowWidth += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
totalHeight += rowHeight
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean,
l: Int, t: Int, r: Int, b: Int) {
var rowTop = paddingTop
var rowLeft = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (rowLeft + child.measuredWidth > r - paddingRight) {
rowTop += rowHeight + verticalSpacing
rowLeft = paddingLeft
rowHeight = 0
}
child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
rowLeft += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}
A FlowLayout felülírja az onMeasure-t: megméri minden leszármazottat, új sorba lép a szélesség túllépésekor, kiszámítja a teljes magasságot. Az onLayout elrendezi a gyermekeket a koordináták szerint, figyelembe véve a sortöréseket. A generateLayoutParams MarginLayoutParams-t ad vissza a gyermek View-ok margóinak támogatásához.
Az egyedi View sima Bezier-görbét rajzol, előre kiszámítva és gyorsítótárazva a Path-et.
class WaveView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#4A90D9")
style = Paint.Style.FILL
}
private val wavePath = Path()
private var isPathDirty = true
private var viewWidth = 0
private var viewHeight = 0
fun refreshWave() {
isPathDirty = true
invalidate()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
isPathDirty = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (isPathDirty) {
wavePath.reset()
val amplitude = viewHeight * 0.1f
wavePath.moveTo(0f, viewHeight * 0.5f)
for (x in 0..viewWidth step 4) {
val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
wavePath.lineTo(x.toFloat(), y)
}
wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
wavePath.lineTo(0f, viewHeight.toFloat())
wavePath.close()
isPathDirty = false
}
canvas.drawPath(wavePath, wavePaint)
}
}
Path gyorsítótárazás: az isPathDirty = true csak a View méreteinek változásakor vagy a refreshWave() meghívásakor. Az onDraw-ban a Path csak akkor számítódik újra, ha „piszkos”. Ez megakadályozza a Bezier-görbe újraszámítását minden animációs képkockán, CPU-t takarítva meg.
Gyakran ismételt kérdések
View Lifecycle — ciklikus rajzolási folyamat (onMeasure → onLayout → onDraw), amely független az Activity létrehozásától/megsemmisítésétől. A View-nak nincs onStart/onStop — vagy látható (az ablakhoz csatolva), vagy nem. Az Activity Lifecycle az alkalmazáskomponens állapotát kezeli, a View Lifecycle — az UI rajzolását.
requestLayout() elindítja a teljes onMeasure → onLayout → onDraw ciklust a teljes View-fa számára a gyökértől kezdve. Ha a requestLayout() gyakran hívódik (pl. minden animációs képkockán), ez jank-ot és kieső képkockákat okoz. A Google szerint egy requestLayout átlagosan 2–5 ms-ig tart egy 10 elemből álló ViewGroup esetén. Animációkhoz használja az invalidate()-et.
onAttachedToWindow akkor hívódik meg, amikor a View csatlakozik az ablakhoz (Window) — a látható hierarchia részévé válik. Ekkor a View HW gyorsítást és hozzáférést kap a Window erőforrásaihoz (WindowManager, Display). Az onAttachedToWindow a megfelelő hely az animációfigyelők és a BroadcastReceiver regisztrálására, amely addig él, amíg a View látható.
Overdraw — az a helyzet, amikor egy pixelt többször rajzolnak meg egyetlen képkockán belül. Minden további áthaladás GPU-idő pazarlás. Csökkentési módszerek: állítsa be a windowBackground értéket a theme-ben (ne rajzoljon hátteret a layout-ban), használja a canvas.clipRect() metódust, kerülje a beágyazott hátterek összeolvadását, alkalmazza a ConstraintLayout-ot a beágyazott LinearLayout helyett. Android Studio → Profile GPU Rendering → Overdraw megmutatja az overdraw színtérképét (kék = 1x, piros = 3x+).
Igen, ha a View-nak van háttere (background). A super.onDraw() megrajzolja a View hátterét. Ha az egyedi View-nak nincs háttere, vagy saját hátteret rajzol, a super.onDraw() elhagyható — ez egy rajzolási áthaladást takarít meg. ViewGroup esetén a super.dispatchDraw() kötelező — ez rajzolja meg a gyermek View-okat.
Ö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