View Lifecycle — Android-in istifadəçi interfeysi elementini (View) ekranda göstərmək və yenidən göstərmək üçün çağırdığı metodlar ardıcıllığıdır. Activity və ya Fragment-dən fərqli olaraq, View genişləndirilmiş həyat dövrü olmayan, lakin ciddi üç fazalı prosesdən keçən yüngül bir komponentdir: onMeasure (ölçmə), onLayout (yerləşdirmə), onDraw (çəkmə). View Lifecycle-ni başa düşmək xüsusi View-lər yaratmaq, performansı optimallaşdırmaq və göstərmə problemlərini həll etmək üçün vacibdir. Google məlumatlarına görə, düzgün tətbiq edildikdə xüsusi View-lər UI-ni 15–40% sürətləndirir standart iç-içə ViewGroup kombinasiyası ilə müqayisədə. Android-in xüsusi View-lər haqqında sənədləri onMeasure, onLayout və onDraw-ı View Lifecycle-nin üç sütunu kimi təsvir edir.
Əsas məqamlar
View Lifecycle — Android View-in (və ViewGroup-un) özünü ekranda göstərmək üçün keçdiyi prosesdir. Activity və ya Fragment-dən fərqli olaraq, View-də onStart/onStop/onDestroy yoxdur — onun "həyatı" ölçmə, yerləşdirmə və çəkmə dövri prosesindən ibarətdir. Bu dövrü View hər dəfə göstərilməli və ya yenidən çəkilməli olduqda işə düşür.
View Lifecycle-nin üç fazası:
View Lifecycle-nin tam dövrü həmçinin View-in pəncərəyə bağlanması ilə əlaqəli metodları əhatə edir: onAttachedToWindow (View pəncərəyə bağlanıb, HW acceleration var) və onDetachedFromWindow (View ayrılıb, resurslar sərbəst buraxılır). Bu metodlar View-in ömrü boyu bir dəfə çağrılır və animasiyaların, sensorların qeydiyyatı/ləğvi üçün vacibdir.
Android Performance Blog məlumatlarına görə, UI performans problemlərinin (jank, kadr buraxma) 65%-i onMeasure və onDraw-ın səhv tətbiqi ilə bağlıdır: həddindən artıq override, lazımsız requestLayout() çağırışı, onDraw-da obyekt yaratma.
onMeasure — View Lifecycle-nin ən vacib və ən mürəkkəb fazası. Bu mərhələdə Android View-in ekranda nə qədər yer tutacağını müəyyən edir. Sistem rejim və ölçüdən ibarət int-ə yığılmış təlimatlar olan MeasureSpec ötürür.
MeasureSpec-in üç rejimi:
| Rejim | Sabit | Məna | Nümunə |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Valideyn tərəfindən təyin edilmiş dəqiq ölçü (match_parent və ya sabit genişlik) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View göstərilən maksimuma qədər ölçüdə ola bilər (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Məhdudiyyətsiz — View istənilən ölçüdə ola bilər (ScrollView, RecyclerView) | genişlik məhdudiyyətsiz → MeasureSpec(0, UNSPECIFIED) |
onMeasure tətbiqi aşağıdakıları etməlidir:
setMeasuredDimension(int width, int height) çağırın.getPaddingLeft() + getPaddingRight() çıxın.measureChild() və ya measureChildWithMargins() vasitəsilə ölçün.Tipik səhv: wrap_content zamanı MeasureSpec-i nəzərə almamaq. View wrap_content-ə təyin edilibsə, lakin onMeasure AT_MOST-u idarə etmir və sabit ölçü qaytarırsa, View ya kəsiləcək, ya da lazım olduğundan çox yer tutacaq.
onLayout — View və ya ViewGroup-un törəmələrini öz sərhədləri daxilində yerləşdirdiyi faza. Adi View (ViewGroup olmayan) üçün onLayout tələb olunmur — sistem valideyndən ötürülən parametrlərlə layout() çağırır. ViewGroup üçün onLayout məcburidir — onsuz uşaq View-lər yerləşdirilməyəcək.
onLayout imzası:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// uşaq View-lərin yerləşdirilməsi
}
changed parametri View-in mövqeyinin və ya ölçüsünün əvvəlki layout ilə müqayisədə dəyişib-dəyişmədiyini göstərir. false olarsa, View optimallaşdırma üçün törəmələrin mövqelərinin yenidən hesablanmasını keçə bilər.
ViewGroup üçün onLayout aşağıdakıları etməlidir:
getChildCount() və getChildAt(i) vasitəsilə nəzərdən keçirin.child.layout(l, t, r, b) çağırın.onLayout onMeasure-dən sonra çağırılır — ölçülmüş ölçülər getMeasuredWidth()/getMeasuredHeight() vasitəsilə əlçatandır. Əgər uşaq View-in layout()-dan sonra fərqli real ölçüləri varsa, yenidən ölçmək üçün requestLayout() çağırılacaq. Buna "layout pass" deyilir və yenidən hesablamaların zəncirvari reaksiyasına səbəb ola bilər.
onDraw — View-in Canvas-da özünü çəkdiyi faza. Bu, onMeasure və onLayout olmadan dəfələrlə çağırıla bilən yeganə fazadır — View invalidate() kimi işarələnmişsə. Canvas çəkmək üçün API təqdim edir: drawLine, drawRect, drawCircle, drawText, drawBitmap və drawPath.
onDraw qaydaları:
canvas.clipRect() istifadə edin.ViewGroup-da çəkmə ardıcıllığı: fon (setBackgroundDrawable) → onDraw (məzmun) → dispatchDraw (uşaq View-lər) → onDrawForeground (ön plan). dispatchDraw hər törəmənin onDraw-ını çağırır. dispatchDraw-ı override etmək uşaq elementlərin üzərinə effektlər əlavə etmək üçün istifadə olunur.
Android Vitals statistikasına görə, onDraw-da kadr buraxmalarının ən çox yayılmış səbəbləri metod daxilində obyekt yaratma (48%), decodeResource çağırışı (22%) və keşləmədən Path ilə mürəkkəb əməliyyatlar (15%).
Invalidation — View-in yenidən çəkilməsini işə salan mexanizmdir. invalidate() çağırışı View-i "çirkli" kimi qeyd edir və növbəti çəkmə dövründə onDraw çağırışını planlaşdırır. requestLayout() çağırışı — daha "ağır" əməliyyatdır, tam dövrü işə salır: onMeasure → onLayout → onDraw.
| Metod | Nə edir | Nə vaxt istifadə etməli |
|---|---|---|
| invalidate() | onMeasure/onLayout olmadan onDraw çağırır | Yalnız görünüş dəyişib (rəng, mətn, gedişat) |
| invalidate(Rect) | Yalnız göstərilən sahəni yenidən çəkir | View-in bir hissəsi dəyişib — animasiya, seçim |
| postInvalidate() | UI olmayan thread-dən invalidate çağırır | Fon thread-i çəkmə üçün məlumatları yeniləyib |
| requestLayout() | onMeasure → onLayout → onDraw işə salır | Məzmun ölçüsü dəyişib (mətn, şəkil) |
| forceLayout() | View-i məcburi yenidən ölçmə üçün qeyd edir | Daxili vəziyyət dəyişib, ölçü dəyişə bilər |
Animasiyalar və View Lifecycle: ViewPropertyAnimator və ValueAnimator animasiyanın hər kadrında invalidate() çağırır. ObjectAnimator View-də setter çağırır, əgər setter ölçünü (width/height) dəyişirsə, avtomatik requestLayout() çağırır. Bu, mürəkkəb ViewGroup üçün bahalı ola bilər: hər requestLayout kök view-ə qədər tam iyerarxiyanı işə salır.
Optimallaşdırma qaydası: invalidate() requestLayout() əvəzinə yalnız görünüş dəyişdikdə (rəng, şəffaflıq, ölçü dəyişmədən döndərmə). requestLayout yalnız ölçülər və ya ölçüyə təsir edən məzmun dəyişdikdə istifadə edin.
Xüsusi View-lər unikal UI yaratmaq üçün güclü alətdir, lakin onlar performans qaydalarına ciddi riayət etməyi tələb edir. View Lifecycle optimallaşdırması üçün Google-ın əsas tövsiyələri.
setLayerType(LAYER_TYPE_HARDWARE) və bitdikdən sonra setLayerType(LAYER_TYPE_NONE) istifadə edin.onMeasure, onDraw və invalidate-in düzgün tətbiqi ilə sadə dairəvi gedişat göstəricisi.
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
}
}
Dairəvi gedişat çubuğu: onMeasure MeasureSpec əsasında kvadrat ölçü qaytarır, onSizeChanged ölçüləri yadda saxlayır, onDraw fon və gedişat qövsünü çəkir. Invalidate gedişat dəyişdikdə çağırılır — onMeasure/onLayout təsirlənmir. Paint konstruktorda bir dəfə yaradılır, onDraw-da yox.
Uşaq View-ləri sətirlərə yerləşdirən xüsusi ViewGroup (Flexbox wrap kimi).
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-i override edir: hər törəməni ölçür, genişlik aşıldıqda yeni sətirə keçir, ümumi hündürlüyü hesablayır. onLayout uşaqları sətir keçidləri nəzərə alınmaqla koordinatlara görə yerləşdirir. generateLayoutParams uşaq View-lərdə margin dəstəyi üçün MarginLayoutParams qaytarır.
Xüsusi View Path-i əvvəlcədən hesablayaraq və keşləyərək hamar Bezier əyrisi çəkir.
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 keşləmə: isPathDirty = true yalnız View ölçüləri dəyişdikdə və ya refreshWave() çağırıldıqda. onDraw-da Path yalnız "çirkli" olduqda yenidən hesablanır. Bu, CPU-ya qənaət edərək Bezier əyrisinin hər animasiya kadrında yenidən hesablanmasının qarşısını alır.
Tez-tez verilən suallar
View Lifecycle — Activity-nin yaradılması/məhv edilməsindən asılı olmayan dövri çəkmə prosesidir (onMeasure → onLayout → onDraw). View-də onStart/onStop yoxdur — o ya görünür (pəncərəyə bağlıdır), ya da yox. Activity Lifecycle tətbiq komponentinin vəziyyətini idarə edir, View Lifecycle — UI-nin çəkilməsini.
requestLayout() kökdən bütün View ağacı üçün tam dövrü işə salır: onMeasure → onLayout → onDraw. requestLayout() tez-tez çağırılarsa (məsələn, animasiyanın hər kadrında), bu jank və kadr buraxmalarına səbəb olur. Google məlumatlarına görə, bir requestLayout 10 elementli ViewGroup-da orta hesabla 2–5 ms çəkir. Animasiyalar üçün invalidate() istifadə edin.
onAttachedToWindow View pəncərəyə (Window) bağlandıqda — görünən iyerarxiyanın bir hissəsi olduqda çağırılır. Bu anda View HW acceleration və Window resurslarına (WindowManager, Display) çıxış əldə edir. onAttachedToWindow animasiya dinləyicilərini və View görünən müddətdə yaşayan BroadcastReceiver-i qeydiyyatdan keçirmək üçün düzgün yerdir.
Overdraw — pikselin bir kadrda bir neçə dəfə çəkildiyi vəziyyətdir. Hər əlavə keçid GPU vaxtının itkisidir. Azaltma üsulları: theme-də windowBackground təyin edin (layout-da fon çəkməyin), canvas.clipRect() istifadə edin, iç-içə fonları birləşdirməkdən qaçının, iç-içə LinearLayout əvəzinə ConstraintLayout tətbiq edin. Android Studio → Profile GPU Rendering → Overdraw overdraw rəng xəritəsini göstərir (mavi = 1x, qırmızı = 3x+).
Bəli, View-in fonu (background) varsa. super.onDraw() View-in fonunu çəkir. Əgər xüsusi View-in fonu yoxdursa və ya öz fonunuzu çəkirsinizsə, super.onDraw() çağırılmaya bilər — bu bir çəkmə keçidinə qənaət edər. ViewGroup üçün super.dispatchDraw() məcburidir — uşaq View-ləri çəkir.
Nəticə
Açar təslim mobil tətbiq hazırlayacağıq
IT Sectr 2017-ci ildən startaplar və bizneslər üçün iOS və Android tətbiqləri yaradır. Sizə məsləhət verəcəyik və ən yaxşı həlli təklif edəcəyik.
Həm də oxuyun