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
    }
}
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.