onLayout() — είναι η μέθοδος της κλάσης ViewGroup που καθορίζει τις θέσεις και τα μεγέθη των θυγατρικών View στο επίπεδο συντεταγμένων του γονεϊκού δοχείου. Το σύστημα Android καλεί το onLayout μετά από τη φάση μέτρησης (onMeasure), όταν για κάθε θυγατρικό View είναι ήδη γνωστά τα μετρημένα πλάτος και ύψος. Σύμφωνα με την Android Developers Documentation (2026), το onLayout είναι υποχρεωτική μέθοδος για υπέρβαση σε κάθε προσαρμοσμένη ViewGroup, καθώς η τυπική υλοποίηση ViewGroup δεν εκτελεί αυτόματη τοποθέτηση παιδιών.
Βασικά σημεία
onLayout(boolean changed, int l, int t, int r, int b) — είναι μια protected μέθοδος της κλάσης ViewGroup που καλείται από το σύστημα για την τοποθέτηση των θυγατρικών View μέσα στο γονεϊκό δοχείο. Ο προγραμματιστής υπερκαλύπτει αυτήν τη μέθοδο όταν δημιουργεί μια προσαρμοσμένη ViewGroup με μη τυπική διαταξή στοιχείων: σε ακολουθία, πλέγμα, σχήμα σκακιές ή σε τυχαίες συντεταγμένες. Κάθε θυγατρικό View λαμβάνει τα τελικά όριά του μέσω της κλήσης child.layout().
Η παράμετρος changed υποδεικνύει εάν η θέση ή το μέγεθος της ίδιας της ViewGroup έχει αλλάξει από το τελευταίο layout. Αν το changed είναι true, όλα τα θυγατρικά στοιχεία πιθανώς χρειάζονται επισης εκ νέου τοποθέτηση. Οι παράμετροι l, t, r, b είναι οι συντεταγμένες της αριστερής επάνω και δεξιάς κάτω γωνίας της ViewGroup στο σύστημα συντεταγμένων του γονέα της. Μέσα στο onLayout, ο προγραμματιστής χρησιμοποιεί αυτές τις τιμές ως αρχικές συντεταγμένες για τη διαταξή των παιδιών.
ViewGroup — είναι η μοναδική κλάση που υπερκαλύπτει το onLayout. Ένα συνηθισμένο View (όχι ViewGroup) δεν έχει θυγατρικά στοιχεία και δεν χρειάζεται onLayout — η δική του τοποθέτηση εξυπηρετείται από το γονεϊκό δοχείο. Ακόμη και αν ένα συνηθισμένο View υπερκαλύψει το onLayout, το σύστημα δεν θα το καλέσει. Αυτή είναι η θεμελιώδης διαφορά μεταξύ onLayout και onMeasure, που καλείται για κάθε View.
Η φάση layout ξεκινάει με την κλήση της δημόσιας μεθόδου layout(int l, int t, int r, int b) στο ριζικό View. Αυτή η μέθοδος ορίζει τις τελικές συντεταγμένες του ίδιου του View και καλεί το onLayout εάν το View είναι ViewGroup. Στη συνέχεια, το onLayout καλεί αναδρομικά το child.layout() για κάθε θυγατρικό στοιχείο, και η διαδικασία επαναλαμβάνεται προς τα κάτω της ιεραρχίας. Με αυτόν τον τρόπο, το layout διαδίδεται από τη ρίζα προς τα φύλλα.
Πριν από την κλήση του onLayout, το σύστημα ελέγχει εάν τα μεγέθη του View έχουν αλλάξει σε σχέση με τον προηγούμενο κύκλο. Αν τα μεγέθη δεν έχουν αλλάξει και δεν έχει κληθεί requestLayout, το onLayout μπορεί να μην κληθεί — το σύστημα χρησιμοποιεί τα αποτελέσματα του προηγούμενου layout. Αυτό είναι μια βελτιστοποίηση που αποτρέπει την περιττή επανυπολογισμό θέσεων κατά τη διάρκεια ανιματισμών ή κύλισης, όταν αλλάζει μόνο το περιεχόμενο, όχι τα μεγέθη.
requestLayout() — είναι μια μέθοδος View που ενημερώνει το σύστημα ότι το layout του View έχει λήξει και χρειάζεται επανυπολογισμό. Η κλήση requestLayout οδηγεί σε έναν πλήρη κύκλο: πρώτα καλείται το onMeasure, έπειτα το onLayout, έπειτα το onDraw. Σε αντίθεση με το invalidate, που εκκινεί μόνο την επανασχεδίαση, το requestLayout εκκινεί τον πλήρη επανυπολογισμό μεγεθών και θέσεων. Η υπερβολική κλήση requestLayout είναι συνηθισμένη αιτία προβλημάτων απόδοσης.
l (left) — η συντεταγμένη X της αριστερής ακμής της ViewGroup στο σύστημα συντεταγμένων του γονέα της. t (top) — η συντεταγμένη Y της άνω ακμής. r (right) — η συντεταγμένη X της δεξιάς ακμής. b (bottom) — η συντεταγμένη Y της κάτω ακμής. Το πλάτος της ViewGroup υπολογίζεται ως r - l, το ύψος ως b - t. Αυτές οι συντεταγμένες περιλαμβάνουν ήδη όλα τα padding της ίδιας της ViewGroup.
Μέσα στο onLayout, ο προγραμματιστής καλεί το child.layout(int childLeft, int childTop, int childRight, int childBottom) για κάθε θυγατρικό View. Οι συντεταγμένες που μεταβίδονται στο child.layout πρέπει να είναι στο σύστημα συντεταγμένων της γονεϊκής ViewGroup. Συνήθως, τα childLeft και childTop υπολογίζονται λαμβάνοντας υπόψη τα padding του γονέα: childLeft = l + paddingLeft + offsetX, childTop = t + paddingTop + offsetY.
| Παράμετρος | Περιγραφή | Τυπική χρήση |
|---|---|---|
| l (left) | Συντεταγμένη αριστερής ακμής ViewGroup στον γονέα | Ρχειο σημείο στον άξονα X για θυγατρικά στοιχεία |
| t (top) | Συντεταγμένη άνω ακμής ViewGroup στον γονέα | Ρχειο σημείο στον άξονα Y για θυγατρικά στοιχεία |
| r (right) | Συντεταγμένη δεξιάς ακμής ViewGroup στον γονέα | Άνω όριο πλάτους, r - l = getWidth() |
| b (bottom) | Συντεταγμένη κάτω ακμής ViewGroup στον γονέα | Άνω όριο ύψους, b - t = getHeight() |
Συντεταγμένες παιδιού υπολογίζονται με τον τύπο: childLeft = l + paddingLeft + (marginLeft if present), childRight = childLeft + child.getMeasuredWidth(). Ομοίως για την κάθετη: childTop = t + paddingTop + (marginTop), childBottom = childTop + child.getMeasuredHeight(). Μετά από τον υπολογισμό αυτών των τεσσάρων τιμών, καλείται το child.layout(childLeft, childTop, childRight, childBottom).
Ας δημιουργήσουμε ένα FlowLayout — μια προσαρμοσμένη ViewGroup που τοποθετεί τα θυγατρικά View σε σειρές, μεταφέροντας τα στοιχεία σε νέα σειρά όταν η τρέχουσα σειρά γεμίσει. Αυτό είναι το ανάλογο του Flexbox με wrap σε ένα επίπεδο. Το onLayout επαναλαμβάνει όλα τα θυγατρικά View, υπολογίζει τη θέση για κάθε ένα και καλεί το child.layout() με τα σωστά όρια.
class FlowLayout(context: Context)
: ViewGroup(context) {
private val horizontalSpacing = 12
private val verticalSpacing = 8
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
val parentWidth =
MeasureSpec.getSize(widthMeasureSpec)
var rowX = paddingLeft
var rowY = paddingTop
var maxRowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child,
widthMeasureSpec, 0,
heightMeasureSpec, 0)
if (rowX + child.measuredWidth >
parentWidth - paddingRight) {
rowX = paddingLeft
rowY += maxRowHeight + verticalSpacing
maxRowHeight = 0
}
rowX += child.measuredWidth +
horizontalSpacing
maxRowHeight = maxOf(maxRowHeight,
child.measuredHeight)
}
val totalHeight = rowY + maxRowHeight +
paddingBottom
setMeasuredDimension(
resolveSize(parentWidth, widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec))
}
override fun onLayout(changed: Boolean,
l: Int, t: Int,
r: Int, b: Int) {
val parentWidth = r - l
var rowX = paddingLeft
var rowY = paddingTop
var maxRowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
val cw = child.measuredWidth
val ch = child.measuredHeight
if (rowX + cw >
parentWidth - paddingRight) {
rowX = paddingLeft
rowY += maxRowHeight + verticalSpacing
maxRowHeight = 0
}
child.layout(rowX, rowY,
rowX + cw, rowY + ch)
rowX += cw + horizontalSpacing
maxRowHeight =
maxOf(maxRowHeight, ch)
}
}
override fun generateLayoutParams(attrs: AttributeSet?)
: LayoutParams =
MarginLayoutParams(context, attrs)
}
onMeasure και onLayout — είναι δύο διαδοχικές φάσεις του κύκλου ζωής View που εκτελούν βασικά διαφορετικές εργασίες. Το onMeasure καθορίζει τα επιθυμητά (measured) μεγέθη του View, ενώ το onLayout ορίζει τις πραγματικές (actual) συντεταγμένες και μεγέθη. Η βασική διαφορά: στο onMeasure τα μεγέθη μπορούν να είναι προσωρινά και αργότερα να διορθωθούν από τον γονέα, ενώ στο onLayout καθορίζεται η τελική θέση κάθε θυγατρικού View.
onMeasure καλείται για κάθε View, συμπεριλαμβανομένων των φύλλων (TextView, ImageView, Button). onLayout καλείται μόνο για ViewGroup. Αυτό εξηγείται από το γεγονός ότι η τοποθέτηση είναι ευθύνη του γονεϊκού δοχείου, όχι του ίδιου του View. Το φύλλο View λαμβάνει τη θέση του μέσω του layout() που καλείται από το onLayout του γονέα.
getMeasuredWidth() και getMeasuredHeight() είναι διαθέσιμα μετά από onMeasure, ενώ τα getWidth() και getHeight() — μόνο μετά από onLayout. Εάν προσπελάσετε το getWidth() μέσα στο onMeasure, θα επιστραφεί η τιμή από τον προηγούμενο κύκλο ή μηδέν. Επομένως, για τον υπολογισμό μεγεθών στο onMeasure πρέπει να χρησιμοποιείτε MeasureSpec και διαδοχικά children.
Τοποθέτηση χωρίς υπόψη padding — το πρώτο λάθος κατά την υλοποίηση του onLayout. Ο προγραμματιστής συχνά ξεχνάει να προσθέσει τα paddingLeft και paddingTop του γονέα στις αρχικές συντεταγμένες των θυγατρικών View. Ως αποτέλεσμα, τα children εμφανίζονται στην άκρη της ViewGroup, αγνοώντας αποστάσεις που ορίστηκαν μέσω setPadding() ή σε XML. Σωστός υπολογισμός: childLeft = paddingLeft + offsetX.
Κλήση layout για αόρατα παιδιά — το δεύτερο συνηθισμένο πρόβλημα. Εάν η ViewGroup περιέχει θυγατρικά View με visibility ίσο με GONE, δεν χρειάζεται να τοποθετηθούν — δεν καταλαμβάνουν χώρο. Ωστόσο, το onLayout πρέπει να χειρίζεται σωστά αυτήν την περίπτωση, παραλείποντας τα GONE παιδιά. Για τα INVISIBLE παιδιά, το layout πρέπει ακόμα να κληθεί — διατηρούν τη θέση τους, αν και δεν εμφανίζονται.
Παράβλεψη της παραμέτρου changed — το τρίτο λάθος. Η παράμετρος changed υποδεικνύει εάν τα μεγέθη ή η θέση της ViewGroup έχουν αλλάξει. Αν changed == false, μπορούν να χρησιμοποιηθούν οι αποθηκευμένες συντεταγμένες και δεν χρειάζεται επανυπολογισμός layout όλων των θυγατρικών στοιχείων. Ωστόσο, η πλήρης αποθήκευση layout είναι δύσκολη εργασία και στις περισσότερες υλοποιήσεις onLayout απλώς επανυπολογίζονται όλα τα στοιχεία κάθε φορά. Αυτό είναι αποδεκτό για μικρό αριθμό παιδιών.
Συχνές Ερωτήσεις
Μπορείτε, εάν η ViewGroup χρησιμοποιεί τυπικά LayoutParams και δεν προσθέτει προσαρμοσμένη λογική τοποθέτησης. Ωστόσο, η τυπική υλοποίηση onLayout σε ViewGroup δεν εκτελεί καμία ενέργεια — τα θυγατρικά στοιχεία δεν θα τοποθετηθούν. Στην πράξη, όλες οι ViewGroup (LinearLayout, RelativeLayout, FrameLayout) υπερκαλύπτουν το onLayout.
layout() — είναι η δημόσια τελική μέθοδος View, που καλείται από το σύστημα ή από τη γονεϊκή ViewGroup. Ορίζει τις συντεταγμένες του ίδιου του View και καλεί το onLayout εάν το View είναι ViewGroup. onLayout() — είναι μια protected μέθοδος που υπερκαλύπτεται από τον προγραμματιστή για προσαρμοσμένη διαταξή των θυγατρικών στοιχείων.
Τεχνικά — ναι, μπορεί. Αλλά αυτό δεν συνιστάται καθόλου, καθώς οδηγεί σε απέραντη αναδρομή: requestLayout → onMeasure → onLayout → requestLayout. Εάν το requestLayout κληθεί μέσα στο onLayout, το σύστημα θα ρίξει την εξαίρεση StackOverflowError. Όλες οι αλλαγές μεγεθών πρέπει να γίνονται πριν από το onLayout.
Ανιματισμοί layout (LayoutTransition) παρακολουθούν τις αλλαγές θέσεων των θυγατρικών View και εφαρμόζουν ανιματισμό μετάβασης. Με ενεργή τη LayoutTransition, το onLayout πρώτα ορίζει τις τελικές θέσεις, και στη συνέχεια η LayoutTransition ανιματοποιεί την μετακίνηση από την παλιά στη νέα θέση. Αυτό απαιτεί σωστή υλοποίηση onLayout με σωστές τελικές συντεταγμένες.
invalidate() εκκινεί μόνο τη φάση draw (επανασχεδίαση), χωρίς να επηρεάζει τα measure και layout. Για να ενεργοποιήσετε το onLayout, πρέπει να καλέσετε requestLayout(), που εκκινεί έναν πλήρη κύκλο: measure → layout → draw. Το invalidate είναι πιο αποδοτικό για ενημέρωση της εμφάνισης όταν τα μεγέθη και οι θέσεις δεν αλλάζουν.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης