Line-breaking widget layout for Android

48,604

Solution 1

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 {
    implementation 'com.google.android.flexbox:flexbox:3.0.0'
}

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 2

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 3

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

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

enter image description here

Solution 5

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()

    }
Share:
48,604

Related videos on Youtube

Henrik Gustafsson
Author by

Henrik Gustafsson

Hacking merrily under the employ of Ticketmaster. I have software development as much as a hobby as something that pays my bills, which is in part why I hang around boards like this.

Updated on February 10, 2021

Comments

  • Henrik Gustafsson
    Henrik Gustafsson about 3 years

    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?

  • Henrik Gustafsson
    Henrik Gustafsson about 15 years
    As far as I know, LinearLayout does not do any wrapping. WRAP_CONTENT on both axes would make the LinearLayout-widget one line high and extend way off the screen. And from what I understand, the setSingleLine on the text-widget only affects the size and shape of that widget, not its parents layout.
  • Henrik Gustafsson
    Henrik Gustafsson about 15 years
    I only did it programmatically because it's much less to paste in order to communicate the idea :)
  • Henrik Gustafsson
    Henrik Gustafsson over 14 years
    The site made me agree that "user contributed content licensed under cc-wiki with attribution required", so... Check the bottom for details
  • Henrik Gustafsson
    Henrik Gustafsson over 14 years
    Also, if you make any improvements it'd be neat if you posted them here :)
  • skorulis
    skorulis about 13 years
    This also fixes the issue when the layout is put in a scrollview. Thanks for solving this one so I didn't have to work it out for myself.
  • Anton Derevyanko
    Anton Derevyanko almost 13 years
    Sometimes this method calculates height not correct (when all child views have different sizes). I suggest you to calculate setMeasuredDimension heigh by multipling lines count by line_heigth. This is simpler and more precise method.
  • Umair A.
    Umair A. over 12 years
    I am using this. I am adding items dynamically to that layout. It gets added but every view is way to small say tiny. why?
  • twig
    twig over 12 years
    For some reason I'm getting "ClassCastException android.view.view cannot be cast to android.view.viewgroup" when trying to inflate the view in the Android layout editor. Any idea on how to fix this?
  • mdelolmo
    mdelolmo almost 12 years
    Among all the solutions I tried, this one was the only one that didn't throuw a ClassCastException when using it from a xml layout file.
  • Vishnu Mohan G
    Vishnu Mohan G almost 12 years
    me too get the class cast exception when we try to cast LayoutParams to PredictParams
  • Henrik Gustafsson
    Henrik Gustafsson almost 12 years
    Note that no updates have been made in close to 2.5 years. Something is likely to have changed. If you solve your problems in a nice, robust and generic way feel free to update the answer with your findings :)
  • devmiles.com
    devmiles.com almost 12 years
    For those who come by this answer - onMeasure() contains some bad logic and won't work as expected. Imagine that this ViewGroup's parent sends a measure request where it doesn't care about height, it will send 0 as height and 0 as measuring mode. PredicateLayout will get 0 as it's own height and will force children to be AT_MOST 0 height.
  • Henrik Gustafsson
    Henrik Gustafsson almost 12 years
    Instead of adding the comment in the code, couldn't you just correct the error? :)
  • jfritz42
    jfritz42 over 11 years
    @Will, I think you misunderstood the meaning of WRAP_CONTENT.
  • Miguel
    Miguel almost 10 years
    I added a view on the layout and give the class that name, and then added child buttons to that view. But the height of them i cannot control? they all look to have a padding top/bottom that i dont want.
  • Melinda Green
    Melinda Green over 9 years
    You can set PAD_V = 0, or add constructor arguments to customize padding.
  • Rahul Patidar
    Rahul Patidar almost 8 years
    @Lukas- I am using the same for Radio Group, UI is working fine but functionality of Radio Group is disturb. Means, in the same Radio Group, I can tick on multiple Radio Buttons. I want to check only one Radio Button at the same time. Please help for this.
  • JakeWilson801
    JakeWilson801 almost 8 years
    Just a heads up it doesn't work in any scrolling container! :(
  • Jay Rathod RJ
    Jay Rathod RJ over 7 years
    This is working but i have a problem how can i give Margin Left and Margin Top between items that are added ?
  • Jay Rathod RJ
    Jay Rathod RJ over 7 years
    @HenrikGustafsson Can i set the max rows inside this method ? I mean i want to set max 2 rows while setting this layout.
  • Henrik Gustafsson
    Henrik Gustafsson over 7 years
    @jaydroider: I doubt what you want can be done without further development. And I haven't touched Android development for many years now. Do have a look at the Google Flexbox Layout though. It's probably the better choice these days. See github.com/google/flexbox-layout
  • Lukas Novak
    Lukas Novak about 7 years
    @JakeWilson801 It worked in scrolling container for me. But now there is FlexboxLayoutManager in RecyclerView, so maybe it solve your issue.
  • Hiren Patel
    Hiren Patel about 6 years
    @LukasNovak, I have to add divider between children view. I have added it but divider's height not adjusting, it takes full height same as child view. I want to reduce the height of divider.
  • Vijayadhas Chandrasekaran
    Vijayadhas Chandrasekaran over 2 years
    Hi.. Using of this code can we arrange the child's from Right to Left??