View Lifecycle — ang pagkakasunod-sunod ng mga pamamaraan na tinatawag ng Android para sa pag-render at muling pag-render ng elemento ng user interface (View) sa screen. Hindi tulad ng Activity o Fragment, ang View ay isang magaan na bahagi na walang pinalawak na lifecycle, ngunit dumadaan sa isang mahigpit na prosesong may tatlong yugto: onMeasure (pagsukat), onLayout (pagpoposisyon), onDraw (pag-drawing). Ang pag-unawa sa View Lifecycle ay kinakailangan para sa paglikha ng custom na View, pag-optimize ng performance, at paglutas ng mga problema sa pag-render. Ayon sa Google, ang custom na View ay nagpapabilis ng UI ng 15–40% kumpara sa kumbinasyon ng karaniwang nested ViewGroup kapag wasto ang implementasyon. Dokumentasyon ng Android tungkol sa custom na View ay naglalarawan ng onMeasure, onLayout at onDraw bilang tatlong haligi ng View Lifecycle.
Mga pangunahing punto
View Lifecycle — ang proseso na pinagdadaanan ng Android View (at ViewGroup) upang maipakita ang sarili sa screen. Hindi tulad ng Activity o Fragment, ang View ay walang onStart/onStop/onDestroy — ang “buhay” nito ay binubuo ng isang paikot na proseso ng pagsukat, pagpoposisyon at pag-drawing. Ang siklong ito ay na-trigger sa bawat oras na kailangang ipakita o muling iguhit ang View.
Tatlong yugto ng View Lifecycle:
Ang buong siklo ng View Lifecycle ay kasama rin ang mga pamamaraan na may kaugnayan sa pag-attach ng View sa window: onAttachedToWindow (View ay naka-attach sa window, may HW acceleration) at onDetachedFromWindow (View ay na-detach, ang mga mapagkukunan ay pinalaya). Ang mga pamamaraang ito ay tinatawag nang isang beses sa buhay ng View at mahalaga para sa pagrehistro/pagkansela ng mga animation, sensor.
Ayon sa Android Performance Blog, 65% ng mga problema sa performance ng UI (jank, nawawalang frame) ay nauugnay sa maling implementasyon ng onMeasure at onDraw: labis na override, pagtawag ng requestLayout() nang hindi kinakailangan, paggawa ng mga bagay sa onDraw.
onMeasure — ang pinakamahalaga at pinakakomplikadong yugto ng View Lifecycle. Sa yugtong ito tinutukoy ng Android kung gaano karaming espasyo ang sasakupin ng View sa screen. Ipinapasa ng system ang MeasureSpec — mga tagubilin na nakabalot sa int na binubuo ng mode at laki.
Tatlong mode ng MeasureSpec:
| Mode | Constant | Kahulugan | Halimbawa |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Eksaktong laki na tinukoy ng magulang (match_parent o nakapirming lapad) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View ay maaaring kasing laki ng tinukoy na maximum (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Walang limitasyon — View ay maaaring kahit anong laki (ScrollView, RecyclerView) | lapad walang limitasyon → MeasureSpec(0, UNSPECIFIED) |
Ang implementasyon ng onMeasure ay dapat:
setMeasuredDimension(int width, int height) upang i-save ang mga nasukat na sukat.getPaddingLeft() + getPaddingRight() mula sa available na lapad.measureChild() o measureChildWithMargins().Karaniwang pagkakamali: hindi pagsasaalang-alang ng MeasureSpec sa wrap_content. Kung ang View ay nakatakda sa wrap_content, ngunit hindi pinoproseso ng onMeasure ang AT_MOST at nagbabalik ng nakapirming laki, ang View ay mapuputol o sasakupin ang mas maraming espasyo kaysa sa kinakailangan.
onLayout — ang yugto kung saan inaayos ng View o ViewGroup ang mga inapo nito sa loob ng mga hangganan nito. Para sa ordinaryong View (hindi ViewGroup) ang onLayout ay hindi kinakailangan — ang system mismo ay tumatawag ng layout() na may mga parameter mula sa magulang. Para sa ViewGroup ang onLayout ay sapilitan — kung wala ito, ang mga child View ay hindi maaayos.
Lagda ng onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// pag-aayos ng mga child View
}
Ang parameter na changed ay nagpapahiwatig kung ang posisyon o laki ng View ay nagbago kumpara sa nakaraang layout. Kung false — maaaring laktawan ng View ang muling pagkalkula ng mga posisyon ng mga inapo para sa pag-optimize.
Para sa ViewGroup ang onLayout ay dapat:
getChildCount() at getChildAt(i).child.layout(l, t, r, b) para sa bawat inapo.Ang onLayout ay tinatawag pagkatapos ng onMeasure — ang mga nasukat na sukat ay available sa pamamagitan ng getMeasuredWidth()/getMeasuredHeight(). Kung ang isang child View ay may iba't ibang aktwal na sukat pagkatapos ng layout(), tatawagin ang requestLayout() para sa muling pagsukat. Ito ay tinatawag na “layout pass” at maaaring magdulot ng chain reaction ng mga muling pagkalkula.
onDraw — ang yugto kung saan iginuguhit ng View ang sarili nito sa Canvas. Ito ang tanging yugto na maaaring tawagin nang maraming beses nang walang onMeasure at onLayout — kung ang View ay minarkahan bilang invalidate(). Ang Canvas ay nagbibigay ng API para sa pag-drawing: drawLine, drawRect, drawCircle, drawText, drawBitmap at drawPath.
Mga patakaran ng onDraw:
canvas.clipRect() para putulin ang mga hindi nakikitang bahagi.Pagkakasunod-sunod ng pag-drawing sa ViewGroup: background (setBackgroundDrawable) → onDraw (nilalaman) → dispatchDraw (mga child View) → onDrawForeground (foreground). Ang dispatchDraw ay tumatawag ng onDraw ng bawat inapo. Ang pag-override ng dispatchDraw ay ginagamit para maglapat ng mga epekto sa ibabaw ng mga elementong anak.
Ayon sa mga istatistika ng Android Vitals, ang mga pinakakaraniwang sanhi ng nawawalang frame sa onDraw — paggawa ng mga bagay sa loob ng pamamaraan (48%), pagtawag ng decodeResource (22%), at komplikadong operasyon sa Path nang walang cache (15%).
Invalidation — mekanismo na nagti-trigger ng muling pag-drawing ng View. Ang pagtawag ng invalidate() ay minamarkahan ang View bilang “dumi” at nag-iiskedyul ng tawag sa onDraw sa susunod na siklo ng pag-drawing. Ang pagtawag ng requestLayout() — mas mabigat na operasyon, na nagti-trigger ng buong siklo: onMeasure → onLayout → onDraw.
| Pamamaraan | Ano ang ginagawa | Kailan gagamitin |
|---|---|---|
| invalidate() | Tumatawag ng onDraw nang walang onMeasure/onLayout | Tanging ang hitsura ang nagbago (kulay, teksto, progreso) |
| invalidate(Rect) | Muling iginuguhit lamang ang tinukoy na lugar | Bahagi ng View ang nagbago — animation, pagpili |
| postInvalidate() | Tumatawag ng invalidate mula sa hindi UI thread | Ang background thread ay nag-update ng data para sa pag-drawing |
| requestLayout() | Nagti-trigger ng onMeasure → onLayout → onDraw | Nagbago ang laki ng nilalaman (teksto, imahe) |
| forceLayout() | Minamarkahan ang View para sa sapilitang muling pagsukat | Nagbago ang panloob na estado, maaaring nagbago ang laki |
Mga animation at View Lifecycle: Ang ViewPropertyAnimator at ValueAnimator ay tumatawag ng invalidate() sa bawat frame ng animation. Ang ObjectAnimator ay tumatawag ng setter sa View na, kung binago ng setter ang laki (width/height), awtomatikong tumatawag ng requestLayout(). Ito ay maaaring magastos para sa komplikadong ViewGroup: bawat requestLayout ay nagti-trigger ng buong hierarchy hanggang sa root view.
Patakaran sa pag-optimize: invalidate() sa halip na requestLayout() kahit saan kung saan ang hitsura lamang ang nagbabago (kulay, transparency, pag-ikot nang walang pagbabago sa laki). Gamitin ang requestLayout lamang kapag nagbabago ang mga sukat o nilalaman na nakakaapekto sa laki.
Ang custom na View — isang makapangyarihang tool para sa paglikha ng natatanging UI, ngunit nangangailangan ng mahigpit na pagsunod sa mga patakaran ng performance. Narito ang mga pangunahing rekomendasyon ng Google para sa pag-optimize ng View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) para sa View na may animation at setLayerType(LAYER_TYPE_NONE) pagkatapos matapos.Simpleng pabilog na indicator ng progreso na may tamang implementasyon ng onMeasure, onDraw at 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
}
}
Pabilog na bar ng progreso: ang onMeasure ay nagbabalik ng parisukat na sukat batay sa MeasureSpec, ang onSizeChanged ay naaalala ang mga sukat, ang onDraw ay iginuguhit ang background at ang arko ng progreso. Ang Invalidate ay tinatawag kapag nagbago ang progreso — ang onMeasure/onLayout ay hindi naaapektuhan. Ang Paint ay ginagawa nang isang beses sa constructor, hindi sa onDraw.
Custom na ViewGroup na nag-aayos ng mga child View sa mga hilera (tulad ng 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)
}
}
Ang FlowLayout ay nag-o-override ng onMeasure: sinusukat ang bawat inapo, lumipat sa bagong hilera kapag lumampas sa lapad, kinakalkula ang kabuuang taas. Ang onLayout ay nag-aayos ng mga bata ayon sa mga coordinate na may pagsasaalang-alang sa mga paglilipat ng linya. Ang generateLayoutParams ay nagbabalik ng MarginLayoutParams para sa suporta ng margin sa mga child View.
Ang custom na View ay iginuguhit ang makinis na Bezier curve, kinakalkula ang Path nang maaga at ina-cache ito.
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 ng Path: ang isPathDirty = true lamang kapag nagbago ang mga sukat ng View o tinawag ang refreshWave(). Sa onDraw ang Path ay muling kinakalkula lamang kung ito ay “dumi”. Ito ay pumipigil sa muling pagkalkula ng Bezier curve sa bawat frame ng animation, nakakatipid ng CPU.
Mga madalas itanong
View Lifecycle — isang paikot na proseso ng pag-drawing (onMeasure → onLayout → onDraw), hindi nakadepende sa paggawa/pagwasak ng Activity. Ang View ay walang onStart/onStop — ito ay nakikita (naka-attach sa window) o hindi. Ang Activity Lifecycle ay namamahala sa estado ng bahagi ng application, ang View Lifecycle — sa pag-drawing ng UI.
requestLayout() ay nagti-trigger ng buong siklo na onMeasure → onLayout → onDraw para sa buong puno ng View mula sa ugat. Kung ang requestLayout() ay madalas na tinatawag (halimbawa bawat frame ng animation), ito ay nagdudulot ng jank at nawawalang frame. Ayon sa Google, ang isang requestLayout ay tumatagal ng average na 2–5 ms sa ViewGroup na may 10 elemento. Para sa mga animation gamitin ang invalidate().
onAttachedToWindow ay tinatawag kapag ang View ay naka-attach sa window (Window) — nagiging bahagi ng nakikitang hierarchy. Sa sandaling ito ang View ay nakakakuha ng HW acceleration at access sa mga mapagkukunan ng Window (WindowManager, Display). Ang onAttachedToWindow ay ang tamang lugar para magrehistro ng mga listener ng animation at BroadcastReceiver na nabubuhay habang nakikita ang View.
Overdraw — sitwasyon kung saan ang isang pixel ay iginuguhit nang maraming beses sa isang frame. Bawat karagdagang pagdaan ay pag-aaksaya ng oras ng GPU. Mga paraan ng pagbawas: itakda ang windowBackground sa theme (huwag gumuhit ng background sa layout), gamitin ang canvas.clipRect(), iwasan ang pagsasama ng mga nested background, ilapat ang ConstraintLayout sa halip na nested LinearLayout. Android Studio → Profile GPU Rendering → Overdraw ay nagpapakita ng color map ng overdraw (asul = 1x, pula = 3x+).
Oo, kung ang View ay may background. Ang super.onDraw() ay iginuguhit ang background ng View. Kung ang iyong custom na View ay walang background o ikaw ay gumuhit ng sarili mong background, ang super.onDraw() ay maaaring laktawan — ito ay nakakatipid ng isang pagdaan ng pag-drawing. Para sa ViewGroup ang super.dispatchDraw() ay sapilitan — ito ay iginuguhit ang mga child View.
Buod
Gagawa kami ng mobile application na turnkey
Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.
Basahin din