View Lifecycle — sekvence metod, které Android volá pro vykreslení a překreslení prvku uživatelského rozhraní (View) na obrazovce. Na rozdíl od Activity nebo Fragment je View lehkou komponentou, která nemá rozšířený životní cyklus, ale prochází přísným třífázovým procesem: onMeasure (měření), onLayout (umístění), onDraw (kreslení). Porozumění View Lifecycle je nezbytné pro vytváření vlastních View, optimalizaci výkonu a řešení problémů s vykreslováním. Podle Google vlastní View zrychlují UI o 15–40% ve srovnání s kombinací standardních vnořených ViewGroup při správné implementaci. Dokumentace Androidu k vlastním View popisuje onMeasure, onLayout a onDraw jako tři pilíře View Lifecycle.
Hlavní body
View Lifecycle — proces, kterým Android View (a ViewGroup) prochází, aby se zobrazilo na obrazovce. Na rozdíl od Activity nebo Fragment View nemá onStart/onStop/onDestroy — jeho „život” se skládá z cyklického procesu měření, umístění a kreslení. Tento cyklus se spouští pokaždé, když je třeba View zobrazit nebo překreslit.
Tři fáze View Lifecycle:
Plný cyklus View Lifecycle také zahrnuje metody související s připojením View k oknu: onAttachedToWindow (View je připojeno k oknu, má HW acceleration) a onDetachedFromWindow (View je odpojeno, zdroje se uvolňují). Tyto metody jsou volány jednou za život View a jsou důležité pro registraci/zrušení animací, senzorů.
Podle Android Performance Blogu 65% problémů s výkonem UI (jank, přeskakování snímků) souvisí s nesprávnou implementací onMeasure a onDraw: nadměrné přepisování, volání requestLayout() bez potřeby, vytváření objektů v onDraw.
onMeasure — nejdůležitější a nejkomplexnější fáze View Lifecycle. V této fázi Android určuje, kolik místa View na obrazovce zabere. Systém předává MeasureSpec — instrukce zabalené do int sestávající z režimu a velikosti.
Tři režimy MeasureSpec:
| Režim | Konstanta | Význam | Příklad |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Přesná velikost určená rodičem (match_parent nebo pevná šířka) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View může mít velikost až do zadaného maxima (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Bez omezení — View může mít libovolnou velikost (ScrollView, RecyclerView) | šířka neomezená → MeasureSpec(0, UNSPECIFIED) |
Implementace onMeasure by měla:
setMeasuredDimension(int width, int height) pro uložení naměřených rozměrů.getPaddingLeft() + getPaddingRight() od dostupné šířky.measureChild() nebo measureChildWithMargins().Typická chyba: nezohlednění MeasureSpec při wrap_content. Pokud je View nastaveno na wrap_content, ale onMeasure nezpracovává AT_MOST a vrací pevnou velikost, View bude buď oříznuto, nebo zabere více místa, než je potřeba.
onLayout — fáze, ve které View nebo ViewGroup rozmísťuje své potomky v rámci svých hranic. Pro běžné View (ne ViewGroup) není onLayout vyžadován — systém sám volá layout() s parametry předanými od rodiče. Pro ViewGroup je onLayout povinný — bez něj nebudou podřízené View umístěny.
Signatura onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// rozmístění podřízených View
}
Parametr changed udává, zda se pozice nebo velikost View změnila oproti předchozímu layout. Pokud false — View může přeskočit přepočet pozic potomků pro optimalizaci.
Pro ViewGroup by onLayout měl:
getChildCount() a getChildAt(i).child.layout(l, t, r, b) pro každého potomka.onLayout je volán po onMeasure — naměřené rozměry jsou dostupné přes getMeasuredWidth()/getMeasuredHeight(). Pokud má podřízené View po layout() jiné skutečné rozměry, zavolá se requestLayout() pro přeměření. Tomu se říká „layout pass” a může vyvolat řetězovou reakci přepočtů.
onDraw — fáze, ve které View kreslí sebe na Canvas. Je to jediná fáze, která může být volána vícekrát bez onMeasure a onLayout — pokud je View označeno jako invalidate(). Canvas poskytuje API pro kreslení: drawLine, drawRect, drawCircle, drawText, drawBitmap a drawPath.
Pravidla onDraw:
canvas.clipRect() pro oříznutí neviditelných částí.Pořadí kreslení v ViewGroup: pozadí (setBackgroundDrawable) → onDraw (obsah) → dispatchDraw (podřízené View) → onDrawForeground (popředí). dispatchDraw volá onDraw každého potomka. Přepsání dispatchDraw se používá pro aplikaci efektů přes podřízené prvky.
Podle statistik Android Vitals nejčastější příčiny přeskakování snímků v onDraw — vytváření objektů uvnitř metody (48%), volání decodeResource (22%) a složité operace s Path bez cache (15%).
Invalidation — mechanismus, který spouští překreslení View. Volání invalidate() označí View jako „špinavé” a naplánuje volání onDraw v dalším cyklu kreslení. Volání requestLayout() — těžší operace, která spouští plný cyklus: onMeasure → onLayout → onDraw.
| Metoda | Co dělá | Kdy použít |
|---|---|---|
| invalidate() | Volá onDraw bez onMeasure/onLayout | Změnil se pouze vzhled (barva, text, průběh) |
| invalidate(Rect) | Překresluje pouze zadanou oblast | Změnila se část View — animace, výběr |
| postInvalidate() | Volá invalidate z ne-UI vlákna | Vlákno na pozadí aktualizovalo data pro kreslení |
| requestLayout() | Spouští onMeasure → onLayout → onDraw | Změnila se velikost obsahu (text, obrázek) |
| forceLayout() | Označí View pro vynucené přeměření | Změnil se vnitřní stav, velikost se mohla změnit |
Animace a View Lifecycle: ViewPropertyAnimator a ValueAnimator volají invalidate() na každém snímku animace. ObjectAnimator volá setter na View, který pokud setter mění velikost (width/height), automaticky volá requestLayout(). To může být nákladné pro složité ViewGroup: každý requestLayout spouští celou hierarchii až k root view.
Pravidlo optimalizace: invalidate() místo requestLayout() všude, kde se mění pouze vzhled (barva, průhlednost, rotace bez změny velikosti). requestLayout používejte pouze při změně rozměrů nebo obsahu ovlivňujícího velikost.
Vlastní View — mocný nástroj pro vytváření jedinečného UI, ale vyžadují přísné dodržování pravidel výkonu. Zde jsou klíčová doporučení Google pro optimalizaci View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) pro View s animacemi a setLayerType(LAYER_TYPE_NONE) po dokončení.Jednoduchý kruhový indikátor průběhu se správnou implementací onMeasure, onDraw a invalidate.
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
}
}
Kruhový indikátor průběhu: onMeasure vrací čtvercovou velikost na základě MeasureSpec, onSizeChanged si pamatuje rozměry, onDraw kreslí pozadí a oblouk průběhu. Invalidate je volán při změně průběhu — onMeasure/onLayout nejsou ovlivněny. Paint je vytvořen jednou v konstruktoru, ne v onDraw.
Vlastní ViewGroup, která rozmísťuje podřízené View do řádků (jako 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)
}
}
FlowLayout přepisuje onMeasure: měří každého potomka, přesouvá na nový řádek při překročení šířky, vypočítá celkovou výšku. onLayout rozmísťuje děti podle souřadnic s ohledem na zalomení řádků. generateLayoutParams vrací MarginLayoutParams pro podporu okrajů u podřízených View.
Vlastní View kreslí hladkou Bézierovu křivku, předem vypočítává Path a kešuje jej.
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)
}
}
Kešování Path: isPathDirty = true pouze při změně rozměrů View nebo volání refreshWave(). V onDraw je Path přepočítán pouze pokud je „špinavý”. To zabraňuje přepočtu Bézierovy křivky na každém snímku animace, šetří CPU.
Často kladené otázky
View Lifecycle — cyklický proces kreslení (onMeasure → onLayout → onDraw), nezávislý na vytváření/ničení Activity. View nemá onStart/onStop — je buď viditelné (připojené k oknu), nebo není. Activity Lifecycle spravuje stav komponenty aplikace, View Lifecycle — kreslení UI.
requestLayout() spouští plný cyklus onMeasure → onLayout → onDraw pro celý strom View od kořene. Pokud je requestLayout() volán často (např. každý snímek animace), způsobuje jank a přeskakování snímků. Podle Google trvá jeden requestLayout v průměru 2–5 ms na ViewGroup s 10 prvky. Pro animace používejte invalidate().
onAttachedToWindow je voláno, když je View připojeno k oknu (Window) — stává se součástí viditelné hierarchie. V tomto okamžiku View získává HW akceleraci a přístup ke zdrojům okna (WindowManager, Display). onAttachedToWindow je správné místo pro registraci posluchačů animací a BroadcastReceiver, který žije, dokud je View viditelné.
Overdraw — situace, kdy je pixel kreslen několikrát v jednom snímku. Každý průchod navíc je ztráta GPU času. Metody snížení: nastavte windowBackground v theme (nekreslete pozadí v layout), použijte canvas.clipRect(), vyhněte se slučování vnořených pozadí, aplikujte ConstraintLayout místo vnořených LinearLayout. Android Studio → Profile GPU Rendering → Overdraw zobrazuje barevnou mapu overdraw (modrá = 1x, červená = 3x+).
Ano, pokud má View pozadí (background). super.onDraw() kreslí pozadí View. Pokud vaše vlastní View nemá pozadí nebo kreslíte vlastní pozadí, super.onDraw() lze vynechat — ušetří to jeden průchod kreslení. Pro ViewGroup je super.dispatchDraw() povinný — kreslí podřízené View.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také