View Lifecycle — পদ্ধতিগুলোর ক্রম যা Android স্ক্রিনে ইউজার ইন্টারফেস উপাদান (View) আঁকা এবং পুনরায় আঁকার জন্য কল করে। Activity বা Fragment-এর বিপরীতে, View একটি হালকা ওজনের উপাদান যার বিস্তৃত জীবনচক্র নেই, তবে এটি একটি কঠোর তিন-পর্যায়ের প্রক্রিয়ার মধ্য দিয়ে যায়: onMeasure (মাপ), onLayout (অবস্থান নির্ধারণ), onDraw (অঙ্কন)। কাস্টম Views তৈরি, পারফরম্যান্স অপটিমাইজ এবং অঙ্কন সমস্যা সমাধানের জন্য View Lifecycle বোঝা প্রয়োজন। Google-এর মতে, সঠিক বাস্তবায়নে স্ট্যান্ডার্ড নেস্টেড ViewGroups-এর সংমিশ্রণের তুলনায় কাস্টম Views UI-কে 15–40% গতি বাড়ায়। কাস্টম Views-এ Android ডকুমেন্টেশন 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 ত্বরণ আছে) এবং onDetachedFromWindow (View বিচ্ছিন্ন, সম্পদ মুক্ত করা হয়)। এই পদ্ধতিগুলো View-এর জীবনে একবার কল করা হয় এবং অ্যানিমেশন/সেন্সর নিবন্ধন/বাতিল করার জন্য গুরুত্বপূর্ণ।
Android Performance Blog-এর মতে, UI পারফরম্যান্স সমস্যার (jank, ফ্রেম ড্রপ) 65% 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()-এর মাধ্যমে সব সন্তানকে মাপা।সাধারণ ভুল: wrap_content ব্যবহার করার সময় MeasureSpec বিবেচনা না করা। যদি View wrap_content-এ সেট করা থাকে, কিন্তু onMeasure AT_MOST হ্যান্ডেল না করে এবং একটি নির্দিষ্ট আকার ফেরত দেয়, তাহলে View হয় ক্লিপ হবে অথবা প্রয়োজনের চেয়ে বেশি জায়গা নেবে।
onLayout — সেই পর্যায় যেখানে View বা ViewGroup তার সন্তানদের সাজায় নিজস্ব সীমানার ভেতরে। সাধারণ View-এর (ViewGroup নয়) জন্য, onLayout প্রয়োজন নেই — সিস্টেম প্যারেন্ট থেকে পাঠানো প্যারামিটারসহ layout() কল করে। ViewGroup-এর জন্য, onLayout বাধ্যতামূলক — এটি ছাড়া চাইল্ড Views স্থাপিত হবে না।
onLayout স্বাক্ষর:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// চাইল্ড Views সাজানো
}
changed প্যারামিটার নির্দেশ করে View-এর অবস্থান বা আকার পূর্ববর্তী layout-এর তুলনায় পরিবর্তিত হয়েছে কিনা। যদি false হয়, View অপটিমাইজেশনের জন্য চাইল্ড অবস্থানের পুনর্গণনা এড়িয়ে যেতে পারে।
ViewGroup-এর জন্য, onLayout করা উচিত:
getChildCount() এবং getChildAt(i)-এর মাধ্যমে সব সন্তানের উপর পুনরাবৃত্তি করা।child.layout(l, t, r, b) কল করা।onMeasure-এর পরে onLayout কল করা হয় — মাপা মাত্রা getMeasuredWidth()/getMeasuredHeight()-এর মাধ্যমে উপলব্ধ। যদি layout()-এর পরে কোনো চাইল্ড View-এর প্রকৃত মাত্রা ভিন্ন হয়, তাহলে পুনরায় মাপার জন্য requestLayout() কল করা হবে। এটিকে “layout পাস” বলা হয় এবং এটি পুনর্গণনার চেইন প্রতিক্রিয়া ট্রিগার করতে পারে।
onDraw — সেই পর্যায় যেখানে View নিজেকে Canvas-এ আঁকে। এটি একমাত্র পর্যায় যা onMeasure এবং onLayout ছাড়া একাধিকবার কল করা যেতে পারে — যদি View-কে invalidate() হিসেবে চিহ্নিত করা হয়। Canvas অঙ্কন API সরবরাহ করে: drawLine, drawRect, drawCircle, drawText, drawBitmap, এবং drawPath।
onDraw নিয়ম:
canvas.clipRect() ব্যবহার করুন।ViewGroup-এ অঙ্কন ক্রম: পটভূমি (setBackgroundDrawable) → onDraw (বিষয়বস্তু) → dispatchDraw (চাইল্ড Views) → onDrawForeground (অগ্রভূমি)। dispatchDraw প্রতিটি চাইল্ডের onDraw কল করে। dispatchDraw ওভাররাইড করা চাইল্ড উপাদানের উপরে প্রভাব প্রয়োগ করতে ব্যবহৃত হয়।
Android Vitals পরিসংখ্যান অনুসারে, onDraw-এ ফ্রেম ড্রপের সবচেয়ে সাধারণ কারণ হল পদ্ধতির ভেতরে অবজেক্ট তৈরি (48%), decodeResource কল (22%), এবং ক্যাশিং ছাড়া জটিল Path অপারেশন (15%)।
অবৈধকরণ — সেই প্রক্রিয়া যা View পুনরাঙ্কন ট্রিগার করে। invalidate() কল করা View-কে “নোংরা” হিসেবে চিহ্নিত করে এবং পরবর্তী অঙ্কন চক্রে onDraw কল নির্ধারণ করে। requestLayout() কল করা আরও “ভারী” অপারেশন, যা পূর্ণ চক্র ট্রিগার করে: onMeasure → onLayout → onDraw।
| পদ্ধতি | এটি কী করে | কখন ব্যবহার করবেন |
|---|---|---|
| invalidate() | onMeasure/onLayout ছাড়া onDraw ট্রিগার করে | শুধুমাত্র চেহারা বদলালে (রং, টেক্সট, অগ্রগতি) |
| invalidate(Rect) | শুধুমাত্র নির্দিষ্ট এলাকা পুনরায় আঁকে | View-এর অংশ বদলালে — অ্যানিমেশন, নির্বাচন |
| postInvalidate() | অ-UI থ্রেড থেকে invalidate কল করে | ব্যাকগ্রাউন্ড থ্রেড অঙ্কনের জন্য ডেটা আপডেট করলে |
| requestLayout() | onMeasure → onLayout → onDraw ট্রিগার করে | বিষয়বস্তুর আকার বদলালে (টেক্সট, ছবি) |
| forceLayout() | বলপূর্বক পুনর্মাপনের জন্য View চিহ্নিত করে | অভ্যন্তরীণ অবস্থা বদলালে, আকার বদলাতে পারে |
অ্যানিমেশন এবং View Lifecycle: ViewPropertyAnimator এবং ValueAnimator প্রতিটি অ্যানিমেশন ফ্রেমে invalidate() কল করে। ObjectAnimator View-এ setter কল করে, যা যদি setter আকার (প্রস্থ/উচ্চতা) বদলায়, স্বয়ংক্রিয়ভাবে requestLayout() কল করে। এটি জটিল ViewGroups-এর জন্য ব্যয়বহুল হতে পারে: প্রতিটি requestLayout রুট ভিউ পর্যন্ত পূর্ণ শ্রেণিবিন্যাস ট্রিগার করে।
অপটিমাইজেশন নিয়ম: যেখানে শুধুমাত্র চেহারা বদলায় (রং, স্বচ্ছতা, আকার না বদলে ঘূর্ণন) সেখানে requestLayout()-এর পরিবর্তে invalidate() ব্যবহার করুন। requestLayout শুধুমাত্র আকার বা বিষয়বস্তু পরিবর্তন করার সময় ব্যবহার করুন যা আকারকে প্রভাবিত করে।
কাস্টম Views অনন্য UI তৈরি করার জন্য একটি শক্তিশালী হাতিয়ার, তবে তাদের পারফরম্যান্স নিয়ম কঠোরভাবে মেনে চলতে হবে। View Lifecycle অপটিমাইজেশনের জন্য Google-এর মূল সুপারিশ এখানে দেওয়া হল।
setLayerType(LAYER_TYPE_HARDWARE) এবং শেষে 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 যা চাইল্ড Viewsকে সারিতে সাজায় (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 চাইল্ড Views-এ মার্জিন সমর্থনের জন্য MarginLayoutParams ফেরত দেয়।
একটি কাস্টম View একটি মসৃণ Bezier বক্ররেখা আঁকে, 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 শুধুমাত্র তখন পুনর্গণনা হয় যখন এটি “নোংরা” হয়। এটি প্রতি অ্যানিমেশন ফ্রেমে Bezier বক্ররেখার পুনর্গণনা প্রতিরোধ করে, CPU বাঁচায়।
সচরাচর জিজ্ঞাসা
View Lifecycle একটি চক্রীয় অঙ্কন প্রক্রিয়া (onMeasure → onLayout → onDraw) যা Activity তৈরি/ধ্বংস থেকে স্বাধীন। View-এ onStart/onStop নেই — এটি হয় দৃশ্যমান (উইন্ডোর সাথে সংযুক্ত) অথবা নয়। Activity Lifecycle অ্যাপ্লিকেশন উপাদানের অবস্থা পরিচালনা করে, View Lifecycle UI অঙ্কন পরিচালনা করে।
requestLayout() মূল থেকে সম্পূর্ণ View ট্রির জন্য পূর্ণ onMeasure → onLayout → onDraw চক্র ট্রিগার করে। যদি requestLayout() ঘন ঘন কল করা হয় (যেমন, প্রতি অ্যানিমেশন ফ্রেম), তাহলে এটি jank এবং ড্রপড ফ্রেম ঘটায়। Google-এর মতে, ১০টি উপাদানের ViewGroup-এ একটি requestLayout গড়ে 2–5 ms নেয়। অ্যানিমেশনের জন্য, invalidate() ব্যবহার করুন।
onAttachedToWindow কল করা হয় যখন View একটি Window-এর সাথে সংযুক্ত হয় — দৃশ্যমান শ্রেণিবিন্যাসের অংশ হয়। এই মুহূর্তে, View HW ত্বরণ এবং Window সম্পদ (WindowManager, Display)-এ অ্যাক্সেস পায়। onAttachedToWindow অ্যানিমেশন শ্রোতা এবং BroadcastReceivers নিবন্ধনের সঠিক জায়গা যা View দৃশ্যমান থাকা পর্যন্ত জীবিত থাকে।
ওভারড্র হল এমন একটি অবস্থা যখন একটি পিক্সেল একক ফ্রেমে একাধিকবার আঁকা হয়। প্রতিটি অতিরিক্ত পাস GPU সময় নষ্ট করে। কমানোর পদ্ধতি: থিমে windowBackground সেট করুন (layout-এ পটভূমি আঁকবেন না), canvas.clipRect() ব্যবহার করুন, নেস্টেড পটভূমি একত্রিত করা এড়িয়ে চলুন, নেস্টেড LinearLayout-এর পরিবর্তে ConstraintLayout ব্যবহার করুন। Android Studio → Profile GPU Rendering → Overdraw ওভারড্রের রঙ মানচিত্র দেখায় (নীল = 1x, লাল = 3x+)।
হ্যাঁ, যদি View-এর পটভূমি থাকে। super.onDraw() View-এর পটভূমি আঁকে। যদি আপনার কাস্টম View-এর পটভূমি না থাকে বা আপনি নিজস্ব পটভূমি আঁকেন, তাহলে super.onDraw() বাদ দেওয়া যেতে পারে — এটি একটি অঙ্কন পাস বাঁচায়। ViewGroup-এর জন্য, super.dispatchDraw() বাধ্যতামূলক — এটি চাইল্ড Views আঁকে।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন