package de.blau.android.views;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* This is an ugly hack providing something like a flow layout.
* The children are arranged in rows of horizontal linear layouts,
* which are in turn arranged in a vertical linear layout (this class).
*
* The actual children can be read and set using {@link #getWrappedChildren()}
* and {@link #setWrappedChildren(ArrayList)}.
* Accessing this view's children directly, e.g. via {@link #getChildAt(int)},
* will yield the horizontal linear layouts that may be replaced at any time.
*
* This layout should be usable via inflation from XML - the original children
* are loaded into this class, wrapped in linear layouts and then re-inserted.
* However, advanced attributes need to be set in the code.
*
* @author Jan Schejbal
*
*/
public class WrappingLayout extends LinearLayout {
private boolean needsRelayout = false;
private boolean relayoutInProgress = false;
private boolean isWrapped = false;
private final LayoutWrapper wrapper;
private ArrayList<View> children;
public WrappingLayout(Context context) {
super(context);
wrapper = new LayoutWrapper(context);
}
public WrappingLayout(Context context, AttributeSet attrs) {
super(context, attrs);
wrapper = new LayoutWrapper(context);
}
public WrappingLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
wrapper = new LayoutWrapper(context);
}
/**
* @return he list of child views that are being line-wrapped
*/
public ArrayList<View> getWrappedChildren() {
eatChildrenIfNecessary();
return children;
}
/**
* Sets the list of child views that should be line-wrapped
* @param children the children to line-wrap
*/
public void setWrappedChildren(ArrayList<View> children) {
this.children = children;
requestLayout();
}
/**
* (Re)does the line-breaking. Use e.g. if you change the size of child elements.
*/
private void triggerRelayout() {
needsRelayout = true;
requestLayout();
}
/**
* Sets the row gravity, i.e. the alignment of items inside rows
* @param gravity the {@link Gravity} to set
* @return LayoutWrapper object (for chaining)
*/
public LayoutWrapper setRowGravity(int gravity) {
return wrapper.setRowGravity(gravity);
}
/**
* Sets whether children will be added from left to right (default, false) or from right to the left (true).
* Most useful with {@link #setRowGravity(int)} set to {@link Gravity#RIGHT}.
* @param rightToLeft
* @return LayoutWrapper object (for chaining)
*/
public LayoutWrapper setRightToLeft(boolean rightToLeft) {
return wrapper.setRightToLeft(rightToLeft);
}
/**
* Sets the vertical spacing in pixels between rows of elements
* @param pixel spacing
* @return LayoutWrapper object (for chaining)
*/
public LayoutWrapper setVerticalSpacing(int pixel) {
return wrapper.setVerticalSpacing(pixel);
}
/**
* Sets the horizontal spacing in pixels between elements
* @param pixel spacing
* @return LayoutWrapper object (for chaining)
*/
public LayoutWrapper setHorizontalSpacing(int pixel) {
return wrapper.setHorizontalSpacing(pixel);
}
/**
* After inflating from XML, calls {@link #eatChildren()} to properly line-wrap children
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
eatChildren();
}
/**
* Removes children that belong directly to the WrappingLayout,
* re-adding them as wrapped children.
* This allows to set the WrappingLayout contents from XML
*/
private void eatChildren() {
ArrayList<View> tmpChildren = new ArrayList<View>();
int count = getChildCount();
for (int i = 0; i < count; i++) {
tmpChildren.add(getChildAt(i));
}
setWrappedChildren(tmpChildren);
}
private void eatChildrenIfNecessary() {
if (children == null) eatChildren();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w != oldw) triggerRelayout();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (needsRelayout) {
performRelayout();
} else {
if (!relayoutInProgress) super.onLayout(changed, l, t, r, b);
}
}
/**
* (Re)does the line wrapping if necessary
*/
private void performRelayout() {
if (!needsRelayout) return;
needsRelayout = false;
relayoutInProgress = true;
if (isWrapped) {
wrapper.unwrap(this);
}
removeAllViews();
wrapper.wrap(children, this);
isWrapped = true;
relayoutInProgress = false;
post(new Runnable() {
@Override
public void run() {
requestLayout();
invalidate();
}
});
}
/**
* Helper class performing the actual line-wrapping of elements into a LinearLayout
* @author Jan
*/
public static class LayoutWrapper {
private final static String LOGTAG = LayoutWrapper.class.getSimpleName();
private Context context;
private int rowGravity;
private boolean rightToLeft;
private int hspace = 0;
private int vspace = 0;
private boolean widthAdjustmentDone = false;
private int newWidth = 0;
private static final int MEASURE_SPEC_UNSPECIFIED = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
public LayoutWrapper(Context context) {
this.context = context;
}
public LayoutWrapper setRowGravity(int gravity) {
rowGravity = gravity;
return this;
}
public LayoutWrapper setRightToLeft(boolean rightToLeft) {
this.rightToLeft = rightToLeft;
return this;
}
public LayoutWrapper setVerticalSpacing(int pixel) {
vspace = pixel;
return this;
}
public LayoutWrapper setHorizontalSpacing(int pixel) {
hspace = pixel;
return this;
}
/**
* Line-Wraps the children into the container. The container must have a width assigned,
* i.e. the container layout should be finished
* @param children the views to layout
* @param container the LinearLayout that should contain the children
*/
public void wrap(List<View> children, LinearLayout container) {
container.setOrientation(LinearLayout.VERTICAL);
if (children == null) {
Log.e("WrappingLayout","wrap: childern null");
return;
}
LayoutParams innerLayoutParams =
new LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
final int availableSpace = container.getWidth() - container.getPaddingLeft() - container.getPaddingRight();
int usedSpace = 0;
if (children.size() > 0 && !widthAdjustmentDone) {
int childWidth = getViewWidth(children.get(0));
float times = (availableSpace - hspace)/(float)(childWidth + hspace);
newWidth = (availableSpace - (((int)times+1)*hspace))/((int)times);
widthAdjustmentDone = true;
}
LinearLayout inner = new LinearLayout(context);
inner.setGravity(rowGravity);
inner.setOrientation(LinearLayout.HORIZONTAL);
// not only For new rows, set margin
innerLayoutParams.topMargin = vspace;
container.addView(inner, new LayoutParams((android.view.ViewGroup.MarginLayoutParams)innerLayoutParams));
if (availableSpace == 0) {
Log.e(LOGTAG, "No width information - read documentation!");
}
for (View child : children) {
int childWidth = getViewWidth(child);
if (newWidth > childWidth) { //TODOthis will fail with non square children views
((TextView)child).setWidth(newWidth);
((TextView)child).setHeight(newWidth);
}
childWidth = getViewWidth(child);
if ((usedSpace + hspace + childWidth) > availableSpace) {
// did not fit, create new row
inner = new LinearLayout(context);
inner.setOrientation(LinearLayout.HORIZONTAL);
inner.setGravity(rowGravity);
container.addView(inner, new LayoutParams((android.view.ViewGroup.MarginLayoutParams)innerLayoutParams));
usedSpace = 0;
}
// adding to current row
// add horizontal spacing if necessary
if (hspace > 0) {
if (rightToLeft) {
inner.addView(new SpacerView(context, hspace, 0), 0);
} else {
inner.addView(new SpacerView(context, hspace, 0));
}
usedSpace += hspace;
}
// add to whatever is the current row now
if (rightToLeft) {
inner.addView(child, 0);
} else {
inner.addView(child);
}
usedSpace += childWidth;
}
}
/**
* Fully unwraps the given layout.
*
* The children are cleanly removed from the row layouts so that they can be re-attached elsewhere.
* Then, the row layouts themselves are removed from the linear layout.
*
* @param wrappedLayout a {@link LinearLayout} wrapped with {@link #wrap(List, LinearLayout)}
*/
public void unwrap(LinearLayout wrappedLayout) {
int count = wrappedLayout.getChildCount();
for (int i = 0; i < count; i++) {
LinearLayout row = (LinearLayout)wrappedLayout.getChildAt(i);
row.removeAllViews();
}
wrappedLayout.removeAllViews();
}
/**
* Measures a view to get its width
* @param view the view to measure
* @return the width including margins
*/
private int getViewWidth(View view) {
view.measure(MEASURE_SPEC_UNSPECIFIED, MEASURE_SPEC_UNSPECIFIED);
int width = view.getMeasuredWidth(); // includes padding, does not include margins
LayoutParams params = (LayoutParams)(view.getLayoutParams());
if (params != null) {
width += params.leftMargin + params.rightMargin;
}
return width;
}
private static class SpacerView extends View {
private SpacerView(Context ctx, int width, int height) {
super(ctx);
setLayoutParams(new LayoutParams(width, height));
}
}
}
}