View Lifecycle — دنبالهای از متدها است که Android برای نمایش و بازنمایی عنصر رابط کاربری (View) روی صفحه فراخوانی میکند. برخلاف Activity یا Fragment، View یک مؤلفه سبکوزن است که چرخه حیات گستردهای ندارد، اما فرآیند سه فازی دقیقی را طی میکند: onMeasure (اندازهگیری)، onLayout (جانمایی)، onDraw (رسم). درک View Lifecycle برای ایجاد Viewهای سفارشی، بهینهسازی عملکرد و رفع مشکلات نمایش ضروری است. طبق دادههای Google، Viewهای سفارشی UI را ۱۵–۴۰٪ سریعتر میکنند در مقایسه با ترکیب ViewGroupهای تو در توی استاندارد در صورت پیادهسازی صحیح. مستندات Android درباره Viewهای سفارشی 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، افت فریم) به پیادهسازی نادرست onMeasure و onDraw مربوط است: override بیش از حد، فراخوانی 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() اندازهگیری کند.خطای رایج: عدم در نظر گرفتن MeasureSpec در wrap_content. اگر View روی wrap_content تنظیم شده باشد، اما onMeasure AT_MOST را پردازش نکند و اندازه ثابت برگرداند، View یا بریده میشود یا فضای بیشتری از نیاز اشغال میکند.
onLayout — فازی که در آن View یا ViewGroup فرزندان خود را درون مرزهایش قرار میدهد. برای View معمولی (غیر ViewGroup) onLayout لازم نیست — سیستم خود layout() را با پارامترهای ارسالی از والد فراخوانی میکند. برای ViewGroup onLayout اجباری است — بدون آن Viewهای فرزند جانمایی نمیشوند.
امضای onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// چیدمان Viewهای فرزند
}
پارامتر changed نشان میدهد که آیا موقعیت یا اندازه View نسبت به layout قبلی تغییر کرده است. اگر false باشد — View میتواند برای بهینهسازی محاسبه مجدد موقعیت فرزندان را نادیده بگیرد.
برای ViewGroup onLayout باید:
getChildCount() و getChildAt(i) پیمایش کند.child.layout(l, t, r, b) را برای هر فرزند فراخوانی کند.onLayout بعد از onMeasure فراخوانی میشود — ابعاد اندازهگیری شده از طریق getMeasuredWidth()/getMeasuredHeight() در دسترس هستند. اگر View فرزند بعد از layout() ابعاد واقعی متفاوتی داشته باشد، requestLayout() برای اندازهگیری مجدد فراخوانی میشود. این «layout pass» نامیده میشود و میتواند واکنش زنجیرهای محاسبات مجدد ایجاد کند.
onDraw — فازی که در آن View خود را روی Canvas رسم میکند. این تنها فازی است که میتواند多次 بدون onMeasure و onLayout فراخوانی شود — اگر View به عنوان invalidate() علامتگذاری شده باشد. Canvas API برای رسم فراهم میکند: drawLine، drawRect، drawCircle، drawText، drawBitmap و drawPath.
قوانین onDraw:
canvas.clipRect() برای برش بخشهای نامرئی استفاده کنید.ترتیب رسم در ViewGroup: پسزمینه (setBackgroundDrawable) → onDraw (محتوا) → dispatchDraw (Viewهای فرزند) → onDrawForeground (پیشزمینه). dispatchDraw onDraw هر فرزند را فراخوانی میکند. override dispatchDraw برای اعمال افکتها روی عناصر فرزند استفاده میشود.
طبق آمار Android Vitals، شایعترین دلایل افت فریم در onDraw — ایجاد اشیاء درون متد (۴۸٪)، فراخوانی decodeResource (۲۲٪) و عملیات پیچیده با Path بدون کش (۱۵٪).
Invalidation — مکانیزمی که بازنمایی View را راهاندازی میکند. فراخوانی invalidate() View را به عنوان «کثیف» علامتگذاری میکند و فراخوانی onDraw را در چرخه بعدی رسم برنامهریزی میکند. فراخوانی requestLayout() — عملیات «سنگینتری» است که چرخه کامل را راهاندازی میکند: onMeasure → onLayout → onDraw.
| متد | چه کاری انجام میدهد | چه زمانی استفاده کنیم |
|---|---|---|
| invalidate() | onDraw را بدون onMeasure/onLayout فراخوانی میکند | فقط ظاهر تغییر کرده است (رنگ، متن، پیشرفت) |
| invalidate(Rect) | فقط ناحیه مشخص شده را بازنمایی میکند | بخشی از View تغییر کرده — انیمیشن، انتخاب |
| postInvalidate() | invalidate را از thread غیر UI فراخوانی میکند | thread پسزمینه دادههای رسم را بهروز کرده است |
| requestLayout() | onMeasure → onLayout → onDraw را راهاندازی میکند | اندازه محتوا تغییر کرده است (متن، تصویر) |
| forceLayout() | View را برای اندازهگیری مجدد اجباری علامتگذاری میکند | وضعیت داخلی تغییر کرده، اندازه ممکن است تغییر کرده باشد |
انیمیشنها و View Lifecycle: ViewPropertyAnimator و ValueAnimator در هر فریم انیمیشن invalidate() فراخوانی میکنند. ObjectAnimator setter را روی View فراخوانی میکند که اگر setter اندازه (width/height) را تغییر دهد، به طور خودکار requestLayout() را فراخوانی میکند. این میتواند برای ViewGroupهای پیچیده هزینهبر باشد: هر requestLayout سلسلهمراتب کامل را تا view ریشه راهاندازی میکند.
قانون بهینهسازی: invalidate() به جای requestLayout() در هر جایی که فقط ظاهر تغییر میکند (رنگ، شفافیت، چرخش بدون تغییر اندازه). requestLayout را فقط هنگام تغییر اندازهها یا محتوای مؤثر بر اندازه استفاده کنید.
Viewهای سفارشی — ابزاری قدرتمند برای ایجاد UI منحصر به فرد، اما نیازمند رعایت دقیق قوانین عملکرد هستند. در اینجا توصیههای کلیدی Google برای بهینهسازی View Lifecycle آورده شده است.
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 سفارشی که Viewهای فرزند را در ردیفها قرار میدهد (مانند 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 را override میکند: هر فرزند را اندازهگیری میکند، هنگام تجاوز از عرض به ردیف جدید منتقل میکند، ارتفاع کل را محاسبه میکند. onLayout کودکان را با مختصات با در نظر گرفتن انتقال خطوط قرار میدهد. generateLayoutParams برای پشتیبانی از margin در Viewهای فرزند، 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() چرخه کامل onMeasure → onLayout → onDraw را برای کل درخت View از ریشه راهاندازی میکند. اگر requestLayout() مکرراً فراخوانی شود (مثلاً هر فریم انیمیشن)، باعث jank و افت فریم میشود. طبق Google، یک requestLayout به طور متوسط ۲–۵ میلیثانیه در ViewGroup با ۱۰ عنصر زمان میبرد. برای انیمیشنها از invalidate() استفاده کنید.
onAttachedToWindow زمانی فراخوانی میشود که View به پنجره (Window) متصل میشود — بخشی از سلسلهمراتب قابل مشاهده میشود. در این لحظه View شتاب HW و دسترسی به منابع Window (WindowManager، Display) را دریافت میکند. onAttachedToWindow مکان مناسبی برای ثبت شنوندگان انیمیشن و BroadcastReceiver است که تا زمانی که View قابل مشاهده است زندگی میکند.
Overdraw — وضعیتی است که در آن یک پیکسل چندین بار در یک فریم رسم میشود. هر عبور اضافی اتلاف زمان GPU است. روشهای کاهش: windowBackground را در theme تنظیم کنید (در layout پسزمینه رسم نکنید)، از canvas.clipRect() استفاده کنید، از ادغام پسزمینههای تو در تو خودداری کنید، به جای LinearLayoutهای تو در تو از ConstraintLayout استفاده کنید. Android Studio → Profile GPU Rendering → Overdraw نقشه رنگی overdraw را نشان میدهد (آبی = 1x، قرمز = 3x+).
بله، اگر View پسزمینه (background) داشته باشد. super.onDraw() پسزمینه View را رسم میکند. اگر View سفارشی شما پسزمینه ندارد یا پسزمینه خود را رسم میکنید، super.onDraw() قابل چشمپوشی است — این کار باعث صرفهجویی در یک عبور رسم میشود. برای ViewGroup super.dispatchDraw() اجباری است — Viewهای فرزند را رسم میکند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید