View Lifecycle — urutan metode yang dipanggil Android untuk merender dan merender ulang elemen antarmuka pengguna (View) di layar. Tidak seperti Activity atau Fragment, View adalah komponen ringan yang tidak memiliki siklus hidup yang diperluas, tetapi melalui proses tiga fase yang ketat: onMeasure (pengukuran), onLayout (penempatan), onDraw (penggambaran). Memahami View Lifecycle diperlukan untuk membuat View kustom, mengoptimalkan performa, dan mengatasi masalah rendering. Menurut Google, View kustom mempercepat UI sebesar 15–40% dibandingkan dengan kombinasi ViewGroup standar bersarang jika diimplementasikan dengan benar. Dokumentasi Android tentang View kustom menjelaskan onMeasure, onLayout, dan onDraw sebagai tiga pilar View Lifecycle.
Poin utama
View Lifecycle — proses yang dilalui Android View (dan ViewGroup) untuk menampilkan dirinya di layar. Tidak seperti Activity atau Fragment, View tidak memiliki onStart/onStop/onDestroy — “hidupnya” terdiri dari proses siklis pengukuran, penempatan, dan penggambaran. Siklus ini dipicu setiap kali View perlu ditampilkan atau digambar ulang.
Tiga fase View Lifecycle:
Siklus lengkap View Lifecycle juga mencakup metode yang terkait dengan pelekatan View ke jendela: onAttachedToWindow (View terpasang ke jendela, memiliki akselerasi HW) dan onDetachedFromWindow (View terlepas, sumber daya dibebaskan). Metode ini dipanggil satu kali selama masa View dan penting untuk mendaftarkan/membatalkan animasi, sensor.
Menurut Android Performance Blog, 65% masalah performa UI (jank, frame terlewat) terkait dengan implementasi onMeasure dan onDraw yang salah: override berlebihan, pemanggilan requestLayout() tanpa perlu, pembuatan objek di onDraw.
onMeasure — fase terpenting dan paling kompleks dari View Lifecycle. Pada tahap ini Android menentukan berapa banyak ruang yang akan ditempati View di layar. Sistem meneruskan MeasureSpec — instruksi yang dikemas dalam int yang terdiri dari mode dan ukuran.
Tiga mode MeasureSpec:
| Mode | Konstanta | Arti | Contoh |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Ukuran tepat yang ditentukan oleh induk (match_parent atau lebar tetap) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View dapat berukuran hingga maksimum yang ditentukan (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Tanpa batasan — View dapat berukuran berapa pun (ScrollView, RecyclerView) | lebar tidak terbatas → MeasureSpec(0, UNSPECIFIED) |
Implementasi onMeasure harus:
setMeasuredDimension(int width, int height) untuk menyimpan ukuran yang diukur.getPaddingLeft() + getPaddingRight() dari lebar yang tersedia.measureChild() atau measureChildWithMargins().Kesalahan umum: tidak memperhitungkan MeasureSpec saat wrap_content. Jika View diatur ke wrap_content, tetapi onMeasure tidak memproses AT_MOST dan mengembalikan ukuran tetap, View akan terpotong atau mengambil lebih banyak ruang daripada yang dibutuhkan.
onLayout — fase di mana View atau ViewGroup menempatkan keturunannya di dalam batasnya. Untuk View biasa (bukan ViewGroup) onLayout tidak diperlukan — sistem sendiri memanggil layout() dengan parameter dari induk. Untuk ViewGroup onLayout wajib — tanpanya View anak tidak akan ditempatkan.
Signature onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// penempatan View anak
}
Parameter changed menunjukkan apakah posisi atau ukuran View berubah dibandingkan dengan layout sebelumnya. Jika false — View dapat melewatkan perhitungan ulang posisi keturunan untuk optimasi.
Untuk ViewGroup onLayout harus:
getChildCount() dan getChildAt(i).child.layout(l, t, r, b) untuk setiap keturunan.onLayout dipanggil setelah onMeasure — ukuran yang diukur tersedia melalui getMeasuredWidth()/getMeasuredHeight(). Jika View anak memiliki ukuran nyata yang berbeda setelah layout(), requestLayout() akan dipanggil untuk pengukuran ulang. Ini disebut “layout pass” dan dapat menyebabkan reaksi berantai perhitungan ulang.
onDraw — fase di mana View menggambar dirinya di Canvas. Ini adalah satu-satunya fase yang dapat dipanggil berkali-kali tanpa onMeasure dan onLayout — jika View ditandai sebagai invalidate(). Canvas menyediakan API untuk menggambar: drawLine, drawRect, drawCircle, drawText, drawBitmap, dan drawPath.
Aturan onDraw:
canvas.clipRect() untuk memotong bagian yang tidak terlihat.Urutan penggambaran di ViewGroup: latar belakang (setBackgroundDrawable) → onDraw (konten) → dispatchDraw (View anak) → onDrawForeground (latar depan). dispatchDraw memanggil onDraw setiap keturunan. Override dispatchDraw digunakan untuk menerapkan efek di atas elemen anak.
Menurut statistik Android Vitals, penyebab paling umum frame terlewat di onDraw — pembuatan objek di dalam metode (48%), pemanggilan decodeResource (22%), dan operasi kompleks dengan Path tanpa cache (15%).
Invalidation — mekanisme yang memicu penggambaran ulang View. Pemanggilan invalidate() menandai View sebagai “kotor” dan menjadwalkan panggilan onDraw di siklus penggambaran berikutnya. Pemanggilan requestLayout() — operasi yang lebih berat, memicu siklus lengkap: onMeasure → onLayout → onDraw.
| Metode | Apa yang dilakukan | Kapan digunakan |
|---|---|---|
| invalidate() | Memanggil onDraw tanpa onMeasure/onLayout | Hanya tampilan yang berubah (warna, teks, progres) |
| invalidate(Rect) | Menggambar ulang hanya area yang ditentukan | Bagian View berubah — animasi, seleksi |
| postInvalidate() | Memanggil invalidate dari thread non-UI | Thread latar belakang memperbarui data untuk digambar |
| requestLayout() | Memicu onMeasure → onLayout → onDraw | Ukuran konten berubah (teks, gambar) |
| forceLayout() | Menandai View untuk pengukuran ulang paksa | Status internal berubah, ukuran mungkin berubah |
Animasi dan View Lifecycle: ViewPropertyAnimator dan ValueAnimator memanggil invalidate() pada setiap frame animasi. ObjectAnimator memanggil setter di View yang, jika setter mengubah ukuran (width/height), secara otomatis memanggil requestLayout(). Ini bisa mahal untuk ViewGroup kompleks: setiap requestLayout memicu seluruh hierarki hingga view root.
Aturan optimasi: invalidate() daripada requestLayout() di mana pun hanya tampilan yang berubah (warna, transparansi, rotasi tanpa perubahan ukuran). Gunakan requestLayout hanya saat mengubah ukuran atau konten yang memengaruhi ukuran.
View kustom — alat yang ampuh untuk membuat UI yang unik, tetapi memerlukan kepatuhan ketat terhadap aturan performa. Berikut rekomendasi utama Google untuk mengoptimalkan View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) untuk View dengan animasi dan setLayerType(LAYER_TYPE_NONE) setelah selesai.Indikator progres melingkar sederhana dengan implementasi onMeasure, onDraw, dan invalidate yang benar.
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
}
}
Bilah progres melingkar: onMeasure mengembalikan ukuran persegi berdasarkan MeasureSpec, onSizeChanged menyimpan ukuran, onDraw menggambar latar belakang dan busur progres. Invalidate dipanggil saat progres berubah — onMeasure/onLayout tidak terpengaruh. Paint dibuat sekali di konstruktor, bukan di onDraw.
ViewGroup kustom yang menempatkan View anak dalam baris (seperti 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 mengoverride onMeasure: mengukur setiap keturunan, pindah ke baris baru saat melebihi lebar, menghitung tinggi total. onLayout menempatkan anak berdasarkan koordinat dengan memperhitungkan pemindahan baris. generateLayoutParams mengembalikan MarginLayoutParams untuk mendukung margin di View anak.
View kustom menggambar kurva Bezier halus, menghitung Path terlebih dahulu dan menyimpannya dalam cache.
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)
}
}
Caching Path: isPathDirty = true hanya saat ukuran View berubah atau refreshWave() dipanggil. Di onDraw Path dihitung ulang hanya jika “kotor”. Ini mencegah perhitungan ulang kurva Bezier pada setiap frame animasi, menghemat CPU.
Pertanyaan yang sering diajukan
View Lifecycle — proses penggambaran siklis (onMeasure → onLayout → onDraw), tidak tergantung pada pembuatan/penghancuran Activity. View tidak memiliki onStart/onStop — ia terlihat (terpasang ke jendela) atau tidak. Activity Lifecycle mengelola status komponen aplikasi, View Lifecycle — penggambaran UI.
requestLayout() memicu siklus lengkap onMeasure → onLayout → onDraw untuk seluruh pohon View dari root. Jika requestLayout() sering dipanggil (misalnya setiap frame animasi), ini menyebabkan jank dan frame terlewat. Menurut Google, satu requestLayout memakan rata-rata 2–5 ms pada ViewGroup dengan 10 elemen. Untuk animasi gunakan invalidate().
onAttachedToWindow dipanggil ketika View terpasang ke jendela (Window) — menjadi bagian dari hierarki yang terlihat. Pada saat ini View mendapatkan akselerasi HW dan akses ke sumber daya Window (WindowManager, Display). onAttachedToWindow adalah tempat yang tepat untuk mendaftarkan listener animasi dan BroadcastReceiver yang hidup selama View terlihat.
Overdraw — situasi di mana piksel digambar beberapa kali dalam satu frame. Setiap lintasan tambahan adalah pemborosan waktu GPU. Metode pengurangan: atur windowBackground di theme (jangan menggambar latar belakang di layout), gunakan canvas.clipRect(), hindari penggabungan latar belakang bersarang, terapkan ConstraintLayout sebagai ganti LinearLayout bersarang. Android Studio → Profile GPU Rendering → Overdraw menunjukkan peta warna overdraw (biru = 1x, merah = 3x+).
Ya, jika View memiliki latar belakang (background). super.onDraw() menggambar latar belakang View. Jika View kustom Anda tidak memiliki latar belakang atau Anda menggambar latar belakang sendiri, super.onDraw() dapat dilewati — ini menghemat satu lintasan penggambaran. Untuk ViewGroup super.dispatchDraw() wajib — ia menggambar View anak.
Ringkasan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga