Android 的小工具布局

我试图创建一个向用户显示一些数据的活动。数据是这样的,它可以分为“单词”,每个单词都是一个小部件,“单词”的序列将形成数据(“句子”?)包含单词的 ViewGroup 小部件。由于一个“句子”中所有“单词”所需的空间将超过显示屏上可用的水平空间,我想把这些“句子”包装起来,就像包装一个普通的文本一样。

以下代码:

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);
}
}

产生类似于左边图片的东西,但是我想要一个像右边那样显示相同的小部件的布局。

non-wrapping

wrapping

是否有这样的布局或布局和参数的组合,或者我必须为此实现自己的 ViewGroup?

49069 次浏览

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.

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

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.

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>

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


}

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;
}
}
}
}

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.

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

enter image description here

    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;
}
}

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;
    }
    }
    }
    }
    

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;
}
}
}

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/

try :

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