View Lifecycle — co to jest, procesy onMeasure onLayout onDraw

Autor: IT Sectr Opublikowano: 2026-03-05 Czas czytania: 10 min

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 składa się z trzech faz: onMeasure (rozmiary), onLayout (pozycje), onDraw (rysowanie) — i jest uruchamiany przy invalidate() lub requestLayout().
  • onMeasure oblicza szerokość i wysokość View na podstawie MeasureSpec (AT_MOST, EXACTLY, UNSPECIFIED).
  • onLayout rozmieszcza widoki potomne wewnątrz ViewGroup, określając ich współrzędne left, top, right, bottom.
  • onDraw renderuje zawartość View na Canvas: tło, tekst, kształty, obrazy.
  • Nieprawidłowy View Lifecycle — główna przyczyna problemów z wydajnością UI (jank, dropped frames) i hierarchią.

View Lifecycle — co to jest w Android

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:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — określa pożądane rozmiary View. System przekazuje MeasureSpec — instrukcję dotyczącą dozwolonych rozmiarów (dokładna wartość, maksimum lub bez ograniczeń).
  • onLayout(boolean changed, int left, int top, int right, int bottom) — rozmieszcza View i jego potomków na ekranie. Dla View określa własne granice, dla ViewGroup — pozycje elementów potomnych.
  • onDraw(Canvas canvas) — rysuje zawartość View na przekazanym Canvas. System udostępnia Canvas, który tłumaczy polecenia na bitmapę lub GPU.

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: pomiar rozmiarów View

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:

