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 ускорение и достъп до ресурсите на 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също