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 се изчисляват, като се взема предвид падингът на родителя: 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. В резултат на това децата се показват на ръба на 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също