TrybStałaZnaczeniePrzykład
EXACTLYMeasureSpec.EXACTLYDokładny rozmiar określony przez rodzica (match_parent lub stała szerokość)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTView może mieć rozmiar do określonego maksimum (wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIEDBez ograniczeń — View może mieć dowolny rozmiar (ScrollView, RecyclerView)szerokość nieograniczona → MeasureSpec(0, UNSPECIFIED)

Implementacja onMeasure powinna:

  • Wywołać setMeasuredDimension(int width, int height) w celu zapisania zmierzonych rozmiarów.
  • Uwzględnić padding — odjąć getPaddingLeft() + getPaddingRight() od dostępnej szerokości.
  • Dla ViewGroup — zmierzyć wszystkich potomków przez measureChild() lub measureChildWithMargins().
  • Dla wrap_content — obliczyć rozmiar na podstawie zawartości (tekstu, obrazu).
  • Nie wywoływać requestLayout() wewnątrz onMeasure — spowoduje to nieskończoną pętlę.

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: rozmieszczenie View na ekranie

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:

java
@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:

  • Przejrzeć wszystkich potomków przez getChildCount() i getChildAt(i).
  • Dla każdego potomka określić left, top, right, bottom — współrzędne wewnątrz ViewGroup (z uwzględnieniem padding).
  • Wywołać child.layout(l, t, r, b) dla każdego potomka.
  • Uwzględnić gravity, marginesy, alignment.

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: rysowanie zawartości Canvas

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:

  • Nie twórz obiektów w onDraw — każde wywołanie onDraw powinno używać wcześniej utworzonych obiektów (Path, Paint, Rect). Tworzenie obiektów w onDraw powoduje przerwy GC i pomijanie klatek.
  • Nie wywołuj requestLayout() ani invalidate() wewnątrz onDraw — uruchomi to nieskończoną pętlę przerysowywania.
  • Nie wykonuj długich obliczeń — onDraw jest wykonywany na wątku UI. Złożone obliczenia powinny być przeniesione do wątku tła lub wstępnie obliczone.
  • Używaj Hardware Acceleration — od API 14+ Canvas może działać przez GPU. Dla złożonej grafiki (gradienty, cienie, obroty) przyspieszenie sprzętowe daje wzrost wydajności do 300%.
  • Rysuj tylko widoczny obszar — używaj 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: kiedy View jest przerysowywane

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.

MetodaCo robiKiedy używać
invalidate()Wywołuje onDraw bez onMeasure/onLayoutZmienił się tylko wygląd (kolor, tekst, postęp)
invalidate(Rect)Przerysowuje tylko wskazany obszarZmieniła się część View — animacja, zaznaczenie
postInvalidate()Wywołuje invalidate z wątku innego niż UIWątek tła zaktualizował dane do rysowania
requestLayout()Uruchamia onMeasure → onLayout → onDrawZmienił się rozmiar zawartości (tekst, obraz)
forceLayout()Oznacza View do wymuszonego ponownego pomiaruStan 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.

Optymalizacja niestandardowych View: najlepsze praktyki

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.

  • Wstępnie obliczaj wszystko, co można obliczyć — rozmiary, współrzędne, ścieżkę Path, kolory gradientu. W onDraw używaj tylko rysowania.
  • Buforuj wynik pomiaru — jeśli View ma stałe rozmiary, zapisz MeasureSpec i zwracaj setMeasuredDimension bez dodatkowych obliczeń.
  • Używaj ViewConfiguration — getScaledTouchSlop, getScaledMinimumFlingVelocity — do obsługi dotknięć.
  • Minimalizuj liczbę View w hierarchii — niestandardowe View łączące wiele elementów są zawsze szybsze niż ViewGroup z 3–5 zagnieżdżonymi View. Google zaleca nie więcej niż 10 zagnieżdżonych View na jednym ekranie.
  • Używaj ConstraintLayout dla płaskiej hierarchii — buduje pojedynczą ViewGroup z wydajnością zbliżoną do RelativeLayout, ale bez zagnieżdżania.
  • Wyłączaj warstwę sprzętową przy przerysowywaniu — używaj setLayerType(LAYER_TYPE_HARDWARE) dla View z animacjami i setLayerType(LAYER_TYPE_NONE) po zakończeniu.
  • Używaj invalidate() z Rect — przerysowuj tylko zmieniony obszar, a nie całe View.
  • Unikaj overdraw — używaj Profile GPU Rendering w Android Studio do wykrywania zbędnych rysowań. Średni overdraw dla aplikacji Google — 1.5x, maksymalny zalecany — 2.5x.

Przykłady kodu View w Kotlin

Przykład 1: Niestandardowy View — wskaźnik postępu

Prosty okrągły wskaźnik postępu z poprawną implementacją onMeasure, onDraw i invalidate.

kotlin
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.

Przykład 2: ViewGroup — prosty FlowLayout

Niestandardowa ViewGroup, która rozmieszcza widoki potomne w wierszach (jak Flexbox wrap).

kotlin
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.

Przykład 3: onDraw z buforowaniem Path

Niestandardowy View rysuje wygładzoną krzywą Beziera, obliczając Path z wyprzedzeniem i buforując go.

kotlin
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

Czym View Lifecycle różni się od Activity Lifecycle?

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.

Jaki wpływ na wydajność ma requestLayout()?

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().

Kiedy wywoływane jest onAttachedToWindow?

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.

Czym jest overdraw i jak go zmniejszyć?

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+).

Czy potrzebny jest super.onDraw() w niestandardowym View?

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

  • View Lifecycle — trzy fazy rysowania: onMeasure (rozmiary), onLayout (pozycja), onDraw (rysowanie).
  • onMeasure przetwarza MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) i wywołuje setMeasuredDimension.
  • onLayout w ViewGroup rozmieszcza widoki potomne z uwzględnieniem współrzędnych left/top/right/bottom.
  • onDraw rysuje zawartość na Canvas — nie twórz obiektów wewnątrz tej metody.
  • invalidate() wywołuje tylko onDraw, requestLayout() — pełny cykl onMeasure → onLayout → onDraw.
  • Niestandardowe View przyspieszają UI o 15–40%, ale wymagają prawidłowej implementacji onMeasure i buforowania obiektów w onDraw.
  • Do złożonej grafiki używaj Hardware Acceleration i buforuj Path/Bitmap.

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.

Omów projekt

Przeczytaj również