View Lifecycle — sekwencja metod, które Android wywołuje w celu renderowania i przerenderowania elementu interfejsu użytkownika (View) na ekranie. W przeciwieństwie do Activity lub Fragment, View jest lekkim komponentem, który nie ma rozszerzonego cyklu życia, ale przechodzi przez ścisły trójfazowy proces: onMeasure (pomiar), onLayout (rozmieszczenie), onDraw (rysowanie). Zrozumienie View Lifecycle jest niezbędne do tworzenia niestandardowych View, optymalizacji wydajności i rozwiązywania problemów z renderowaniem. Według Google, niestandardowe View przyspieszają UI o 15–40% w porównaniu z kombinacją standardowych zagnieżdżonych ViewGroup przy prawidłowej implementacji. Dokumentacja Android dotycząca niestandardowych View opisuje onMeasure, onLayout i onDraw jako trzy filary View Lifecycle.
Najważniejsze
View Lifecycle — to proces, który Android View (i ViewGroup) przechodzi, aby wyświetlić się na ekranie. W przeciwieństwie do Activity lub Fragment, View nie ma onStart/onStop/onDestroy — jego „życie” składa się z cyklicznego procesu pomiaru, rozmieszczenia i rysowania. Cykl ten jest uruchamiany za każdym razem, gdy View musi zostać wyświetlone lub przerysowane.
Trzy fazy View Lifecycle:
Pełny cykl View Lifecycle obejmuje również metody związane z dołączaniem View do okna: onAttachedToWindow (View jest dołączone do okna, ma HW acceleration) i onDetachedFromWindow (View jest odłączone, zasoby są zwalniane). Metody te są wywoływane raz w ciągu życia View i są ważne dla rejestracji/anulowania animacji, sensorów.
Według Android Performance Blog, 65% problemów z wydajnością UI (jank, pomijanie klatek) jest związanych z nieprawidłową implementacją onMeasure i onDraw: nadmierne nadpisywanie, wywoływanie requestLayout() bez potrzeby, tworzenie obiektów w onDraw.
onMeasure — najważniejsza i najtrudniejsza faza View Lifecycle. Na tym etapie Android określa, ile miejsca View zajmie na ekranie. System przekazuje MeasureSpec — spakowane w int instrukcje składające się z trybu i rozmiaru.
Trzy tryby MeasureSpec:
| Tryb | Stała | Znaczenie | Przykład |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Dokładny rozmiar określony przez rodzica (match_parent lub stała szerokość) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View może mieć rozmiar do określonego maksimum (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Bez ograniczeń — View może mieć dowolny rozmiar (ScrollView, RecyclerView) | szerokość nieograniczona → MeasureSpec(0, UNSPECIFIED) |
Implementacja onMeasure powinna:
setMeasuredDimension(int width, int height) w celu zapisania zmierzonych rozmiarów.getPaddingLeft() + getPaddingRight() od dostępnej szerokości.measureChild() lub measureChildWithMargins().Typowy błąd: nieuwzględnianie MeasureSpec przy wrap_content. Jeśli View jest ustawione na wrap_content, ale onMeasure nie obsługuje AT_MOST i zwraca stały rozmiar, View zostanie albo przycięte, albo zajmie więcej miejsca niż potrzeba.
onLayout — faza, w której View lub ViewGroup rozmieszcza swoich potomków wewnątrz swoich granic. Dla zwykłego View (nie ViewGroup) onLayout nie jest wymagany — system sam wywołuje layout() z parametrami przekazanymi od rodzica. Dla ViewGroup onLayout jest obowiązkowy — bez niego widoki potomne nie zostaną rozmieszczone.
Sygnatura onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// rozmieszczanie widoków potomnych
}
Parametr changed wskazuje, czy pozycja lub rozmiar View zmieniły się w porównaniu z poprzednim layout. Jeśli false — View może pominąć ponowne obliczanie pozycji potomków w celu optymalizacji.
Dla ViewGroup onLayout powinien:
getChildCount() i getChildAt(i).child.layout(l, t, r, b) dla każdego potomka.onLayout jest wywoływany po onMeasure — zmierzone rozmiary są dostępne przez getMeasuredWidth()/getMeasuredHeight(). Jeśli widok potomny ma inne rzeczywiste rozmiary po layout(), wywoływana jest requestLayout() w celu ponownego pomiaru. Jest to nazywane „layout pass” i może wywołać reakcję łańcuchową przeliczeń.
onDraw — faza, w której View rysuje się na Canvas. Jest to jedyna faza, która może być wywołana wielokrotnie bez onMeasure i onLayout — jeśli View jest oznaczone jako invalidate(). Canvas udostępnia API do rysowania: drawLine, drawRect, drawCircle, drawText, drawBitmap i drawPath.
Zasady onDraw:
canvas.clipRect() do odcinania niewidocznych części.Kolejność rysowania w ViewGroup: tło (setBackgroundDrawable) → onDraw (zawartość) → dispatchDraw (widoki potomne) → onDrawForeground (pierwszy plan). dispatchDraw wywołuje onDraw każdego potomka. Nadpisywanie dispatchDraw jest używane do nakładania efektów na elementy potomne.
Według statystyk Android Vitals, najczęstsze przyczyny pomijania klatek w onDraw to tworzenie obiektów wewnątrz metody (48%), wywołanie decodeResource (22%) i złożone operacje na Path bez buforowania (15%).
Invalidation — mechanizm uruchamiający przerysowanie View. Wywołanie invalidate() oznacza View jako „brudne” i planuje wywołanie onDraw w następnym cyklu rysowania. Wywołanie requestLayout() — to bardziej „ciężka” operacja, uruchamiająca pełny cykl: onMeasure → onLayout → onDraw.
| Metoda | Co robi | Kiedy używać |
|---|---|---|
| invalidate() | Wywołuje onDraw bez onMeasure/onLayout | Zmienił się tylko wygląd (kolor, tekst, postęp) |
| invalidate(Rect) | Przerysowuje tylko wskazany obszar | Zmieniła się część View — animacja, zaznaczenie |
| postInvalidate() | Wywołuje invalidate z wątku innego niż UI | Wątek tła zaktualizował dane do rysowania |
| requestLayout() | Uruchamia onMeasure → onLayout → onDraw | Zmienił się rozmiar zawartości (tekst, obraz) |
| forceLayout() | Oznacza View do wymuszonego ponownego pomiaru | Stan wewnętrzny się zmienił, rozmiar mógł się zmienić |
Animacje a View Lifecycle: ViewPropertyAnimator i ValueAnimator wywołują invalidate() na każdej klatce animacji. ObjectAnimator wywołuje setter na View, który, jeśli setter zmienia rozmiar (width/height), automatycznie wywołuje requestLayout(). Może to być kosztowne dla złożonych ViewGroup: każda requestLayout uruchamia pełną hierarchię aż do widoku głównego.
Zasada optymalizacji: invalidate() zamiast requestLayout() wszędzie tam, gdzie zmienia się tylko wygląd (kolor, przezroczystość, obrót bez zmiany rozmiaru). requestLayout używaj tylko przy zmianie rozmiarów lub zawartości wpływającej na rozmiar.
Niestandardowe View — potężne narzędzie do tworzenia unikalnego UI, ale wymagają ścisłego przestrzegania zasad wydajności. Oto kluczowe zalecenia Google dotyczące optymalizacji View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) dla View z animacjami i setLayerType(LAYER_TYPE_NONE) po zakończeniu.Prosty okrągły wskaźnik postępu z poprawną implementacją onMeasure, onDraw i 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
}
}
Okrągły pasek postępu: onMeasure zwraca kwadratowy rozmiar na podstawie MeasureSpec, onSizeChanged zapamiętuje rozmiary, onDraw rysuje tło i łuk postępu. Invalidate jest wywoływane przy zmianie postępu — onMeasure/onLayout nie są dotknięte. Paint jest tworzony raz w konstruktorze, a nie w onDraw.
Niestandardowa ViewGroup, która rozmieszcza widoki potomne w wierszach (jak 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 nadpisuje onMeasure: mierzy każdego potomka, przenosi do nowego wiersza po przekroczeniu szerokości, oblicza całkowitą wysokość. onLayout rozmieszcza dzieci według współrzędnych z uwzględnieniem przenoszenia. generateLayoutParams zwraca MarginLayoutParams dla obsługi marginesów u widoków potomnych.
Niestandardowy View rysuje wygładzoną krzywą Beziera, obliczając Path z wyprzedzeniem i buforując go.
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)
}
}
Buforowanie Path: isPathDirty = true tylko przy zmianie rozmiarów View lub wywołaniu refreshWave(). W onDraw Path jest przeliczany tylko jeśli jest „brudny”. Zapobiega to ponownemu obliczaniu krzywej Beziera na każdej klatce animacji, oszczędzając CPU.
Często zadawane pytania
View Lifecycle — cykliczny proces rysowania (onMeasure → onLayout → onDraw), niezależny od tworzenia/niszczenia Activity. View nie ma onStart/onStop — jest albo widoczne (dołączone do okna), albo nie. Activity Lifecycle zarządza stanem komponentu aplikacji, View Lifecycle — rysowaniem UI.
requestLayout() uruchamia pełny cykl onMeasure → onLayout → onDraw dla całego drzewa View od korzenia. Jeśli requestLayout() jest wywoływany często (np. co klatkę animacji), powoduje to jank i pomijanie klatek. Według Google, jeden requestLayout zajmuje średnio 2–5 ms na ViewGroup z 10 elementów. Do animacji używaj invalidate().
onAttachedToWindow jest wywoływane, gdy View zostaje dołączone do okna (Window) — staje się częścią widocznej hierarchii. W tym momencie View otrzymuje przyspieszenie sprzętowe i dostęp do zasobów Window (WindowManager, Display). onAttachedToWindow to właściwe miejsce do rejestracji słuchaczy animacji i BroadcastReceiver, który żyje, dopóki View jest widoczne.
Overdraw — sytuacja, w której piksel jest rysowany kilka razy w jednej klatce. Każde dodatkowe przejście to strata czasu GPU. Metody redukcji: ustaw windowBackground w theme (nie rysuj tła w layout), używaj canvas.clipRect(), unikaj scalania zagnieżdżonych tła, stosuj ConstraintLayout zamiast zagnieżdżonych LinearLayout. Android Studio → Profile GPU Rendering → Overdraw pokazuje mapę kolorową overdraw (niebieski = 1x, czerwony = 3x+).
Tak, jeśli View ma tło (background). super.onDraw() rysuje tło View. Jeśli Twój niestandardowy View nie ma tła lub rysujesz własne tło, super.onDraw() można pominąć — zaoszczędzi to jedno przejście rysowania. Dla ViewGroup super.dispatchDraw() jest obowiązkowe — rysuje widoki potomne.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również