Saturday 29 December 2012

Android - horizontal flow layout

If you use a LinearLayout which has horizontal orientation and limited width, you'll notice that the child views you add to this LinearLayout are placed one after another left-to-right up to and beyond the width of the LinearLayout. The child views will not wrap to a new line and there is no native layout to do this. So! I've wrote one, as below. It's an adaptation of a view I found on Nishant Nair's Blog here:
https://nishantvnair.wordpress.com/2010/09/28/flowlayout-in-android

UPDATE: For the latest version of the HorizontalFlowLayout class, see this repository:
https://bitbucket.org/adilson05uk/android-utils
 
 
/**
 * Custom view which extends {@link RelativeLayout}
 * and which places its children horizontally,
 * flowing over to a new line whenever it runs out of width.
 */
public class HorizontalFlowLayout extends RelativeLayout {

  /**
   * Constructor to use when creating View from code.
   */
  public HorizontalFlowLayout(Context context) {
    super(context);
  }

  /**
   * Constructor that is called when inflating View from XML.
   */
  public HorizontalFlowLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  /**
   * Perform inflation from XML and apply a class-specific base style.
   */
  public HorizontalFlowLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // need to call super.onMeasure(...) otherwise get some funny behaviour
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    final int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    // increment the x position as we progress through a line
    int xpos = getPaddingLeft();
    // increment the y position as we progress through the lines
    int ypos = getPaddingTop();
    // the height of the current line
    int line_height = 0;

    // go through children
    // to work out the height required for this view

    // call to measure size of children not needed I think?!
    // getting child's measured height/width seems to work okay without it
    //measureChildren(widthMeasureSpec, heightMeasureSpec);

    View child;
    MarginLayoutParams childMarginLayoutParams;
    int childWidth, childHeight, childMarginLeft, childMarginRight, childMarginTop, childMarginBottom;

    for (int i = 0; i < getChildCount(); i++) {
      child = getChildAt(i);

      if (child.getVisibility() != GONE) {
        childWidth = child.getMeasuredWidth();
        childHeight = child.getMeasuredHeight();

        if (child.getLayoutParams() != null
            && child.getLayoutParams() instanceof MarginLayoutParams) {
          childMarginLayoutParams = (MarginLayoutParams)child.getLayoutParams();

          childMarginLeft = childMarginLayoutParams.leftMargin;
          childMarginRight = childMarginLayoutParams.rightMargin;
          childMarginTop = childMarginLayoutParams.topMargin;
          childMarginBottom = childMarginLayoutParams.bottomMargin;
        }
        else {
          childMarginLeft = 0;
          childMarginRight = 0;
          childMarginTop = 0;
          childMarginBottom = 0;
        }

        if (xpos + childMarginLeft + childWidth + childMarginRight + getPaddingRight() > width) {
          // this child will need to go on a new line

          xpos = getPaddingLeft();
          ypos += line_height;

          line_height = childMarginTop + childHeight + childMarginBottom;
        }
        else {
          // enough space for this child on the current line
          line_height = Math.max(
              line_height,
              childMarginTop + childHeight + childMarginBottom);
        }

        xpos += childMarginLeft + childWidth + childMarginRight;
      }
    }

    ypos += line_height + getPaddingBottom();

    if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
      // set height as measured since there's no height restrictions
      height = ypos;
    }
    else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST
        && ypos < height) {
      // set height as measured since it's less than the maximum allowed
      height = ypos;
    }

    setMeasuredDimension(width, height);
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // increment the x position as we progress through a line
    int xpos = getPaddingLeft();
    // increment the y position as we progress through the lines
    int ypos = getPaddingTop();
    // the height of the current line
    int line_height = 0;

    View child;
    MarginLayoutParams childMarginLayoutParams;
    int childWidth, childHeight, childMarginLeft, childMarginRight, childMarginTop, childMarginBottom;

    for (int i = 0; i < getChildCount(); i++) {
      child = getChildAt(i);

      if (child.getVisibility() != GONE) {
        childWidth = child.getMeasuredWidth();
        childHeight = child.getMeasuredHeight();

        if (child.getLayoutParams() != null
            && child.getLayoutParams() instanceof MarginLayoutParams) {
          childMarginLayoutParams = (MarginLayoutParams)child.getLayoutParams();

          childMarginLeft = childMarginLayoutParams.leftMargin;
          childMarginRight = childMarginLayoutParams.rightMargin;
          childMarginTop = childMarginLayoutParams.topMargin;
          childMarginBottom = childMarginLayoutParams.bottomMargin;
        }
        else {
          childMarginLeft = 0;
          childMarginRight = 0;
          childMarginTop = 0;
          childMarginBottom = 0;
        }

        if (xpos + childMarginLeft + childWidth + childMarginRight + getPaddingRight() > r - l) {
          // this child will need to go on a new line

          xpos = getPaddingLeft();
          ypos += line_height;

          line_height = childHeight + childMarginTop + childMarginBottom;
        }
        else {
          // enough space for this child on the current line
          line_height = Math.max(
              line_height,
              childMarginTop + childHeight + childMarginBottom);
        }

        child.layout(
            xpos + childMarginLeft,
            ypos + childMarginTop,
            xpos + childMarginLeft + childWidth,
            ypos + childMarginTop + childHeight);

        xpos += childMarginLeft + childWidth + childMarginRight;
      }
    }
  }
}
 

15 comments:

Anonymous said...

thank you very much!
the most useful code for FlowLayout for Android I have found so far in the web!
just a single class and the trick is done, no exceptions or errors so far :D

Anonymous said...

I just wanted to say THANK YOU for this invaluable peace of code :)

Anonymous said...

Thanks a lot!!the best one.

Anonymous said...

Is there a specific credit line you would like us to add when we use this code?

adil said...

No, it's fine. You can put in a link to this Blog post if you like but you don't have to.

Unknown said...

How can I make the children be horizontal centred in this layout?

adil said...

Hi Ravil, I've not tried that. You could try setting a "center" value for the "gravity" attribute. Otherwise you might have to play around with and customise the view further.

Unknown said...

Adil, thanks for your reply. Setting a "center" value for the "gravity" attribute of layout doesn't work for me. Do you have any thoughts on further customizing it?

adil said...

Hi Ravil, looking at the code it looks like you're going to have to calculate exactly where to place each child view if you want them centre-aligned rather than left-aligned. I can't see a quick fix unfortunately.

HK said...

A code that "works" ..
Thanks a lot buddy !!!

Unknown said...

hi
thanks for you code.

how can i start to add textview from right to left in this flowlayout.

thanks.

adil said...

Hi Wali, the implementation as is assumes left-to-right placement of sub-views. To allow for right-to-left placement you'll have to modify the onMeasure(...) and onLayout(...) methods to do what you want. Now that you mention it, I'll probably modify the HorizontalFlowLayout view to do this but I won't get time to do so any time soon unfortunately :-(

Unknown said...

how can i use this class for placing dynamically produced buttons in a linear layout?

adil said...

Hossein, you add views to it like you would any other LinearLayout or RelativeLayout by calling one of its addView(...) methods.

managed it services said...

nice blog.