package org.openintents.plaphoons.ui.widget;
import org.openintents.plaphoons.sample.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* A layout that arranges views into a grid of same-sized squares.
*
* This source code contained in this file is in the Public Domain.
*
* @author Tom Gibara
*
*/
public class SquareGridLayout extends ViewGroup {
// fields
/**
* Records the number of views on each side of the square (ie. the number of
* rows and columns)
*/
private int mSize = 1;
/**
* Records the size of the square in pixels (excluding padding). This is set
* during {@link #onMeasure(int, int)}
*/
private int mSquareDimensions;
private int mColumns = 6;
private int mRows = 4;
private int mDivisor;
// constructors
/**
* Constructor used to create layout programatically.
*/
public SquareGridLayout(Context context) {
super(context);
}
/**
* Constructor used to inflate layout from XML. It extracts the size from
* the attributes and sets it.
*/
/*
* This requires a resource to be defined like this:
*
* <resources> <declare-styleable name="SquareGridLayout"> <attr name="size"
* format="integer"/> </declare-styleable> </resources>
*
* So that the attribute can be set like this:
*
* <com.tomgibara.android.util.SquareGridLayout
* xmlns:android="http://schemas.android.com/apk/res/android" xmlns:util=
* "http://schemas.android.com/apk/res/com.tomgibara.android.background"
* util:size="3" />
*/
public SquareGridLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = getContext().obtainStyledAttributes(attrs,
R.styleable.SquareGridLayout);
setSize(a.getInt(R.styleable.SquareGridLayout_size, 1));
a.recycle();
}
// accessors
/**
* Sets the number of views on each side of the square.
*
* @param size
* the size of grid (at least 1)
*/
public void setSize(int size) {
if (size < 1)
throw new IllegalArgumentException("size must be positive");
if (mSize != size) {
mSize = size;
requestLayout();
}
}
// View methods
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// breakdown specs
final int mw = MeasureSpec.getMode(widthMeasureSpec);
final int mh = MeasureSpec.getMode(heightMeasureSpec);
final int sw = MeasureSpec.getSize(widthMeasureSpec);
final int sh = MeasureSpec.getSize(heightMeasureSpec);
// compute padding
final int pw = getPaddingLeft() + getPaddingRight();
final int ph = getPaddingTop() + getPaddingBottom();
// compute largest size of square (both with and without padding)
final int s;
final int sp;
final int d;
if (mw == MeasureSpec.UNSPECIFIED && mh == MeasureSpec.UNSPECIFIED) {
throw new IllegalArgumentException(
"Layout must be constrained on at least one axis");
} else if (mw == MeasureSpec.UNSPECIFIED) {
s = sh;
sp = s - ph;
d = mRows;
} else if (mh == MeasureSpec.UNSPECIFIED) {
s = sw;
sp = s - pw;
d = mColumns;
} else {
if ((sw - pw) / mColumns < (sh - ph) / mRows) {
s = sw;
sp = s - pw;
d = mColumns;
} else {
s = sh;
sp = s - ph;
d = mRows;
}
}
// guard against giving the children a -ve measure spec due to excessive
// padding
final int spp = Math.max(sp, 0);
int spec = MeasureSpec.makeMeasureSpec(spp / d - 10,
MeasureSpec.EXACTLY);
// pass on our rigid dimensions to our children
final int size = mSize;
for (int y = 0; y < mRows; y++) {
for (int x = 0; x < mColumns; x++) {
final View child = getChildAt(y * mColumns + x);
if (child == null)
continue;
// measure each child
// we could try to accommodate oversized children, but we don't
measureChildWithMargins(child, spec, 0, spec, 0);
}
}
// record our dimensions
setMeasuredDimension(mw == MeasureSpec.EXACTLY ? sw : (sp / d) * mRows
+ pw, mh == MeasureSpec.EXACTLY ? sh : (sp / d) * mColumns + ph);
mSquareDimensions = sp;
mDivisor = d;
}
// ViewGroup methods
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// recover the previously computed square dimensions for efficiency
final int s = mSquareDimensions;
final int d = mDivisor;
{
// adjust for our padding
final int pl = getPaddingLeft();
final int pt = getPaddingTop();
final int pr = getPaddingRight();
final int pb = getPaddingBottom();
// allocate any extra spare space evenly
l = pl + (r - pr - l - pl - ((s / d) * mColumns)) / 2;
t = pt + (b - pb - t - pb - ((s / d) * mRows)) / 2;
}
final int size = mSize;
for (int y = 0; y < mRows; y++) {
for (int x = 0; x < mColumns; x++) {
View child = getChildAt(y * mColumns + x);
// optimization: we are moving through the children in order
// when we hit null, there are no more children to layout so
// return
if (child == null)
return;
// get the child's layout parameters so that we can honour their
// margins
MarginLayoutParams lps = (MarginLayoutParams) child
.getLayoutParams();
// we don't support gravity, so the arithmetic is simplified
child.layout(l + (s * x) / d + 5, t + (s * y) / d + 5, l
+ (s * (x + 1)) / d - 5, t + (s * (y + 1)) / d - 5);
}
}
}
public void setSize(int rows, int columns) {
mRows = rows;
mColumns = columns;
invalidate();
}
}