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 — процес кроз који Android View (и ViewGroup) пролази да би се приказао на екрану. За разлику од Activity или Fragment, View нема onStart/onStop/onDestroy — његов „живот” се састоји од цикличног процеса мерења, распоређивања и цртања. Овај циклус се покреће сваки пут када View треба да се прикаже или поново нацрта.
Три фазе View Lifecycle-а:
Пун циклус View Lifecycle-а такође укључује методе везане за причвршћивање View-а за прозор: onAttachedToWindow (View је причвршћен за прозор, има HW acceleration) и onDetachedFromWindow (View је одвојен, ресурси се ослобађају). Ове методе се позивају једном у животу View-а и важне су за регистрацију/отказивање анимација, сензора.
Према Android Performance Blog-у, 65% проблема са перформансама UI-ја (jank, прескакање кадрова) повезано је са неправилном имплементацијом onMeasure и onDraw: претерано преоптерећење, позивање requestLayout() без потребе, креирање објеката у onDraw.
onMeasure — најважнија и најсложенија фаза View Lifecycle-а. У овој фази Android одређује колико простора ће View заузети на екрану. Систем прослеђује MeasureSpec — упакована упутства у int, која се састоје од режима и величине.
Три режима MeasureSpec-а:
| Режим | Константа | Значење | Пример |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Тачна величина коју је одредио родитељ (match_parent или фиксна ширина) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View може бити величине до наведеног максимума (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Без ограничења — View може бити било које величине (ScrollView, RecyclerView) | ширина неограничена → MeasureSpec(0, UNSPECIFIED) |
Имплементација onMeasure-а треба:
setMeasuredDimension(int width, int height) за чување измерених димензија.getPaddingLeft() + getPaddingRight() од доступне ширине.measureChild() или measureChildWithMargins().Типична грешка: не узимање у обзир MeasureSpec-а при wrap_content-у. Ако је View подешен на wrap_content, али onMeasure не обрађује AT_MOST и враћа фиксну величину, View ће бити или исечен или ће заузети више простора него што је потребно.
onLayout — фаза у којој View или ViewGroup распоређује своје потомке унутар својих граница. За обичан View (не ViewGroup) onLayout није потребан — систем сам позива layout() са параметрима прослеђеним од родитеља. За ViewGroup onLayout је обавезан — без њега подређени View-ови неће бити распоређени.
Сигнатура onLayout-а:
@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).child.layout(l, t, r, b) за сваког потомка.onLayout се позива после onMeasure-а — измерене димензије су доступне преко getMeasuredWidth()/getMeasuredHeight(). Ако подређени View има другачије стварне димензије после layout()-а, позваће се requestLayout() ради поновног мерења. Ово се назива „layout pass” и може изазвати ланчану реакцију поновних израчунавања.
onDraw — фаза у којој View црта себе на Canvas-у. Ово је једина фаза која може бити позвана више пута без onMeasure и onLayout — ако је View означен као invalidate(). Canvas пружа API за цртање: drawLine, drawRect, drawCircle, drawText, drawBitmap и drawPath.
Правила onDraw-а:
canvas.clipRect() за одсецање невидљивих делова.Редослед цртања у ViewGroup-у: позадина (setBackgroundDrawable) → onDraw (садржај) → dispatchDraw (подређени View-ови) → onDrawForeground (предњи план). dispatchDraw позива onDraw сваког потомка. Преоптерећење dispatchDraw-а се користи за наношење ефеката преко подређених елемената.
Према статистици Android Vitals-а, најчешћи узроци прескакања кадрова у onDraw-у — креирање објеката унутар метода (48%), позив decodeResource (22%) и сложене операције са Path-ом без кеширања (15%).
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-ови — моћан алат за креирање јединственог UI-ја, али захтевају строго поштовање правила перформанси. Ево кључних Google-ових препорука за оптимизацију View Lifecycle-а.
setLayerType(LAYER_TYPE_HARDWARE) за View-ове са анимацијама и setLayerType(LAYER_TYPE_NONE) након завршетка.Једноставан кружни индикатор напретка са коректном имплементацијом onMeasure, onDraw и 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
}
}
Кружна трака напретка: onMeasure враћа квадратну величину на основу MeasureSpec-а, onSizeChanged памти димензије, onDraw црта позадину и лук напретка. Invalidate се позива при промени напретка — onMeasure/onLayout нису погођени. Paint се креира једном у конструктору, а не у onDraw-у.
Прилагођени ViewGroup који распоређује подређене View-ове у редове (као 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 преоптерећује onMeasure: мери сваког потомка, пребацује у нови ред при прекорачењу ширине, израчунава укупну висину. onLayout распоређује децу по координатама са прелазима редова. generateLayoutParams враћа MarginLayoutParams за подршку маргинама код подређених View-ова.
Прилагођени View црта глатку Безијерову криву, израчунавајући Path унапред и кеширајући га.
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 — циклични процес цртања (onMeasure → onLayout → onDraw), независан од креирања/уништавања Activity-ја. View нема onStart/onStop — он је или видљив (причвршћен за прозор) или није. Activity Lifecycle управља стањем компоненте апликације, View Lifecycle — цртањем UI-ја.
requestLayout() покреће пун циклус onMeasure → onLayout → onDraw за цело стабло View-а од корена. Ако се requestLayout() често позива (нпр. сваки кадар анимације), то изазива jank и прескакање кадрова. Према Google-у, један requestLayout траје у просеку 2–5 ms на ViewGroup-у са 10 елемената. За анимације користите invalidate().
onAttachedToWindow се позива када се View причврсти за прозор (Window) — постаје део видљиве хијерархије. У том тренутку View добија HW acceleration и приступ ресурсима Window-а (WindowManager, Display). onAttachedToWindow је право место за регистрацију слушалаца анимација и BroadcastReceiver-а који живи док је View видљив.
Overdraw — ситуација када се пиксел црта неколико пута у једном кадру. Сваки додатни пролаз је губитак GPU времена. Методе смањења: поставите windowBackground у theme (не цртајте позадину у layout-у), користите canvas.clipRect(), избегавајте стапање угњеждених позадина, примењујте ConstraintLayout уместо угњеждених LinearLayout-а. Android Studio → Profile GPU Rendering → Overdraw приказује мапу боја overdraw-а (плава = 1x, црвена = 3x+).
Да, ако View има позадину (background). super.onDraw() црта позадину View-а. Ако ваш прилагођени View нема позадину или цртате сопствену позадину, super.onDraw() се може изоставити — то штеди један пролаз цртања. За ViewGroup super.dispatchDraw() је обавезан — црта подређене View-ове.
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође