View Lifecycle — η ακολουθία μεθόδων που καλεί το Android για την απόδοση και επαναπόδοση ενός στοιχείου διεπαφής χρήστη (View) στην οθόνη. Σε αντίθεση με το Activity ή το Fragment, το View είναι ένα ελαφρύ στοιχείο που δεν έχει εκτεταμένο κύκλο ζωής, αλλά περνά από μια αυστηρή τριφασική διαδικασία: onMeasure (μέτρηση), onLayout (τοποθέτηση), onDraw (σχεδίαση). Η κατανόηση του View Lifecycle είναι απαραίτητη για τη δημιουργία προσαρμοσμένων View, τη βελτιστοποίηση απόδοσης και την επίλυση προβλημάτων απόδοσης. Σύμφωνα με την Google, τα προσαρμοσμένα View επιταχύνουν το UI κατά 15–40% σε σύγκριση με τον συνδυασμό τυπικών ένθετων 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 acceleration) και onDetachedFromWindow (το View αποσπάται, οι πόροι ελευθερώνονται). Αυτές οι μέθοδοι καλούνται μία φορά στη ζωή του View και είναι σημαντικές για την εγγραφή/ακύρωση κινούμενων σχεδίων, αισθητήρων.
Σύμφωνα με το Android Performance Blog, το 65% των προβλημάτων απόδοσης UI (jank, χαμένα καρέ) σχετίζονται με λανθασμένη υλοποίηση των 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().Τυπικό σφάλμα: μη λήψη υπόψη του 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 έχει αλλάξει σε σύγκριση με την προηγούμενη διάταξη. Εάν είναι 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 κάθε απογόνου. Η παράκαμψη του dispatchDraw χρησιμοποιείται για εφαρμογή εφέ πάνω από θυγατρικά στοιχεία.
Σύμφωνα με στατιστικά του Android Vitals, οι συχνότερες αιτίες χαμένων καρέ στο onDraw — δημιουργία αντικειμένων εντός της μεθόδου (48%), κλήση decodeResource (22%) και σύνθετες λειτουργίες με Path χωρίς προσωρινή αποθήκευση (15%).
Invalidation — ο μηχανισμός που ενεργοποιεί την επανασχεδίαση του View. Η κλήση invalidate() επισημαίνει το View ως "βρώμικο" και προγραμματίζει την κλήση onDraw στον επόμενο κύκλο σχεδίασης. Η κλήση requestLayout() — μια πιο "βαριά" λειτουργία που ενεργοποιεί τον πλήρη κύκλο: onMeasure → onLayout → onDraw.
| Μέθοδος | Τι κάνει | Πότε να χρησιμοποιείται |
|---|---|---|
| invalidate() | Καλεί το onDraw χωρίς onMeasure/onLayout | Άλλαξε μόνο η εμφάνιση (χρώμα, κείμενο, πρόοδος) |
| invalidate(Rect) | Επανασχεδιάζει μόνο την καθορισμένη περιοχή | Άλλαξε μέρος του View — κινούμενο σχέδιο, επιλογή |
| postInvalidate() | Καλεί invalidate από μη UI νήμα | Το νήμα παρασκηνίου ενημέρωσε δεδομένα για σχεδίαση |
| 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) για View με κινούμενα σχέδια και 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: μετρά κάθε απόγονο, μεταφέρει σε νέα σειρά όταν υπερβαίνει το πλάτος, υπολογίζει το συνολικό ύψος. Το onLayout τοποθετεί τα παιδιά βάσει συντεταγμένων λαμβάνοντας υπόψη τις αλλαγές σειρών. Η generateLayoutParams επιστρέφει MarginLayoutParams για υποστήριξη περιθωρίων στα θυγατρικά View.
Προσαρμοσμένο 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 διαρκεί κατά μέσο όρο 2–5 ms σε ένα ViewGroup με 10 στοιχεία. Για κινούμενα σχέδια χρησιμοποιήστε invalidate().
onAttachedToWindow καλείται όταν το View προσαρτάται στο παράθυρο (Window) — γίνεται μέρος της ορατής ιεραρχίας. Αυτή τη στιγμή το View λαμβάνει HW acceleration και πρόσβαση στους πόρους του Window (WindowManager, Display). Το onAttachedToWindow είναι το κατάλληλο μέρος για εγγραφή ακροατών κινούμενων σχεδίων και BroadcastReceiver που ζει όσο το View είναι ορατό.
Overdraw — κατάσταση κατά την οποία ένα εικονοστοιχείο σχεδιάζεται πολλές φορές σε ένα καρέ. Κάθε επιπλέον πέρασμα είναι σπατάλη χρόνου GPU. Μέθοδοι μείωσης: ορίστε windowBackground στο theme (μην σχεδιάζετε φόντο στο layout), χρησιμοποιήστε canvas.clipRect(), αποφύγετε τη συγχώνευση ένθετων φόντων, εφαρμόστε ConstraintLayout αντί για ένθετα LinearLayout. Android Studio → Profile GPU Rendering → Overdraw δείχνει χρωματικό χάρτη overdraw (μπλε = 1x, κόκκινο = 3x+).
Ναι, εάν το View έχει φόντο (background). Το super.onDraw() σχεδιάζει το φόντο του View. Εάν το προσαρμοσμένο View δεν έχει φόντο ή σχεδιάζετε το δικό σας φόντο, το super.onDraw() μπορεί να παραλειφθεί — αυτό εξοικονομεί ένα πέρασμα σχεδίασης. Για ViewGroup το super.dispatchDraw() είναι υποχρεωτικό — σχεδιάζει τα θυγατρικά View.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης