Custom Android ViewGroup: Overriding onLayout() in Kotlin for Advanced UI

When developing Android applications using Kotlin with XML layouts, creating custom ViewGroups provides a powerful way to manage and arrange child views. Overriding the onLayout() method is crucial for defining how these child views are positioned within the custom ViewGroup. This blog post will explore how to override onLayout() effectively, with comprehensive code examples to guide you.

What is a Custom ViewGroup?

A ViewGroup is a subclass of View that acts as a container to hold other Views, also known as child views. Custom ViewGroups allow developers to create complex and reusable UI components by defining their own layout behavior.

Why Override onLayout()?

The onLayout() method is called when a ViewGroup should assign a size and position to each of its children. By overriding this method, you have direct control over how each child view is laid out, enabling you to create unique and custom layouts.

How to Override onLayout() in a Custom ViewGroup

Follow these steps to override onLayout() effectively:

Step 1: Create a Custom ViewGroup Class

First, create a Kotlin class that extends ViewGroup. This will be the foundation for your custom layout.


import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup

class CustomLayout : ViewGroup {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        // Layout logic will be implemented here
    }
}

Step 2: Implement the onMeasure() Method

Before implementing onLayout(), you need to handle the measurement of child views in the onMeasure() method. This is essential to determine the size requirements of the child views.


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    var totalWidth = 0
    var totalHeight = 0

    // Measure each child
    for (i in 0 until childCount) {
        val child = getChildAt(i)
        if (child.visibility != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec)
            
            // Accumulate the dimensions
            totalWidth += child.measuredWidth
            totalHeight = maxOf(totalHeight, child.measuredHeight)
        }
    }

    // Consider padding
    totalWidth += paddingLeft + paddingRight
    totalHeight += paddingTop + paddingBottom

    // Resolve the final size based on the provided specifications
    val resolvedWidth = resolveSize(totalWidth, widthMeasureSpec)
    val resolvedHeight = resolveSize(totalHeight, heightMeasureSpec)

    setMeasuredDimension(resolvedWidth, resolvedHeight)
}

Step 3: Override the onLayout() Method

Now, override the onLayout() method to define how child views are positioned. The parameters include the changed flag, and the left, top, right, and bottom positions of the ViewGroup.


override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    var currentLeft = paddingLeft
    var currentTop = paddingTop

    for (i in 0 until childCount) {
        val child = getChildAt(i)
        if (child.visibility != GONE) {
            val childWidth = child.measuredWidth
            val childHeight = child.measuredHeight

            // Place the child
            child.layout(currentLeft, currentTop, currentLeft + childWidth, currentTop + childHeight)

            // Update the current left position for the next child
            currentLeft += childWidth
        }
    }
}

Step 4: Add Custom Attributes (Optional)

You can define custom attributes for your ViewGroup in res/values/attrs.xml:


<resources>
    <declare-styleable name="CustomLayout">
        <attr name="spacing" format="dimension" />
    </declare-styleable>
</resources>

Retrieve these attributes in the constructor:


class CustomLayout(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    private var spacing = 0

    init {
        attrs?.let {
            val typedArray = context.obtainStyledAttributes(it, R.styleable.CustomLayout)
            spacing = typedArray.getDimensionPixelSize(R.styleable.CustomLayout_spacing, 0)
            typedArray.recycle()
        }
    }

    // ... (rest of the code)
}

Step 5: Use the Custom Attributes in onLayout()

Incorporate the custom attributes to modify the layout behavior:


override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    var currentLeft = paddingLeft
    var currentTop = paddingTop

    for (i in 0 until childCount) {
        val child = getChildAt(i)
        if (child.visibility != GONE) {
            val childWidth = child.measuredWidth
            val childHeight = child.measuredHeight

            // Place the child with spacing
            child.layout(currentLeft, currentTop, currentLeft + childWidth, currentTop + childHeight)

            // Update the current left position for the next child, considering spacing
            currentLeft += childWidth + spacing
        }
    }
}

Complete Example


import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import android.view.View
import androidx.core.view.children

class CustomLayout : ViewGroup {

    private var spacing = 0

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        attrs?.let {
            val typedArray = context.obtainStyledAttributes(it, R.styleable.CustomLayout)
            spacing = typedArray.getDimensionPixelSize(R.styleable.CustomLayout_spacing, 0)
            typedArray.recycle()
        }
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var totalWidth = 0
        var totalHeight = 0

        // Measure each child
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child.visibility != View.GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec)

                // Accumulate the dimensions
                totalWidth += child.measuredWidth + spacing
                totalHeight = maxOf(totalHeight, child.measuredHeight)
            }
        }

        // Consider padding
        totalWidth += paddingLeft + paddingRight
        totalHeight += paddingTop + paddingBottom

        // Resolve the final size based on the provided specifications
        val resolvedWidth = resolveSize(totalWidth, widthMeasureSpec)
        val resolvedHeight = resolveSize(totalHeight, heightMeasureSpec)

        setMeasuredDimension(resolvedWidth, resolvedHeight)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var currentLeft = paddingLeft
        var currentTop = paddingTop

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child.visibility != View.GONE) {
                val childWidth = child.measuredWidth
                val childHeight = child.measuredHeight

                // Place the child with spacing
                child.layout(currentLeft, currentTop, currentLeft + childWidth, currentTop + childHeight)

                // Update the current left position for the next child, considering spacing
                currentLeft += childWidth + spacing
            }
        }
    }
}

Conclusion

Overriding onLayout() in a custom ViewGroup in Kotlin with XML layouts allows you to create highly customized and reusable UI components. By understanding the onMeasure() and onLayout() methods, developers can precisely control the positioning and sizing of child views. Custom ViewGroups enhance code maintainability and offer a structured approach to building complex Android applications.