Custom ViewGroup for advanced layouts
Alex Chen
Jan 2026
2 tabs
package com.example.myapp.ui.custom
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import kotlin.math.max
class FlowLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
private var horizontalSpacing = 0
private var verticalSpacing = 0
init {
context.obtainStyledAttributes(attrs, R.styleable.FlowLayout).apply {
horizontalSpacing = getDimensionPixelSize(
R.styleable.FlowLayout_horizontalSpacing, 0
)
verticalSpacing = getDimensionPixelSize(
R.styleable.FlowLayout_verticalSpacing, 0
)
recycle()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
var currentWidth = paddingLeft
var currentHeight = paddingTop
var maxLineHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == GONE) continue
measureChildWithMargins(
child,
widthMeasureSpec,
0,
heightMeasureSpec,
0
)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
val lp = child.layoutParams as MarginLayoutParams
val childWidthWithMargins = childWidth + lp.leftMargin + lp.rightMargin
val childHeightWithMargins = childHeight + lp.topMargin + lp.bottomMargin
if (currentWidth + childWidthWithMargins + paddingRight > widthSize) {
// Move to next line
currentWidth = paddingLeft
currentHeight += maxLineHeight + verticalSpacing
maxLineHeight = 0
}
currentWidth += childWidthWithMargins + horizontalSpacing
maxLineHeight = max(maxLineHeight, childHeightWithMargins)
}
currentHeight += maxLineHeight + paddingBottom
val finalWidth = if (widthMode == MeasureSpec.EXACTLY) widthSize else currentWidth
val finalHeight = if (heightMode == MeasureSpec.EXACTLY) heightSize else currentHeight
setMeasuredDimension(finalWidth, finalHeight)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var currentX = paddingLeft
var currentY = paddingTop
var maxLineHeight = 0
val width = r - l
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == GONE) continue
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
val lp = child.layoutParams as MarginLayoutParams
val childWidthWithMargins = childWidth + lp.leftMargin + lp.rightMargin
if (currentX + childWidthWithMargins + paddingRight > width) {
// Move to next line
currentX = paddingLeft
currentY += maxLineHeight + verticalSpacing
maxLineHeight = 0
}
val left = currentX + lp.leftMargin
val top = currentY + lp.topMargin
val right = left + childWidth
val bottom = top + childHeight
child.layout(left, top, right, bottom)
currentX += childWidthWithMargins + horizontalSpacing
maxLineHeight = max(maxLineHeight, childHeight + lp.topMargin + lp.bottomMargin)
}
}
override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT
)
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
return MarginLayoutParams(p)
}
override fun checkLayoutParams(p: LayoutParams?): Boolean {
return p is MarginLayoutParams
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FlowLayout">
<attr name="horizontalSpacing" format="dimension" />
<attr name="verticalSpacing" format="dimension" />
</declare-styleable>
</resources>
2 files · kotlin, xml
Explain with highlit
Custom ViewGroups enable specialized layout behavior beyond standard containers. I extend ViewGroup and override onMeasure() to measure children and onLayout() to position them. measureChild() or measureChildWithMargins() determines child sizes. Layout params define child-specific configuration. requestLayout() triggers remeasure when properties change. Custom attributes use attrs.xml and obtainStyledAttributes(). Performance matters—minimize allocations in measure/layout passes. Custom ViewGroups power complex UIs like carousels, flow layouts, or masonry grids. They provide precise control over child positioning, enabling designs impossible with standard layouts while maintaining proper Android measurement and drawing contracts.