View Lifecycle — Android’in ekranda bir kullanıcı arayüzü öğesini (View) çizmek ve yeniden çizmek için çağırdığı metotlar dizisidir. Activity veya Fragment’ın aksine, View genişletilmiş bir yaşam döngüsü olmayan hafif bir bileşendir, ancak katı bir üç aşamalı süreçten geçer: onMeasure (ölçüm), onLayout (konumlandırma), onDraw (çizim). View Lifecycle’ı anlamak, özel View’lar oluşturmak, performansı optimize etmek ve çizim sorunlarını çözmek için gereklidir. Google’a göre, özel View’lar doğru şekilde uygulandığında standart iç içe ViewGroup kombinasyonlarına kıyasla UI’yi %15–40 hızlandırır. Özel View’lar hakkında Android belgeleri, onMeasure, onLayout ve onDraw’ı View Lifecycle’ın üç temel direği olarak tanımlar.
Anahtar Noktalar
View Lifecycle, bir Android View’ın (ve ViewGroup) kendini ekranda görüntülemek için geçirdiği süreçtir. Activity veya Fragment’ın aksine, View’ın onStart/onStop/onDestroy’si yoktur — “ömrü” ölçme, konumlandırma ve çizimden oluşan döngüsel bir süreçten oluşur. Bu döngü, bir View’ın görüntülenmesi veya yeniden çizilmesi gerektiğinde tetiklenir.
View Lifecycle’ın üç aşaması:
Tam View Lifecycle döngüsü ayrıca View’ı bir pencereye eklemeyle ilgili metotları da içerir: onAttachedToWindow (View bir pencereye eklendi, HW hızlandırması var) ve onDetachedFromWindow (View ayrıldı, kaynaklar serbest bırakıldı). Bu metotlar View ömrü boyunca bir kez çağrılır ve animasyonları/sensörleri kaydetmek/iptal etmek için önemlidir.
Android Performance Blog’a göre, UI performans sorunlarının (jank, düşen kareler) %65’i onMeasure ve onDraw’ın yanlış uygulanmasıyla ilgilidir: aşırı geçersiz kılma, gereksiz requestLayout() çağrısı, onDraw’da nesne oluşturma.
onMeasure — View Lifecycle’ın en önemli ve en karmaşık aşaması. Bu aşamada Android, View’ın ekranda ne kadar yer kaplayacağını belirler. Sistem MeasureSpec’i iletir — bir mod ve boyuttan oluşan int paketlenmiş talimatlar.
Üç MeasureSpec modu:
| Mod | Sabit | Anlamı | Örnek |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Üst öğe tarafından belirlenen kesin boyut (match_parent veya sabit genişlik) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View belirtilen maksimum boyuta kadar olabilir (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Kısıtlama yok — View herhangi bir boyutta olabilir (ScrollView, RecyclerView) | genişlik sınırsız → MeasureSpec(0, UNSPECIFIED) |
onMeasure uygulaması şunları yapmalıdır:
setMeasuredDimension(int width, int height) çağırmalıdır.getPaddingLeft() + getPaddingRight() çıkarmalıdır.measureChild() veya measureChildWithMargins() aracılığıyla tüm alt öğeleri ölçmelidir.Tipik hata: wrap_content kullanırken MeasureSpec’i dikkate almamak. Bir View wrap_content olarak ayarlanmışsa, ancak onMeasure AT_MOST’u işlemez ve sabit bir boyut döndürürse, View ya kırpılır ya da gerekenden fazla yer kaplar.
onLayout — bir View veya ViewGroup’un alt öğelerini kendi sınırları içinde düzenlediği aşama. Normal bir View için (ViewGroup değil), onLayout gerekli değildir — sistem, üst öğeden iletilen parametrelerle layout() çağırır. Bir ViewGroup için onLayout zorunludur — onsuz alt View’lar yerleştirilmez.
onLayout imzası:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// alt View’ları düzenleme
}
changed parametresi, View’ın konumunun veya boyutunun önceki layout’a göre değişip değişmediğini belirtir. false ise, View optimizasyon için alt öğe konumlarını yeniden hesaplamayı atlayabilir.
ViewGroup için onLayout şunları yapmalıdır:
getChildCount() ve getChildAt(i) aracılığıyla tüm alt öğeleri dolaşmalıdır.child.layout(l, t, r, b) çağırmalıdır.onLayout, onMeasure’den sonra çağrılır — ölçülen boyutlar getMeasuredWidth()/getMeasuredHeight() aracılığıyla kullanılabilir. layout() sonrası bir alt View’ın gerçek boyutları farklıysa, yeniden ölçmek için requestLayout() çağrılır. Buna “layout geçişi” denir ve zincirleme bir yeniden hesaplama reaksiyonunu tetikleyebilir.
onDraw — bir View’ın kendini Canvas üzerine çizdiği aşama. Bu, onMeasure ve onLayout olmadan birden çok kez çağrılabilen tek aşamadır — View invalidate() olarak işaretlenmişse. Canvas, çizim API’si sağlar: drawLine, drawRect, drawCircle, drawText, drawBitmap ve drawPath.
onDraw kuralları:
canvas.clipRect() kullanın.ViewGroup’ta çizim sırası: arka plan (setBackgroundDrawable) → onDraw (içerik) → dispatchDraw (alt View’lar) → onDrawForeground (ön plan). dispatchDraw her alt öğenin onDraw’ını çağırır. dispatchDraw’ı geçersiz kılmak, alt öğelerin üzerine efekt uygulamak için kullanılır.
Android Vitals istatistiklerine göre, onDraw’da kare düşmesinin en yaygın nedenleri metot içinde nesne oluşturma (%48), decodeResource çağırma (%22) ve önbellekleme olmadan karmaşık Path işlemleridir (%15).
Geçersiz kılma — View yeniden çizimini tetikleyen mekanizma. invalidate() çağrısı View’ı “kirli” olarak işaretler ve sonraki çizim döngüsünde onDraw çağrısını programlar. requestLayout() çağrısı daha “ağır” bir işlemdir ve tam döngüyü tetikler: onMeasure → onLayout → onDraw.
| Metot | Ne yapar | Ne zaman kullanılır |
|---|---|---|
| invalidate() | onMeasure/onLayout olmadan onDraw’ı tetikler | Sadece görünüm değişti (renk, metin, ilerleme) |
| invalidate(Rect) | Sadece belirtilen alanı yeniden çizer | View’ın bir kısmı değişti — animasyon, seçim |
| postInvalidate() | UI olmayan bir iş parçacığından invalidate çağırır | Arka plan iş parçacığı çizim için verileri güncelledi |
| requestLayout() | onMeasure → onLayout → onDraw’ı tetikler | İçerik boyutu değişti (metin, görsel) |
| forceLayout() | Zorunlu yeniden ölçüm için View’ı işaretler | Dahili durum değişti, boyut değişmiş olabilir |
Animasyonlar ve View Lifecycle: ViewPropertyAnimator ve ValueAnimator, her animasyon karesinde invalidate() çağırır. ObjectAnimator, View üzerinde bir setter çağırır ve setter boyutu (genişlik/yükseklik) değiştirirse otomatik olarak requestLayout() çağırır. Bu, karmaşık ViewGroup’lar için maliyetli olabilir: her requestLayout, kök görünüme kadar tüm hiyerarşiyi tetikler.
Optimizasyon kuralı: yalnızca görünümün değiştiği her yerde (renk, şeffaflık, boyut değişmeden dönüş) requestLayout() yerine invalidate() kullanın. requestLayout’u yalnızca boyutları veya boyutu etkileyen içeriği değiştirirken kullanın.
Özel View’lar benzersiz UI oluşturmak için güçlü bir araçtır, ancak performans kurallarına sıkı sıkıya uyulmasını gerektirir. View Lifecycle optimizasyonu için Google’ın temel önerileri şunlardır.
setLayerType(LAYER_TYPE_HARDWARE) ve tamamlandıktan sonra setLayerType(LAYER_TYPE_NONE) kullanın.onMeasure, onDraw ve invalidate’in doğru uygulamasına sahip basit bir dairesel ilerleme göstergesi.
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
}
}
Dairesel ilerleme çubuğu: onMeasure, MeasureSpec’e göre kare boyut döndürür, onSizeChanged boyutları hatırlar, onDraw arka planı ve ilerleme yayını çizer. İlerleme değiştiğinde Invalidate çağrılır — onMeasure/onLayout etkilenmez. Paint, onDraw’da değil, yapıcıda bir kez oluşturulur.
Alt View’ları satırlar halinde düzenleyen özel bir ViewGroup (Flexbox wrap gibi).
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’ı geçersiz kılar: her alt öğeyi ölçer, genişlik aşıldığında yeni satıra geçer, toplam yüksekliği hesaplar. onLayout, alt öğeleri satır sonlarını hesaba katarak koordinatlara göre yerleştirir. generateLayoutParams, alt View’larda kenar boşluğunu desteklemek için MarginLayoutParams döndürür.
Path’i önceden hesaplayıp önbelleğe alan, düzgün bir Bezier eğrisi çizen özel bir View.
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 önbelleklemesi: isPathDirty = true yalnızca View boyutları değiştiğinde veya refreshWave() çağrıldığında. onDraw’da Path yalnızca “kirli” ise yeniden hesaplanır. Bu, her animasyon karesinde Bezier eğrisinin yeniden hesaplanmasını önleyerek CPU tasarrufu sağlar.
Sıkça Sorulan Sorular
View Lifecycle, Activity oluşturma/yok etmeden bağımsız döngüsel bir çizim sürecidir (onMeasure → onLayout → onDraw). View’ın onStart/onStop’u yoktur — ya görünür (bir pencereye ekli) ya da değildir. Activity Lifecycle, uygulama bileşeni durumunu yönetir, View Lifecycle ise UI çizimini yönetir.
requestLayout(), kökten tüm View ağacı için tam onMeasure → onLayout → onDraw döngüsünü tetikler. requestLayout() sık sık çağrılırsa (örn. her animasyon karesi), jank ve düşen karelere neden olur. Google’a göre, 10 öğeli bir ViewGroup’ta bir requestLayout ortalama 2–5 ms sürer. Animasyonlar için invalidate() kullanın.
onAttachedToWindow, bir View bir Window’a eklendiğinde çağrılır — görünür hiyerarşinin parçası olur. Bu anda View, HW hızlandırması ve Window kaynaklarına (WindowManager, Display) erişim kazanır. onAttachedToWindow, View görünür olduğu sürece yaşayan animasyon dinleyicilerini ve BroadcastReceiver’ları kaydetmek için doğru yerdir.
Overdraw, bir pikselin tek bir karede birden çok kez çizildiği bir durumdur. Her ekstra geçiş GPU zamanını boşa harcar. Azaltma yöntemleri: temada windowBackground ayarlayın (layout’ta arka plan çizmeyin), canvas.clipRect() kullanın, iç içe arka planları birleştirmekten kaçının, iç içe LinearLayout yerine ConstraintLayout kullanın. Android Studio → Profile GPU Rendering → Overdraw, overdraw’ın renk haritasını gösterir (mavi = 1x, kırmızı = 3x+).
Evet, View’ın arka planı varsa. super.onDraw(), View’ın arka planını çizer. Özel View’ınızın arka planı yoksa veya kendi arka planınızı çiziyorsanız, super.onDraw() atlanabilir — bu bir çizim geçişinden tasarruf sağlar. ViewGroup için super.dispatchDraw() zorunludur — alt View’ları çizer.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun