View Lifecycle — шта је то, процеси onMeasure onLayout onDraw

Аутор: IT Sectr Објављено: 2026-03-05 Време читања: 10 мин

View Lifecycle — низ метода које Android позива за приказивање и поновно приказивање елемента корисничког интерфејса (View) на екрану. За разлику од Activity или Fragment, View је лагана компонента која нема проширени животни циклус, али пролази кроз строги трофазни процес: onMeasure (мерење), onLayout (распоређивање), onDraw (цртање). Разумевање View Lifecycle-а је неопходно за креирање прилагођених View-ова, оптимизацију перформанси и решавање проблема са приказивањем. Према Google-у, прилагођени View-ови убрзавају UI за 15–40% у поређењу са комбинацијом стандардних угњеждених ViewGroup-ова уз правилну имплементацију. Документација Android-а о прилагођеним View-овима описује onMeasure, onLayout и onDraw као три стуба View Lifecycle-а.

Главно

  • View Lifecycle се састоји од три фазе: onMeasure (димензије), onLayout (позиције), onDraw (цртање) — и покреће се при invalidate() или requestLayout().
  • onMeasure израчунава ширину и висину View-а на основу MeasureSpec-а (AT_MOST, EXACTLY, UNSPECIFIED).
  • onLayout распоређује подређене View-ове унутар ViewGroup-а, одређујући њихове координате left, top, right, bottom.
  • onDraw приказује садржај View-а на Canvas-у: позадину, текст, облике, слике.
  • Нетачан View Lifecycle — главни узрок проблема са перформансама UI-ја (jank, dropped frames) и хијерархијом.

View Lifecycle — шта је то у Android-у

View Lifecycle — процес кроз који Android View (и ViewGroup) пролази да би се приказао на екрану. За разлику од Activity или Fragment, View нема onStart/onStop/onDestroy — његов „живот” се састоји од цикличног процеса мерења, распоређивања и цртања. Овај циклус се покреће сваки пут када View треба да се прикаже или поново нацрта.

Три фазе View Lifecycle-а:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — одређује жељене димензије View-а. Систем прослеђује MeasureSpec — упутство о дозвољеним димензијама (тачна вредност, максимум или без ограничења).
  • onLayout(boolean changed, int left, int top, int right, int bottom) — распоређује View и његове потомке на екрану. За View одређује сопствене границе, за ViewGroup — позиције подређених елемената.
  • onDraw(Canvas canvas) — црта садржај View-а на прослеђеном Canvas-у. Систем обезбеђује Canvas који преводи команде у bitmap или GPU.

Пун циклус View Lifecycle-а такође укључује методе везане за причвршћивање View-а за прозор: onAttachedToWindow (View је причвршћен за прозор, има HW acceleration) и onDetachedFromWindow (View је одвојен, ресурси се ослобађају). Ове методе се позивају једном у животу View-а и важне су за регистрацију/отказивање анимација, сензора.

Према Android Performance Blog-у, 65% проблема са перформансама UI-ја (jank, прескакање кадрова) повезано је са неправилном имплементацијом onMeasure и onDraw: претерано преоптерећење, позивање requestLayout() без потребе, креирање објеката у onDraw.

onMeasure: мерење димензија View-а

onMeasure — најважнија и најсложенија фаза View Lifecycle-а. У овој фази Android одређује колико простора ће View заузети на екрану. Систем прослеђује MeasureSpec — упакована упутства у int, која се састоје од режима и величине.

Три режима MeasureSpec-а:

РежимКонстантаЗначењеПример
EXACTLYMeasureSpec.EXACTLYТачна величина коју је одредио родитељ (match_parent или фиксна ширина)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTView може бити величине до наведеног максимума (wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIEDБез ограничења — View може бити било које величине (ScrollView, RecyclerView)ширина неограничена → MeasureSpec(0, UNSPECIFIED)

Имплементација onMeasure-а треба:

  • Да позове setMeasuredDimension(int width, int height) за чување измерених димензија.
  • Да узме у обзир padding — одузме getPaddingLeft() + getPaddingRight() од доступне ширине.
  • За ViewGroup — да измери све потомке кроз measureChild() или measureChildWithMargins().
  • За wrap_content — да израчуна величину према садржају (тексту, слици).
  • Да не позива requestLayout() унутар onMeasure-а — то ће изазвати бесконачну петљу.

Типична грешка: не узимање у обзир MeasureSpec-а при wrap_content-у. Ако је View подешен на wrap_content, али onMeasure не обрађује AT_MOST и враћа фиксну величину, View ће бити или исечен или ће заузети више простора него што је потребно.

onLayout: распоређивање View-а на екрану

onLayout — фаза у којој View или ViewGroup распоређује своје потомке унутар својих граница. За обичан View (не ViewGroup) onLayout није потребан — систем сам позива layout() са параметрима прослеђеним од родитеља. За ViewGroup onLayout је обавезан — без њега подређени View-ови неће бити распоређени.

Сигнатура onLayout-а:

java
@Override
protected void onLayout(boolean changed,
        int left, int top,
        int right, int bottom) {
    // распоређивање подређених View-ова
}

Параметар changed указује да ли се позиција или величина View-а променила у односу на претходни layout. Ако је false — View може да прескочи поновно израчунавање позиција потомака ради оптимизације.

За ViewGroup onLayout треба:

  • Да прегледа све потомке кроз getChildCount() и getChildAt(i).
  • За сваког потомка одреди left, top, right, bottom — координате унутар ViewGroup-а (са padding-ом).
  • Да позове child.layout(l, t, r, b) за сваког потомка.
  • Да узме у обзир gravity, маргине, поравнање.

onLayout се позива после onMeasure-а — измерене димензије су доступне преко getMeasuredWidth()/getMeasuredHeight(). Ако подређени View има другачије стварне димензије после layout()-а, позваће се requestLayout() ради поновног мерења. Ово се назива „layout pass” и може изазвати ланчану реакцију поновних израчунавања.

onDraw: цртање садржаја Canvas-а

onDraw — фаза у којој View црта себе на Canvas-у. Ово је једина фаза која може бити позвана више пута без onMeasure и onLayout — ако је View означен као invalidate(). Canvas пружа API за цртање: drawLine, drawRect, drawCircle, drawText, drawBitmap и drawPath.

Правила onDraw-а:

  • Не креирајте објекте у onDraw-у — сваки позив onDraw-а треба да користи унапред креиране објекте (Path, Paint, Rect). Креирање објеката у onDraw-у изазива GC паузе и прескакање кадрова.
  • Не позивајте requestLayout() или invalidate() унутар onDraw-а — то ће покренути бесконачну петљу поновног цртања.
  • Не обављајте дуга израчунавања — onDraw се извршава на UI нити. Сложена израчунавања треба преместити у позадинску нит или унапред израчунати.
  • Користите Hardware Acceleration — од API 14+ Canvas може радити преко GPU-а. За сложену графику (градијенти, сенке, ротације) HW убрзање даје повећање перформанси до 300%.
  • Цртајте само видљиву област — користите canvas.clipRect() за одсецање невидљивих делова.

Редослед цртања у ViewGroup-у: позадина (setBackgroundDrawable) → onDraw (садржај) → dispatchDraw (подређени View-ови) → onDrawForeground (предњи план). dispatchDraw позива onDraw сваког потомка. Преоптерећење dispatchDraw-а се користи за наношење ефеката преко подређених елемената.

Према статистици Android Vitals-а, најчешћи узроци прескакања кадрова у onDraw-у — креирање објеката унутар метода (48%), позив decodeResource (22%) и сложене операције са Path-ом без кеширања (15%).

Invalidation: када се View поново приказује

Invalidation — механизам који покреће поновно цртање View-а. Позив invalidate() означава View као „прљав” и планира позив onDraw-а у следећем циклусу цртања. Позив requestLayout() — тежа операција која покреће пун циклус: onMeasure → onLayout → onDraw.

МетодШта радиКада користити
invalidate()Позива onDraw без onMeasure/onLayoutПроменио се само изглед (боја, текст, напредак)
invalidate(Rect)Поново црта само наведену областПроменио се део View-а — анимација, селекција
postInvalidate()Позива invalidate из не-UI нитиПозадинска нит је ажурирала податке за цртање
requestLayout()Покреће onMeasure → onLayout → onDrawПроменила се величина садржаја (текст, слика)
forceLayout()Означава View за принудно поновно мерењеУнутрашње стање се променило, величина се могла променити

Анимације и View Lifecycle: ViewPropertyAnimator и ValueAnimator позивају invalidate() на сваком кадру анимације. ObjectAnimator позива setter на View-у који, ако setter мења величину (width/height), аутоматски позива requestLayout(). Ово може бити скупо за сложене ViewGroup-ове: сваки requestLayout покреће потпуну хијерархију до коренског view-а.

Правило оптимизације: invalidate() уместо requestLayout() свуда где се мења само изглед (боја, провидност, ротација без промене величине). requestLayout користите само при промени величина или садржаја који утиче на величину.

Оптимизација прилагођених View-ова: најбоље праксе

Прилагођени View-ови — моћан алат за креирање јединственог UI-ја, али захтевају строго поштовање правила перформанси. Ево кључних Google-ових препорука за оптимизацију View Lifecycle-а.

  • Унапред израчунајте све што се може израчунати — димензије, координате, путању Path, боје градијента. У onDraw-у користите само цртање.
  • Кеширајте резултат мерења — ако View има фиксне димензије, сачувајте MeasureSpec и враћајте setMeasuredDimension без додатних израчунавања.
  • Користите ViewConfiguration — getScaledTouchSlop, getScaledMinimumFlingVelocity — за обраду додира.
  • Минимизирајте број View-ова у хијерархији — прилагођени View-ови који комбинују више елемената су увек бржи од ViewGroup-а са 3–5 угњеждених View-ова. Google препоручује не више од 10 угњеждених View-ова на једном екрану.
  • Користите ConstraintLayout за равну хијерархију — гради један ViewGroup са перформансама блиским RelativeLayout-у, али без угњежђивања.
  • Искључите хардверски слој при поновном цртању — користите setLayerType(LAYER_TYPE_HARDWARE) за View-ове са анимацијама и setLayerType(LAYER_TYPE_NONE) након завршетка.
  • Користите invalidate() са Rect-ом — поново цртајте само измењену област, а не цео View.
  • Избегавајте overdraw — користите Profile GPU Rendering у Android Studio-у за откривање непотребних цртања. Просечан overdraw за Google апликације — 1.5x, максимални препоручени — 2.5x.

Примери кода View-а у Kotlin-у

Пример 1: Прилагођени View — индикатор напретка

Једноставан кружни индикатор напретка са коректном имплементацијом onMeasure, onDraw и 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
    }
}

Кружна трака напретка: onMeasure враћа квадратну величину на основу MeasureSpec-а, onSizeChanged памти димензије, onDraw црта позадину и лук напретка. Invalidate се позива при промени напретка — onMeasure/onLayout нису погођени. Paint се креира једном у конструктору, а не у onDraw-у.

Пример 2: ViewGroup — једноставан FlowLayout

Прилагођени ViewGroup који распоређује подређене View-ове у редове (као 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 преоптерећује onMeasure: мери сваког потомка, пребацује у нови ред при прекорачењу ширине, израчунава укупну висину. onLayout распоређује децу по координатама са прелазима редова. generateLayoutParams враћа MarginLayoutParams за подршку маргинама код подређених View-ова.

Пример 3: onDraw са кеширањем Path-а

Прилагођени View црта глатку Безијерову криву, израчунавајући Path унапред и кеширајући га.

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)
    }
}

Кеширање Path-а: isPathDirty = true само при промени димензија View-а или позиву refreshWave(). У onDraw-у Path се поново израчунава само ако је „прљав”. Ово спречава поновно израчунавање Безијерове криве на сваком кадру анимације, штедећи CPU.

Често постављана питања

Че се View Lifecycle разликује од Activity Lifecycle-а?

View Lifecycle — циклични процес цртања (onMeasure → onLayout → onDraw), независан од креирања/уништавања Activity-ја. View нема onStart/onStop — он је или видљив (причвршћен за прозор) или није. Activity Lifecycle управља стањем компоненте апликације, View Lifecycle — цртањем UI-ја.

Какав утицај requestLayout() има на перформансе?

requestLayout() покреће пун циклус onMeasure → onLayout → onDraw за цело стабло View-а од корена. Ако се requestLayout() често позива (нпр. сваки кадар анимације), то изазива jank и прескакање кадрова. Према Google-у, један requestLayout траје у просеку 2–5 ms на ViewGroup-у са 10 елемената. За анимације користите invalidate().

Када се позива onAttachedToWindow?

onAttachedToWindow се позива када се View причврсти за прозор (Window) — постаје део видљиве хијерархије. У том тренутку View добија HW acceleration и приступ ресурсима Window-а (WindowManager, Display). onAttachedToWindow је право место за регистрацију слушалаца анимација и BroadcastReceiver-а који живи док је View видљив.

Шта је overdraw и како га смањити?

Overdraw — ситуација када се пиксел црта неколико пута у једном кадру. Сваки додатни пролаз је губитак GPU времена. Методе смањења: поставите windowBackground у theme (не цртајте позадину у layout-у), користите canvas.clipRect(), избегавајте стапање угњеждених позадина, примењујте ConstraintLayout уместо угњеждених LinearLayout-а. Android Studio → Profile GPU Rendering → Overdraw приказује мапу боја overdraw-а (плава = 1x, црвена = 3x+).

Да ли је super.onDraw() потребан у прилагођеном View-у?

Да, ако View има позадину (background). super.onDraw() црта позадину View-а. Ако ваш прилагођени View нема позадину или цртате сопствену позадину, super.onDraw() се може изоставити — то штеди један пролаз цртања. За ViewGroup super.dispatchDraw() је обавезан — црта подређене View-ове.

Закључци

  • View Lifecycle — три фазе цртања: onMeasure (димензије), onLayout (позиција), onDraw (цртање).
  • onMeasure обрађује MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) и позива setMeasuredDimension.
  • onLayout у ViewGroup-у распоређује подређене View-ове са координатама left/top/right/bottom.
  • onDraw црта садржај на Canvas-у — не креирајте објекте унутар овог метода.
  • invalidate() позива само onDraw, requestLayout() — пун циклус onMeasure → onLayout → onDraw.
  • Прилагођени View-ови убрзавају UI за 15–40%, али захтевају правилну имплементацију onMeasure-а и кеширање објеката у onDraw-у.
  • За сложену графику користите Hardware Acceleration и кеширајте Path/Bitmap.

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође