Line-breaking widget layout for Android

JavaAndroidUser InterfaceLayout

Java Problem Overview


I'm trying to create an activity that presents some data to the user. The data is such that it can be divided into 'words', each being a widget, and sequence of 'words' would form the data ('sentence'?), the ViewGroup widget containing the words. As space required for all 'words' in a 'sentence' would exceed the available horizontal space on the display, I would like to wrap these 'sentences' as you would a normal piece of text.

The following code:

public class WrapTest extends Activity {
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		LinearLayout l = new LinearLayout(this);
		LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
				LinearLayout.LayoutParams.FILL_PARENT,
				LinearLayout.LayoutParams.WRAP_CONTENT);
		LinearLayout.LayoutParams mlp = new LinearLayout.LayoutParams(
				new ViewGroup.MarginLayoutParams(
						LinearLayout.LayoutParams.WRAP_CONTENT,
						LinearLayout.LayoutParams.WRAP_CONTENT));
		mlp.setMargins(0, 0, 2, 0);

		for (int i = 0; i < 10; i++) {
			TextView t = new TextView(this);
			t.setText("Hello");
			t.setBackgroundColor(Color.RED);
			t.setSingleLine(true);
			l.addView(t, mlp);
		}

		setContentView(l, lp);
	}
}

yields something like the left picture, but I would want a layout presenting the same widgets like in the right one.

non-wrapping

wrapping

Is there such a layout or combination of layouts and parameters, or do I have to implement my own ViewGroup for this?

Java Solutions


Solution 1 - Java

I made my own layout that does what I want, but it is quite limited at the moment. Comments and improvement suggestions are of course welcome.

The activity:

package se.fnord.xmms2.predicate;

import se.fnord.android.layout.PredicateLayout;
import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.TextView;

public class Predicate extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        PredicateLayout l = new PredicateLayout(this);
        for (int i = 0; i < 10; i++) {
            TextView t = new TextView(this);
            t.setText("Hello");
            t.setBackgroundColor(Color.RED);
            t.setSingleLine(true);
            l.addView(t, new PredicateLayout.LayoutParams(2, 0));
        }

        setContentView(l);
    }
}

Or in an XML layout:

<se.fnord.android.layout.PredicateLayout
    android:id="@+id/predicate_layout"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
/>

And the Layout:

package se.fnord.android.layout;

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

/**
 * ViewGroup that arranges child views in a similar way to text, with them laid
 * out one line at a time and "wrapping" to the next line as needed.
 * 
 * Code licensed under CC-by-SA
 *  
 * @author Henrik Gustafsson
 * @see http://stackoverflow.com/questions/549451/line-breaking-widget-layout-for-android
 * @license http://creativecommons.org/licenses/by-sa/2.5/
 *
 */
public class PredicateLayout extends ViewGroup {

    private int line_height;

    public PredicateLayout(Context context) {
        super(context);
    }
    
    public PredicateLayout(Context context, AttributeSet attrs){
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        assert(MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED);

        final int width = MeasureSpec.getSize(widthMeasureSpec);

        // The next line is WRONG!!! Doesn't take into account requested MeasureSpec mode!
        int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
        final int count = getChildCount();
        int line_height = 0;
        
        int xpos = getPaddingLeft();
        int ypos = getPaddingTop();
        
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                child.measure(
                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED));
                
                final int childw = child.getMeasuredWidth();
                line_height = Math.max(line_height, child.getMeasuredHeight() + lp.height);
                
                if (xpos + childw > width) {
                    xpos = getPaddingLeft();
                    ypos += line_height;
                }
                
                xpos += childw + lp.width;
            }
        }
        this.line_height = line_height;
        
        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED){
            height = ypos + line_height;

        } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST){
            if (ypos + line_height < height){
                height = ypos + line_height;
            }
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(1, 1); // default of 1px spacing
    }

    @Override
    protected boolean checkLayoutParams(LayoutParams p) {
        return (p instanceof LayoutParams);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        final int width = r - l;
        int xpos = getPaddingLeft();
        int ypos = getPaddingTop();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final int childw = child.getMeasuredWidth();
                final int childh = child.getMeasuredHeight();
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (xpos + childw > width) {
                    xpos = getPaddingLeft();
                    ypos += line_height;
                }
                child.layout(xpos, ypos, xpos + childw, ypos + childh);
                xpos += childw + lp.width;
            }
        }
    }
}

With the result:

Wrapped widgets

Solution 2 - Java

Since May 2016 there is new layout called FlexboxLayout from Google, which is highly configurable for purpose you want.

FlexboxLayout is in Google GitHub repository at https://github.com/google/flexbox-layout at this moment.

You can use it in your project by adding dependency to your build.gradle file:

dependencies {
    compile 'com.google.android:flexbox:0.3.2'
}

More about FlexboxLayout usage and all the attributes you can find in repository readme or in Mark Allison articles here:

https://blog.stylingandroid.com/flexboxlayout-part-1/

https://blog.stylingandroid.com/flexboxlayout-part2/

https://blog.stylingandroid.com/flexboxlayout-part-3/

Solution 3 - Java

I implemented something very similar to this, but using what I think is a little more standard way to handle spacing and padding. Please let me know what you guys think, and feel free to reuse without attribution:

package com.asolutions.widget;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import com.asolutions.widget.R;

public class RowLayout extends ViewGroup {
	public static final int DEFAULT_HORIZONTAL_SPACING = 5;
	public static final int DEFAULT_VERTICAL_SPACING = 5;
	private final int horizontalSpacing;
	private final int verticalSpacing;
	private List<RowMeasurement> currentRows = Collections.emptyList();

	public RowLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
		TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.RowLayout);
		horizontalSpacing = styledAttributes.getDimensionPixelSize(R.styleable.RowLayout_android_horizontalSpacing,
				DEFAULT_HORIZONTAL_SPACING);
		verticalSpacing = styledAttributes.getDimensionPixelSize(R.styleable.RowLayout_android_verticalSpacing,
				DEFAULT_VERTICAL_SPACING);
		styledAttributes.recycle();
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		final int maxInternalWidth = MeasureSpec.getSize(widthMeasureSpec) - getHorizontalPadding();
		final int maxInternalHeight = MeasureSpec.getSize(heightMeasureSpec) - getVerticalPadding();
		List<RowMeasurement> rows = new ArrayList<RowMeasurement>();
		RowMeasurement currentRow = new RowMeasurement(maxInternalWidth, widthMode);
		rows.add(currentRow);
		for (View child : getLayoutChildren()) {
			LayoutParams childLayoutParams = child.getLayoutParams();
			int childWidthSpec = createChildMeasureSpec(childLayoutParams.width, maxInternalWidth, widthMode);
			int childHeightSpec = createChildMeasureSpec(childLayoutParams.height, maxInternalHeight, heightMode);
			child.measure(childWidthSpec, childHeightSpec);
			int childWidth = child.getMeasuredWidth();
			int childHeight = child.getMeasuredHeight();
			if (currentRow.wouldExceedMax(childWidth)) {
				currentRow = new RowMeasurement(maxInternalWidth, widthMode);
				rows.add(currentRow);
			}
			currentRow.addChildDimensions(childWidth, childHeight);
		}

		int longestRowWidth = 0;
		int totalRowHeight = 0;
		for (int index = 0; index < rows.size(); index++) {
			RowMeasurement row = rows.get(index);
			totalRowHeight += row.getHeight();
			if (index < rows.size() - 1) {
				totalRowHeight += verticalSpacing;
			}
			longestRowWidth = Math.max(longestRowWidth, row.getWidth());
		}
		setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? MeasureSpec.getSize(widthMeasureSpec) : longestRowWidth
				+ getHorizontalPadding(), heightMode == MeasureSpec.EXACTLY ? MeasureSpec.getSize(heightMeasureSpec)
				: totalRowHeight + getVerticalPadding());
		currentRows = Collections.unmodifiableList(rows);
	}

	private int createChildMeasureSpec(int childLayoutParam, int max, int parentMode) {
		int spec;
		if (childLayoutParam == LayoutParams.FILL_PARENT) {
			spec = MeasureSpec.makeMeasureSpec(max, MeasureSpec.EXACTLY);
		} else if (childLayoutParam == LayoutParams.WRAP_CONTENT) {
			spec = MeasureSpec.makeMeasureSpec(max, parentMode == MeasureSpec.UNSPECIFIED ? MeasureSpec.UNSPECIFIED
					: MeasureSpec.AT_MOST);
		} else {
			spec = MeasureSpec.makeMeasureSpec(childLayoutParam, MeasureSpec.EXACTLY);
		}
		return spec;
	}

	@Override
	protected void onLayout(boolean changed, int leftPosition, int topPosition, int rightPosition, int bottomPosition) {
		final int widthOffset = getMeasuredWidth() - getPaddingRight();
		int x = getPaddingLeft();
		int y = getPaddingTop();

		Iterator<RowMeasurement> rowIterator = currentRows.iterator();
		RowMeasurement currentRow = rowIterator.next();
		for (View child : getLayoutChildren()) {
			final int childWidth = child.getMeasuredWidth();
			final int childHeight = child.getMeasuredHeight();
			if (x + childWidth > widthOffset) {
				x = getPaddingLeft();
				y += currentRow.height + verticalSpacing;
				if (rowIterator.hasNext()) {
					currentRow = rowIterator.next();
				}
			}
			child.layout(x, y, x + childWidth, y + childHeight);
			x += childWidth + horizontalSpacing;
		}
	}

	private List<View> getLayoutChildren() {
		List<View> children = new ArrayList<View>();
		for (int index = 0; index < getChildCount(); index++) {
			View child = getChildAt(index);
			if (child.getVisibility() != View.GONE) {
				children.add(child);
			}
		}
		return children;
	}

	protected int getVerticalPadding() {
		return getPaddingTop() + getPaddingBottom();
	}

	protected int getHorizontalPadding() {
		return getPaddingLeft() + getPaddingRight();
	}

	private final class RowMeasurement {
		private final int maxWidth;
		private final int widthMode;
		private int width;
		private int height;

		public RowMeasurement(int maxWidth, int widthMode) {
			this.maxWidth = maxWidth;
			this.widthMode = widthMode;
		}

		public int getHeight() {
			return height;
		}

		public int getWidth() {
			return width;
		}

		public boolean wouldExceedMax(int childWidth) {
			return widthMode == MeasureSpec.UNSPECIFIED ? false : getNewWidth(childWidth) > maxWidth;
		}

		public void addChildDimensions(int childWidth, int childHeight) {
			width = getNewWidth(childWidth);
			height = Math.max(height, childHeight);
		}

		private int getNewWidth(int childWidth) {
			return width == 0 ? childWidth : width + horizontalSpacing + childWidth;
		}
	}
}

This also requires an entry under /res/values/attrs.xml, which you can create if it's not already there.

<?xml version="1.0" encoding="utf-8"?>
<resources>
	<declare-styleable name="RowLayout">
		<attr name="android:verticalSpacing" />
		<attr name="android:horizontalSpacing" />
	</declare-styleable>
</resources>

Usage in an xml layout looks like this:

<?xml version="1.0" encoding="utf-8"?>
<com.asolutions.widget.RowLayout 
	xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:padding="10dp"
    android:horizontalSpacing="10dp"
   	android:verticalSpacing="20dp">
    <FrameLayout android:layout_width="30px" android:layout_height="40px" android:background="#F00"/>
    <FrameLayout android:layout_width="60px" android:layout_height="40px" android:background="#F00"/>
    <FrameLayout android:layout_width="70px" android:layout_height="20px" android:background="#F00"/>
    <FrameLayout android:layout_width="20px" android:layout_height="60px" android:background="#F00"/>
    <FrameLayout android:layout_width="10px" android:layout_height="40px" android:background="#F00"/>
    <FrameLayout android:layout_width="40px" android:layout_height="40px" android:background="#F00"/>
    <FrameLayout android:layout_width="40px" android:layout_height="40px" android:background="#F00"/>
    <FrameLayout android:layout_width="40px" android:layout_height="40px" android:background="#F00"/>
</com.asolutions.widget.RowLayout>

Solution 4 - Java

The android-flowlayout project by ApmeM support line breaks, too.

enter image description here

Solution 5 - Java

Here is my simplified, code-only version:

    package com.superliminal.android.util;

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


    /**
     * A view container with layout behavior like that of the Swing FlowLayout.
     * Originally from http://nishantvnair.wordpress.com/2010/09/28/flowlayout-in-android/ itself derived from
     * http://stackoverflow.com/questions/549451/line-breaking-widget-layout-for-android
     *
     * @author Melinda Green
     */
    public class FlowLayout extends ViewGroup {
        private final static int PAD_H = 2, PAD_V = 2; // Space between child views.
        private int mHeight;

        public FlowLayout(Context context) {
            super(context);
        }

        public FlowLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            assert (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED);
            final int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
            int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
            final int count = getChildCount();
            int xpos = getPaddingLeft();
            int ypos = getPaddingTop();
            int childHeightMeasureSpec;
            if(MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST)
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
            else
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            mHeight = 0;
            for(int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                if(child.getVisibility() != GONE) {
                    child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), childHeightMeasureSpec);
                    final int childw = child.getMeasuredWidth();
                    mHeight = Math.max(mHeight, child.getMeasuredHeight() + PAD_V);
                    if(xpos + childw > width) {
                        xpos = getPaddingLeft();
                        ypos += mHeight;
                    }
                    xpos += childw + PAD_H;
                }
            }
            if(MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
                height = ypos + mHeight;
            } else if(MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
                if(ypos + mHeight < height) {
                    height = ypos + mHeight;
                }
            }
            height += 5; // Fudge to avoid clipping bottom of last row.
            setMeasuredDimension(width, height);
        } // end onMeasure()

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            final int width = r - l;
            int xpos = getPaddingLeft();
            int ypos = getPaddingTop();
            for(int i = 0; i < getChildCount(); i++) {
                final View child = getChildAt(i);
                if(child.getVisibility() != GONE) {
                    final int childw = child.getMeasuredWidth();
                    final int childh = child.getMeasuredHeight();
                    if(xpos + childw > width) {
                        xpos = getPaddingLeft();
                        ypos += mHeight;
                    }
                    child.layout(xpos, ypos, xpos + childw, ypos + childh);
                    xpos += childw + PAD_H;
                }
            }
        } // end onLayout()

    }

Solution 6 - Java

There is a problem with the first Answer:

child.measure(
    MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
    MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));

In a ListView, for example, the list items are passed a heightMeasureSpec of 0 (UNSPECIFIED) and so, here, the MeasureSpec of size 0 (AT_MOST) is passed to all of the children. This means that the whole PredicateLayout is invisible (height 0).

As a quick fix, I changed the child height MeasureSpec like this:

int childHeightMeasureSpec;
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
    childHeightMeasureSpec =
        MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
}
else {
    childHeightMeasureSpec = 
        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);			
}

and then

child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 
    childHeightMeasureSpec);

which seems to work for me although does not handle EXACT mode which would be much more tricky.

Solution 7 - Java

My slightly modified version based on posted here previously:

  • It works in Eclipse layout editor

  • It centers all the items horizontally

      import android.content.Context;
      import android.util.AttributeSet;
      import android.view.View;
      import android.view.ViewGroup;
    
      public class FlowLayout extends ViewGroup {
    
      private int line_height;
    
      public static class LayoutParams extends ViewGroup.LayoutParams {
    
          public final int horizontal_spacing;
          public final int vertical_spacing;
    
          /**
           * @param horizontal_spacing Pixels between items, horizontally
           * @param vertical_spacing Pixels between items, vertically
           */
          public LayoutParams(int horizontal_spacing, int vertical_spacing, ViewGroup.LayoutParams viewGroupLayout) {
              super(viewGroupLayout);
              this.horizontal_spacing = horizontal_spacing;
              this.vertical_spacing = vertical_spacing;
          }
          
          /**
           * @param horizontal_spacing Pixels between items, horizontally
           * @param vertical_spacing Pixels between items, vertically
           */
          public LayoutParams(int horizontal_spacing, int vertical_spacing) {
              super(0, 0);
              this.horizontal_spacing = horizontal_spacing;
              this.vertical_spacing = vertical_spacing;
          }
      }
    
      public FlowLayout(Context context) {
          super(context);
      }
    
      public FlowLayout(Context context, AttributeSet attrs) {
          super(context, attrs);
      }
    
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          assert (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED);
    
          final int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
          int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
          final int count = getChildCount();
          int line_height = 0;
    
          int xpos = getPaddingLeft();
          int ypos = getPaddingTop();
    
          int childHeightMeasureSpec;
          if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
              childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
          } else {
              childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
          }
    
    
          for (int i = 0; i < count; i++) {
              final View child = getChildAt(i);
              if (child.getVisibility() != GONE) {
                  final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                  child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), childHeightMeasureSpec);
                  final int childw = child.getMeasuredWidth();
                  line_height = Math.max(line_height, child.getMeasuredHeight() + lp.vertical_spacing);
    
                  if (xpos + childw > width) {
                      xpos = getPaddingLeft();
                      ypos += line_height;
                  }
    
                  xpos += childw + lp.horizontal_spacing;
              }
          }
          this.line_height = line_height;
    
          if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
              height = ypos + line_height;
    
          } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
              if (ypos + line_height < height) {
                  height = ypos + line_height;
              }
          }
          setMeasuredDimension(width, height);
      }
    
      @Override
      protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
          return new LayoutParams(1, 1); // default of 1px spacing
      }
    
      @Override
      protected android.view.ViewGroup.LayoutParams generateLayoutParams(
      		android.view.ViewGroup.LayoutParams p) {
      	return new LayoutParams(1, 1, p);
      }
    
      @Override
      protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
          if (p instanceof LayoutParams) {
              return true;
          }
          return false;
      }
    
      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
          final int count = getChildCount();
          final int width = r - l;
          int xpos = getPaddingLeft();
          int ypos = getPaddingTop();
          
          int lastHorizontalSpacing = 0;
          int rowStartIdx = 0;
    
          for (int i = 0; i < count; i++) {
              final View child = getChildAt(i);
              if (child.getVisibility() != GONE) {
                  final int childw = child.getMeasuredWidth();
                  final int childh = child.getMeasuredHeight();
                  final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                  if (xpos + childw > width) {
                  	final int freeSpace = width - xpos + lastHorizontalSpacing;
                  	xpos = getPaddingLeft() + freeSpace / 2;
                  	
                  	for (int j = rowStartIdx; j < i; ++j) {
                  		final View drawChild = getChildAt(j);
                  		drawChild.layout(xpos, ypos, xpos + drawChild.getMeasuredWidth(), ypos + drawChild.getMeasuredHeight());
                  		xpos += drawChild.getMeasuredWidth() + ((LayoutParams)drawChild.getLayoutParams()).horizontal_spacing;
      				}
                  	
                  	lastHorizontalSpacing = 0;
                  	xpos = getPaddingLeft();
                      ypos += line_height;
                      rowStartIdx = i;
                  } 
                  
                  child.layout(xpos, ypos, xpos + childw, ypos + childh);
                  xpos += childw + lp.horizontal_spacing;
                  lastHorizontalSpacing = lp.horizontal_spacing;
              }
          }
          
          if (rowStartIdx < count) {
          	final int freeSpace = width - xpos + lastHorizontalSpacing;
          	xpos = getPaddingLeft() + freeSpace / 2;
          	
          	for (int j = rowStartIdx; j < count; ++j) {
          		final View drawChild = getChildAt(j);
          		drawChild.layout(xpos, ypos, xpos + drawChild.getMeasuredWidth(), ypos + drawChild.getMeasuredHeight());
          		xpos += drawChild.getMeasuredWidth() + ((LayoutParams)drawChild.getLayoutParams()).horizontal_spacing;
      		}
          }
      }
      }
    

Solution 8 - Java

I have updated this sample to fix a bug, now, each line can have a different height !

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

/**
 * ViewGroup that arranges child views in a similar way to text, with them laid
 * out one line at a time and "wrapping" to the next line as needed.
 * 
 * Code licensed under CC-by-SA
 *  
 * @author Henrik Gustafsson
 * @see http://stackoverflow.com/questions/549451/line-breaking-widget-layout-for-android
 * @license http://creativecommons.org/licenses/by-sa/2.5/
 *
 * Updated by Aurélien Guillard
 * Each line can have a different height
 * 
 */
public class FlowLayout extends ViewGroup {

	public static class LayoutParams extends ViewGroup.LayoutParams {

		public final int horizontal_spacing;
		public final int vertical_spacing;

		/**
		 * @param horizontal_spacing Pixels between items, horizontally
		 * @param vertical_spacing Pixels between items, vertically
		 */
		public LayoutParams(int horizontal_spacing, int vertical_spacing) {
			super(0, 0);
			this.horizontal_spacing = horizontal_spacing;
			this.vertical_spacing = vertical_spacing;
		}
	}

	public FlowLayout(Context context) {
		super(context);
	}

	public FlowLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		assert (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED);

		final int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
		
		int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
		final int count = getChildCount();
		int line_height = 0;

		int xpos = getPaddingLeft();
		int ypos = getPaddingTop();

		int childHeightMeasureSpec;
		
		if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
			childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
			
		} else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
			childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
			
		} else {
			childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
		}

		for (int i = 0; i < count; i++) {
			final View child = getChildAt(i);
			if (child.getVisibility() != GONE) {
				final LayoutParams lp = (LayoutParams) child.getLayoutParams();
				child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), childHeightMeasureSpec);
				final int childw = child.getMeasuredWidth();

				if (xpos + childw > width) {
					xpos = getPaddingLeft();
					ypos += line_height;
				}

				xpos += childw + lp.horizontal_spacing;
				line_height = child.getMeasuredHeight() + lp.vertical_spacing;
			}
		}

		if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
			height = ypos + line_height;

		} else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
			if (ypos + line_height < height) {
				height = ypos + line_height;
			}
		}
		setMeasuredDimension(width, height);
	}

	@Override
	protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
		return new LayoutParams(1, 1); // default of 1px spacing
	}

	@Override
	protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
		if (p instanceof LayoutParams) {
			return true;
		}
		return false;
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		final int count = getChildCount();
		final int width = r - l;
		int xpos = getPaddingLeft();
		int ypos = getPaddingTop();
		int lineHeight = 0;

		for (int i = 0; i < count; i++) {
			final View child = getChildAt(i);
			if (child.getVisibility() != GONE) {
				final int childw = child.getMeasuredWidth();
				final int childh = child.getMeasuredHeight();
				final LayoutParams lp = (LayoutParams) child.getLayoutParams();

				if (xpos + childw > width) {
					xpos = getPaddingLeft();
					ypos += lineHeight;
				}
				
				lineHeight = child.getMeasuredHeight() + lp.vertical_spacing;
				
				child.layout(xpos, ypos, xpos + childw, ypos + childh);
				xpos += childw + lp.horizontal_spacing;
			}
		}
	}
}

Solution 9 - Java

Some of the different answers here would give me a ClassCastException in the Exclipse layout editor. In my case, I wanted to use ViewGroup.MarginLayoutParams rather than making my own. Either way, make sure to return the instance of LayoutParams that your custom layout class needs in generateLayoutParams. This is what mine looks like, just replace MarginLayoutParams with the one your ViewGroup needs:

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}

@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof MarginLayoutParams;
}

It looks like this method gets called to to assign a LayoutParams object for each child in the ViewGroup.

Solution 10 - Java

	LinearLayout row = new LinearLayout(this);
	
	//get the size of the screen
	Display display = getWindowManager().getDefaultDisplay();
	this.screenWidth = display.getWidth();  // deprecated
	
	for(int i=0; i<this.users.length; i++) {
		
		row.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
		
		this.tag_button = new Button(this);
		this.tag_button.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, 70));
		//get the size of the button text
		Paint mPaint = new Paint();
		mPaint.setAntiAlias(true);
		mPaint.setTextSize(tag_button.getTextSize());
		mPaint.setTypeface(Typeface.create(Typeface.SERIF, Typeface.NORMAL));
		float size = mPaint.measureText(tag_button.getText().toString(), 0, tag_button.getText().toString().length());
		size = size+28;
		this.totalTextWidth += size;
		
		if(totalTextWidth < screenWidth) {
			row.addView(tag_button);
		} else {
			this.tags.addView(row);
			row = new LinearLayout(this);
			row.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
			row.addView(tag_button);
			this.totalTextWidth = size;
		}
	}

Solution 11 - Java

I adapted some of the ode above and implemented a flow layout which centers all child views, horizontal and vertical. It fits my needs.

public class CenteredFlowLayout extends ViewGroup {

  private int lineHeight;

  private int centricHeightPadding;

  private final int halfGap;

  public static final List<View> LINE_CHILDREN = new ArrayList<View>();

  public static class LayoutParams extends ViewGroup.LayoutParams {

    public final int horizontalSpacing;

    public final int verticalSpacing;

    public LayoutParams(int horizontalSpacing, int verticalSpacing) {
      super(0, 0);
      this.horizontalSpacing = horizontalSpacing;
      this.verticalSpacing = verticalSpacing;
    }
  }

  public CenteredFlowLayout(Context context) {
    super(context);
    halfGap = getResources().getDimensionPixelSize(R.dimen.half_gap);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
    int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
    final int maxHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
    final int count = getChildCount();
    int lineHeight = 0;

    int xAxis = getPaddingLeft();
    int yAxis = getPaddingTop();

    int childHeightMeasureSpec;
    if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
      childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
    } else {
      childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    }

    for (int i = 0; i < count; i++) {
      final View child = getChildAt(i);
      if (child.getVisibility() != GONE) {
        final CentricFlowLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
        child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), childHeightMeasureSpec);
        final int childMeasuredWidth = child.getMeasuredWidth();
        lineHeight = Math.max(lineHeight, child.getMeasuredHeight() + lp.verticalSpacing);

        if (xAxis + childMeasuredWidth > width) {
          xAxis = getPaddingLeft();
          yAxis += lineHeight;
        } else if (i + 1 == count) {
          yAxis += lineHeight;
        }

        xAxis += childMeasuredWidth + lp.horizontalSpacing;
      }
    }
    this.lineHeight = lineHeight;

    if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
      height = yAxis + lineHeight;
    } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
      if (yAxis + lineHeight < height) {
        height = yAxis + lineHeight;
      }
    }
    if (maxHeight == 0) {
      maxHeight = height + getPaddingTop();
    }
    centricHeightPadding = (maxHeight - height) / 2;
    setMeasuredDimension(width, disableCenterVertical ? height + getPaddingTop() : maxHeight);
  }

  @Override
  protected CentricFlowLayout.LayoutParams generateDefaultLayoutParams() {
    return new CentricFlowLayout.LayoutParams(halfGap, halfGap);
  }

  @Override
  protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    if (p instanceof LayoutParams) {
      return true;
    }
    return false;
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int count = getChildCount();
    final int width = r - l;
    int yAxis = centricHeightPadding + getPaddingTop() + getPaddingBottom();
    View child;
    int measuredWidth;
    int lineWidth = getPaddingLeft() + getPaddingRight();
    CentricFlowLayout.LayoutParams lp;
    int offset;
    LINE_CHILDREN.clear();
    for (int i = 0; i < count; i++) {
      child = getChildAt(i);
      lp = (LayoutParams) child.getLayoutParams();
      if (GONE != child.getVisibility()) {
        measuredWidth = child.getMeasuredWidth();
        if (lineWidth + measuredWidth + lp.horizontalSpacing > width) {
          offset = (width - lineWidth) / 2;
          layoutHorizontalCentricLine(LINE_CHILDREN, offset, yAxis);
          lineWidth = getPaddingLeft() + getPaddingRight() + measuredWidth + lp.horizontalSpacing;
          yAxis += lineHeight;
          LINE_CHILDREN.clear();
          LINE_CHILDREN.add(child);
        } else {
          lineWidth += measuredWidth + lp.horizontalSpacing;
          LINE_CHILDREN.add(child);
        }
      }
    }
    offset = (width - lineWidth) / 2;
    layoutHorizontalCentricLine(LINE_CHILDREN, offset, yAxis);
  }

  private void layoutHorizontalCentricLine(final List<View> children, final int offset, final int yAxis) {
    int xAxis = getPaddingLeft() + getPaddingRight() + offset;
    for (View child : children) {
      final int measuredWidth = child.getMeasuredWidth();
      final int measuredHeight = child.getMeasuredHeight();
      final CentricFlowLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
      child.layout(xAxis, yAxis, xAxis + measuredWidth, yAxis + measuredHeight);
      xAxis += measuredWidth + lp.horizontalSpacing;
    }
  }    
}

Solution 12 - Java

try :

  dependencies {
       implementation'com.google.android:flexbox:0.3.2'
    }

Solution 13 - Java

Try setting both of lp's LayoutParams to be WRAP_CONTENT.

Setting mlp to be WRAP_CONTENT, WRAP_CONTENT ensures that your TextView(s) t are just wide and tall enough enough to hold "Hello" or whatever String you put in them. I think l may not be aware of how wide your t's are. The setSingleLine(true) may be contributing too.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionHenrik GustafssonView Question on Stackoverflow
Solution 1 - JavaHenrik GustafssonView Answer on Stackoverflow
Solution 2 - JavaLukas NovakView Answer on Stackoverflow
Solution 3 - JavaMicah HainlineView Answer on Stackoverflow
Solution 4 - JavaMohsen AfshinView Answer on Stackoverflow
Solution 5 - JavaMelinda GreenView Answer on Stackoverflow
Solution 6 - JavaMarkView Answer on Stackoverflow
Solution 7 - JavaErnestas VaiciukevičiusView Answer on Stackoverflow
Solution 8 - JavaAurélien GuillardView Answer on Stackoverflow
Solution 9 - JavaHenryView Answer on Stackoverflow
Solution 10 - JavaMuhammad Aamir AliView Answer on Stackoverflow
Solution 11 - JavaThomas R.View Answer on Stackoverflow
Solution 12 - Javazahra salmaninejadView Answer on Stackoverflow
Solution 13 - JavaWillView Answer on Stackoverflow