Understanding View Layout Process: How ViewGroup Differs from View
Core Layout Mechanism in Views
The layout() method is responsible for positioning a View on the screen by establishing its boundaries through four critical parameters: mLeft, mTop, mBottom, and mRight. These values define the rectangular area occupied by the View.
When examining the internal implementation:
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}
return changed;
The method compares incoming coordinates with existing ones. If any boundary differs, the View is marked as changed. The system then records the old dimensions for later comparison, calculates new dimensions, and triggers invalidation if sizing occurred. Finally, all four boundary values are updated along with the render node.
The layout() method essentially performs one operation: it accepts coordinate parameters and assigns them to internal fields. This simplicity enables efficient dimension retrieval:
public final int getWidth() {
return mRight - mLeft;
}
ViewGroup Layout Behavior
When inspecting ViewGroup's layout implementation:
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
mLayoutCalledWhileSuppressed = true;
}
}
Despite the additional checks, ViewGroup ultimately delegates to View's layout() method. The key distinction lies in the onLayout() callback, which is empty in View but abstract in ViewGroup:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
Every ViewGroup subclass must implement onLayout() to position its children. The implementation typically iterates through child views, calling layout() on each to establish their positions.
Building a Custom VerticalLayout
Creating a custom ViewGroup resembling LinearLayout's vertical orientation involves several key steps.
Extend ViewGroup
class VerticalLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr)
Override generateLayoutParams
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams? {
return MarginLayoutParams(context, attrs)
}
This enables margin support for child views.
Implement onMeasure
The measurement phase processes widthMeasureSpec and heightMeasureSpec to determine appropriate dimensions based on parent constraints.
When MeasureSpec.EXACTLY is specified, use the provided size directly. For AT_MOST or UNSPECIFIED modes, calculate dimensions by summing child contributions:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val providedWidth = MeasureSpec.getSize(widthMeasureSpec)
val providedHeight = MeasureSpec.getSize(heightMeasureSpec)
var totalHeight = 0
var maxWidth = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChild(child, widthMeasureSpec, heightMeasureSpec)
val params = child.layoutParams as MarginLayoutParams
val childWidth = child.measuredWidth + params.leftMargin + params.rightMargin
val childHeight = child.measuredHeight + params.topMargin + params.bottomMargin
maxWidth = maxOf(maxWidth, childWidth)
totalHeight += childHeight
}
setMeasuredDimension(
if (widthMode == MeasureSpec.EXACTLY) providedWidth else maxWidth,
if (heightMode == MeasureSpec.EXACTLY) providedHeight else totalHeight
)
}
The ViewGroup's final width equals the widest child (including margins), while height equals the cumulative height of all children.
Implement onLayout
Positioning children requires calculating the top coordinate for each view, incrementing by the previous child's bottom edge:
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var currentTop = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
val params = child.layoutParams as MarginLayoutParams
currentTop += params.topMargin
val left = params.leftMargin
val top = currentTop
val right = left + child.measuredWidth
val bottom = top + child.measuredHeight
child.layout(left, top, right, bottom)
currentTop += child.measuredHeight + params.bottomMargin
}
}
Each child receives fixed left values (from margin) and dynamically calculated top values. The right and bottom derive from width and height respectively.
Usage Example
<com.example.VerticalLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/primary"
android:text="Header" />
<TextView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_marginTop="20dp"
android:background="@color/secondary"
android:text="Content" />
<TextView
android:layout_width="140dp"
android:layout_height="100dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:background="@color/accent"
android:text="Footer" />
</com.example.VerticalLayout>
The layout result displays children stacked vertically, mainatining proper spacing through margin values.
Summary
The layout phase establishes where each View appears on screen. View handles its own positioning, while ViewGroup additionally manages child positioning through onLayout(). The process involves comparing incoming coordinates, updating boundary values, and invalidating regions when dimensions change.