Extending ViewGroup

The process to create a custom layout is quite similar to creating a custom view. We've got to create a class that extends from ViewGroup instead of view, create the appropriate constructors, implement the onMeasure() method, and override the onLayout() method rather than the onDraw() method.

Let's create a very simple custom layout; it will add elements to the right of the previous element until it doesn't fit on the screen, then it'll start a new row, using the higher element to calculate where this new row will start and avoid any overlapping between views.

Adding random sized views, where each view has a red background, will look as follows:

First, let's create a class that extends from ViewGroup:

public class CustomLayout extends ViewGroup { 
 
    public CustomLayout(Context context, AttributeSet attrs) { 
        super(context, attrs); 
    } 
 
    @Override 
   protected void onLayout(boolean changed, int l, int t, int r, int b) { 
 
   } 
} 

We created the constructor and we implemented the onLayout() method as it's an abstract method and we've got to implement it. Let's add some logic to it:

@Override 
   protected void onLayout(boolean changed, int l, int t, int r, int b){ 
        int count = getChildCount(); 
        int left = l + getPaddingLeft(); 
        int top = t + getPaddingTop(); 
 
        // keeps track of maximum row height 
        int rowHeight = 0; 
 
        for (int i = 0; i < count; i++) { 
            View child = getChildAt(i); 
 
            int childWidth = child.getMeasuredWidth(); 
            int childHeight = child.getMeasuredHeight(); 
 
            // if child fits in this row put it there 
            if (left + childWidth < r - getPaddingRight()) { 
                child.layout(left, top, left + childWidth, top +
childHeight); left += childWidth; } else { // otherwise put it on next row left = l + getPaddingLeft(); top += rowHeight; rowHeight = 0; } // update maximum row height if (childHeight > rowHeight) rowHeight = childHeight; } }

This logic implements what we've described before; it tries to add a child to the right of the previous child and if it doesn't fit on the layout width, checking the current left position plus the measured child width, it starts a new row. The rowHeight variable measures the higher view on that row.

Let's also implement the onMeasure() method:

@Override 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
    
    int count = getChildCount(); 
 
    int rowHeight = 0; 
    int maxWidth = 0; 
    int maxHeight = 0; 
    int left = 0; 
    int top = 0; 
 
    for (int i = 0; i < count; i++) { 
        View child = getChildAt(i); 
        measureChild(child, widthMeasureSpec, heightMeasureSpec); 
 
        int childWidth = child.getMeasuredWidth(); 
        int childHeight = child.getMeasuredHeight(); 
 
        // if child fits in this row put it there 
        if (left + childWidth < getWidth()) { 
            left += childWidth; 
        } else { 
            // otherwise put it on next row 
            if(left > maxWidth) maxWidth = left; 
            left = 0; 
            top += rowHeight; 
            rowHeight = 0; 
        } 
 
        // update maximum row height 
        if (childHeight > rowHeight) rowHeight = childHeight; 
    } 
 
    if(left > maxWidth) maxWidth = left; 
    maxHeight = top + rowHeight; 
 
    setMeasuredDimension(getMeasure(widthMeasureSpec, maxWidth),
getMeasure(heightMeasureSpec, maxHeight)); }

The logic is the same as before, but it's not laying out its children. It calculates the maximum width and height that will be needed, and then with the help of a helper method sets the dimensions of this custom layout according to the width and height measurement specs:

private int getMeasure(int spec, int desired) { 
        switch(MeasureSpec.getMode(spec)) { 
            case MeasureSpec.EXACTLY: 
                return MeasureSpec.getSize(spec); 
 
            case MeasureSpec.AT_MOST: 
                return Math.min(MeasureSpec.getSize(spec), desired); 
 
            case MeasureSpec.UNSPECIFIED: 
            default: 
                return desired; 
        } 
    } 

Now that we've got our custom layout, let's add it to our activity_main layout:

<?xml version="1.0" encoding="utf-8"?> 
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:tools="http://schemas.android.com/tools" 
    android:id="@+id/activity_main" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" 
    android:padding="@dimen/activity_vertical_margin" 
    tools:context="com.packt.rrafols.customview.MainActivity"> 
 
    <com.packt.rrafols.customview.CustomLayout 
        android:id="@+id/custom_layout" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent"> 
 
    </com.packt.rrafols.customview.CustomLayout> 
</RelativeLayout> 

For the last step, let's add some random sized views to it:

public class MainActivity extends AppCompatActivity { 
    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 
 
        CustomLayout customLayout = (CustomLayout)
findViewById(R.id.custom_layout); Random rnd = new Random(); for(int i = 0; i < 50; i++) { OwnCustomView view = new OwnCustomView(this); int width = rnd.nextInt(200) + 50; int height = rnd.nextInt(100) + 100; view.setLayoutParams(new ViewGroup.LayoutParams(width,
height)); view.setPadding(2, 2, 2, 2); customLayout.addView(view); } } }

Check the Example08-CustomLayout folder on GitHub for the full source code of this example.

On this page, we can also find a quite complex example of a full-featured custom layout.