/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import com.android.internal.R.styleable; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static java.lang.Math.max; import static java.lang.Math.min; /** * A layout that places its children in a rectangular grid. *

* The grid is composed of a set of infinitely thin lines that separate the * viewing area into cells. Throughout the API, grid lines are referenced * by grid indices. A grid that has N columns * has N + 1 grid indices that run from 0 * through N inclusive. Regardless of how GridLayout is * configured, grid index 0 is fixed to the leading edge of the * container and grid index N is fixed to its trailing edge * (after padding is taken into account). * *

Row and Column Groups

* * Children occupy one or more contiguous cells, as defined * by their {@link GridLayout.LayoutParams#rowGroup rowGroup} and * {@link GridLayout.LayoutParams#columnGroup columnGroup} layout parameters. * Each group specifies the set of rows or columns that are to be * occupied; and how children should be aligned within the resulting group of cells. * Although cells do not normally overlap in a GridLayout, GridLayout does * not prevent children being defined to occupy the same cell or group of cells. * In this case however, there is no guarantee that children will not themselves * overlap after the layout operation completes. * *

Default Cell Assignment

* * If no child specifies the row and column indices of the cell it * wishes to occupy, GridLayout assigns cell locations automatically using its: * {@link GridLayout#setOrientation(int) orientation}, * {@link GridLayout#setRowCount(int) rowCount} and * {@link GridLayout#setColumnCount(int) columnCount} properties. * *

Space

* * Space between children may be specified either by using instances of the * dedicated {@link Space} view or by setting the * * {@link ViewGroup.MarginLayoutParams#leftMargin leftMargin}, * {@link ViewGroup.MarginLayoutParams#topMargin topMargin}, * {@link ViewGroup.MarginLayoutParams#rightMargin rightMargin} and * {@link ViewGroup.MarginLayoutParams#bottomMargin bottomMargin} * * layout parameters. When the * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} * property is set, default margins around children are automatically * allocated based on the child's visual characteristics. Each of the * margins so defined may be independently overridden by an assignment * to the appropriate layout parameter. * *

Excess Space Distribution

* * Like {@link LinearLayout}, a child's ability to stretch is controlled * using weights, which are specified using the * {@link GridLayout.LayoutParams#rowWeight rowWeight} and * {@link GridLayout.LayoutParams#columnWeight columnWeight} layout parameters. *

*

* See {@link GridLayout.LayoutParams} for a full description of the * layout parameters used by GridLayout. * * @attr ref android.R.styleable#GridLayout_orientation * @attr ref android.R.styleable#GridLayout_rowCount * @attr ref android.R.styleable#GridLayout_columnCount * @attr ref android.R.styleable#GridLayout_useDefaultMargins * @attr ref android.R.styleable#GridLayout_rowOrderPreserved * @attr ref android.R.styleable#GridLayout_columnOrderPreserved */ public class GridLayout extends ViewGroup { // Public constants /** * The horizontal orientation. */ public static final int HORIZONTAL = LinearLayout.HORIZONTAL; /** * The vertical orientation. */ public static final int VERTICAL = LinearLayout.VERTICAL; // Misc constants private static final String TAG = GridLayout.class.getName(); private static final boolean DEBUG = false; private static final int UNDEFINED = Integer.MIN_VALUE; private static final Paint GRID_PAINT = new Paint(); private static final double GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2; private static final int MIN = 0; private static final int PRF = 1; private static final int MAX = 2; // Defaults private static final int DEFAULT_ORIENTATION = HORIZONTAL; private static final int DEFAULT_COUNT = UNDEFINED; private static final boolean DEFAULT_USE_DEFAULT_MARGINS = false; private static final boolean DEFAULT_ORDER_PRESERVED = false; // TypedArray indices private static final int ORIENTATION = styleable.GridLayout_orientation; private static final int ROW_COUNT = styleable.GridLayout_rowCount; private static final int COLUMN_COUNT = styleable.GridLayout_columnCount; private static final int USE_DEFAULT_MARGINS = styleable.GridLayout_useDefaultMargins; private static final int ROW_ORDER_PRESERVED = styleable.GridLayout_rowOrderPreserved; private static final int COLUMN_ORDER_PRESERVED = styleable.GridLayout_columnOrderPreserved; // Instance variables private final Axis mHorizontalAxis = new Axis(true); private final Axis mVerticalAxis = new Axis(false); private boolean mLayoutParamsValid = false; private int mOrientation = DEFAULT_ORIENTATION; private boolean mUseDefaultMargins = DEFAULT_USE_DEFAULT_MARGINS; private int mDefaultGravity = Gravity.NO_GRAVITY; boolean maximizing = false; boolean accommodateBothMinAndMax = false; // Constructors /** * {@inheritDoc} */ public GridLayout(Context context) { super(context); if (DEBUG) { setWillNotDraw(false); } } /** * {@inheritDoc} */ public GridLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); processAttributes(context, attrs); } /** * {@inheritDoc} */ public GridLayout(Context context, AttributeSet attrs) { super(context, attrs); processAttributes(context, attrs); } private void processAttributes(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, styleable.GridLayout); try { setRowCount(a.getInteger(ROW_COUNT, DEFAULT_COUNT)); setColumnCount(a.getInteger(COLUMN_COUNT, DEFAULT_COUNT)); mOrientation = a.getInteger(ORIENTATION, DEFAULT_ORIENTATION); mUseDefaultMargins = a.getBoolean(USE_DEFAULT_MARGINS, DEFAULT_USE_DEFAULT_MARGINS); setRowOrderPreserved(a.getBoolean(ROW_ORDER_PRESERVED, DEFAULT_ORDER_PRESERVED)); setColumnOrderPreserved(a.getBoolean(COLUMN_ORDER_PRESERVED, DEFAULT_ORDER_PRESERVED)); } finally { a.recycle(); } } // Implementation /** * Returns the current orientation. * * @return either {@link #HORIZONTAL} or {@link #VERTICAL}. The default * is {@link #HORIZONTAL}. * * @see #setOrientation(int) * * @attr ref android.R.styleable#GridLayout_orientation */ public int getOrientation() { return mOrientation; } /** * The orientation property does not affect layout. Orientation is used * only to generate default row/column indices when they are not specified * by a component's layout parameters. * * @param orientation the orientation, either {@link #HORIZONTAL} or {@link #VERTICAL}. * * @see #getOrientation() * * @attr ref android.R.styleable#GridLayout_orientation */ public void setOrientation(int orientation) { if (mOrientation != orientation) { mOrientation = orientation; requestLayout(); } } /** * Returns the current number of rows. This is either the last value that was set * with {@link #setRowCount(int)} or, if no such value was set, the maximum * value of each the upper bounds defined in {@link LayoutParams#rowGroup}. * * @return the current number of rows * * @see #setRowCount(int) * @see LayoutParams#rowGroup * * @attr ref android.R.styleable#GridLayout_rowCount */ public int getRowCount() { return mVerticalAxis.getCount(); } /** * The rowCount property does not affect layout. RowCount is used * only to generate default row/column indices when they are not specified * by a component's layout parameters. * * @param rowCount the number of rows. * * @see #getRowCount() * @see LayoutParams#rowGroup * * @attr ref android.R.styleable#GridLayout_rowCount */ public void setRowCount(int rowCount) { mVerticalAxis.setCount(rowCount); } /** * Returns the current number of columns. This is either the last value that was set * with {@link #setColumnCount(int)} or, if no such value was set, the maximum * value of each the upper bounds defined in {@link LayoutParams#columnGroup}. * * @return the current number of columns * * @see #setColumnCount(int) * @see LayoutParams#columnGroup * * @attr ref android.R.styleable#GridLayout_columnCount */ public int getColumnCount() { return mHorizontalAxis.getCount(); } /** * The columnCount property does not affect layout. ColumnCount is used * only to generate default column/column indices when they are not specified * by a component's layout parameters. * * @param columnCount the number of columns. * * @see #getColumnCount() * @see LayoutParams#columnGroup * * @attr ref android.R.styleable#GridLayout_columnCount */ public void setColumnCount(int columnCount) { mHorizontalAxis.setCount(columnCount); } /** * Returns whether or not this GridLayout will allocate default margins when no * corresponding layout parameters are defined. * * @return true if default margins should be allocated. * * @see #setUseDefaultMargins(boolean) * * @attr ref android.R.styleable#GridLayout_useDefaultMargins */ public boolean getUseDefaultMargins() { return mUseDefaultMargins; } /** * When true, GridLayout allocates default margins around children * based on the child's visual characteristics. Each of the * margins so defined may be independently overridden by an assignment * to the appropriate layout parameter. *

* When false, the default value of all margins is zero. * * @param useDefaultMargins use true to make GridLayout allocate default margins * * @see #getUseDefaultMargins() * * @see MarginLayoutParams#leftMargin * @see MarginLayoutParams#topMargin * @see MarginLayoutParams#rightMargin * @see MarginLayoutParams#bottomMargin * * @attr ref android.R.styleable#GridLayout_useDefaultMargins */ public void setUseDefaultMargins(boolean useDefaultMargins) { mUseDefaultMargins = useDefaultMargins; } /** * Returns whether or not row boundaries are ordered by their grid indices. * * @return true if row boundaries must appear in the order of their indices, false otherwise. * The default is false. * * @see #setRowOrderPreserved(boolean) * * @attr ref android.R.styleable#GridLayout_rowOrderPreserved */ public boolean isRowOrderPreserved() { return mVerticalAxis.isOrderPreserved(); } /** * When this property is false, the default state, GridLayout * is at liberty to choose an order that better suits the heights of its children.

* When this property is true, GridLayout is forced to place row boundaries * (the {@link Interval#min min} and {@link Interval#max max} values of * a {@link LayoutParams#rowGroup rowGroup}'s {@link Group#span span}) * so that they appear in ascending order in the view. *

* GridLayout implements this specification by creating ordering constraints between * the variables that represent the locations of the row boundaries. * * When this property is true, constraints are added for each pair of consecutive * indices: i.e. between row boundaries: [0..1], [1..2], [3..4],... etc. * * When the property is false, the ordering constraints are placed * only between boundaries that separate opposing edges of the layout's children. * * @param rowOrderPreserved use true to force GridLayout to respect the order * of row boundaries. * * @see #isRowOrderPreserved() * * @attr ref android.R.styleable#GridLayout_rowOrderPreserved */ public void setRowOrderPreserved(boolean rowOrderPreserved) { mVerticalAxis.setOrderPreserved(rowOrderPreserved); } /** * Returns whether or not column boundaries are ordered by their grid indices. * * @return true if column boundaries must appear in the order of their indices, false otherwise. * The default is false. * * @see #setColumnOrderPreserved(boolean) * * @attr ref android.R.styleable#GridLayout_columnOrderPreserved */ public boolean isColumnOrderPreserved() { return mHorizontalAxis.isOrderPreserved(); } /** * When this property is false, the default state, GridLayout * is at liberty to choose an order that better suits the widths of its children.

* When this property is true, GridLayout is forced to place column boundaries * (the {@link Interval#min min} and {@link Interval#max max} values of * a {@link LayoutParams#columnGroup columnGroup}'s {@link Group#span span}) * so that they appear in ascending order in the view. *

* GridLayout implements this specification by creating ordering constraints between * the variables that represent the locations of the column boundaries. * * When this property is true, constraints are added for each pair of consecutive * indices: i.e. between column boundaries: [0..1], [1..2], [3..4],... etc. * * When the property is false, the ordering constraints are placed * only between boundaries that separate opposing edges of the layout's children. * * @param columnOrderPreserved use true to force GridLayout to respect the order * of column boundaries. * * @see #isColumnOrderPreserved() * * @attr ref android.R.styleable#GridLayout_columnOrderPreserved */ public void setColumnOrderPreserved(boolean columnOrderPreserved) { mHorizontalAxis.setOrderPreserved(columnOrderPreserved); } private static int compare(int i, int j) { return i < j ? -1 : i > j ? 1 : 0; } private static int sum(int[] a) { int result = 0; for (int i = 0, length = a.length; i < length; i++) { result += a[i]; } return result; } private int getDefaultMargin(View c, boolean leading, boolean horizontal) { // In the absence of any other information, calculate a default gap such // that, in a grid of identical components, the heights and the vertical // gaps are in the proportion of the golden ratio. // To effect this with equal margins at each edge, set each of the // four margin values to half this amount. c.measure(0, 0); return (int) (c.getMeasuredHeight() / GOLDEN_RATIO / 2); } private int getDefaultMargin(View c, boolean isAtEdge, boolean leading, boolean horizontal) { // todo remove 20 - use padding here? return isAtEdge ? 20 : getDefaultMargin(c, leading, horizontal); } private int getDefaultMarginValue(View c, LayoutParams p, boolean leading, boolean horizontal) { if (!mUseDefaultMargins) { return 0; } Group group = horizontal ? p.columnGroup : p.rowGroup; Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; Interval span = group.span; boolean isAtEdge = leading ? span.min == 0 : span.max == axis.getCount(); return getDefaultMargin(c, isAtEdge, leading, horizontal); } private int getMargin(View view, boolean leading, boolean horizontal) { LayoutParams lp = getLayoutParams(view); int margin = horizontal ? leading ? lp.leftMargin : lp.rightMargin : leading ? lp.topMargin : lp.bottomMargin; return margin == UNDEFINED ? getDefaultMarginValue(view, lp, leading, horizontal) : margin; } private static boolean isUndefined(Interval span) { return span.min == UNDEFINED || span.max == UNDEFINED; } private void validateLayoutParams() { // install default indices for cells if *none* are defined if (mHorizontalAxis.maxIndex1() == UNDEFINED || mVerticalAxis.maxIndex1() == UNDEFINED) { boolean horizontal = mOrientation == HORIZONTAL; int count = horizontal ? mHorizontalAxis.count : mVerticalAxis.count; if (count == UNDEFINED) { count = Integer.MAX_VALUE; } int x = 0; int y = 0; int maxSize = 0; for (int i = 0, size = getChildCount(); i < size; i++) { LayoutParams lp = getLayoutParams1(getChildAt(i)); Interval hSpan = lp.columnGroup.span; int cellWidth = hSpan.size(); Interval vSpan = lp.rowGroup.span; int cellHeight = vSpan.size(); if (horizontal) { if (x + cellWidth > count) { x = 0; y += maxSize; maxSize = 0; } } else { if (y + cellHeight > count) { y = 0; x += maxSize; maxSize = 0; } } lp.setHorizontalGroupSpan(new Interval(x, x + cellWidth)); lp.setVerticalGroupSpan(new Interval(y, y + cellHeight)); if (horizontal) { x = x + cellWidth; } else { y = y + cellHeight; } maxSize = max(maxSize, horizontal ? cellHeight : cellWidth); } } else { /* At least one row and one column index have been defined. Assume missing row/cols are in error and set them to zero so that they will display top/left and the developer can add the right indices. Without this UNDEFINED would cause ArrayIndexOutOfBoundsException. */ for (int i = 0, size = getChildCount(); i < size; i++) { LayoutParams lp = getLayoutParams1(getChildAt(i)); if (isUndefined(lp.columnGroup.span)) { lp.setHorizontalGroupSpan(LayoutParams.DEFAULT_SPAN); } if (isUndefined(lp.rowGroup.span)) { lp.setVerticalGroupSpan(LayoutParams.DEFAULT_SPAN); } } } } private void invalidateStructure() { mLayoutParamsValid = false; mHorizontalAxis.invalidateStructure(); mVerticalAxis.invalidateStructure(); // This can end up being done twice. But better that than not at all. invalidateValues(); } private void invalidateValues() { mHorizontalAxis.invalidateValues(); mVerticalAxis.invalidateValues(); } private LayoutParams getLayoutParams1(View c) { return (LayoutParams) c.getLayoutParams(); } private LayoutParams getLayoutParams(View c) { if (!mLayoutParamsValid) { validateLayoutParams(); mLayoutParamsValid = true; } return getLayoutParams1(c); } @Override protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs, mDefaultGravity); } @Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } // Draw grid private void drawLine(Canvas graphics, int x1, int y1, int x2, int y2, Paint paint) { int dx = getPaddingLeft(); int dy = getPaddingTop(); graphics.drawLine(dx + x1, dy + y1, dx + x2, dy + y2, paint); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (DEBUG) { int height = getHeight() - getPaddingTop() - getPaddingBottom(); int width = getWidth() - getPaddingLeft() - getPaddingRight(); int[] xs = mHorizontalAxis.locations; for (int i = 0, length = xs.length; i < length; i++) { int x = xs[i]; drawLine(canvas, x, 0, x, height - 1, GRID_PAINT); } int[] ys = mVerticalAxis.locations; for (int i = 0, length = ys.length; i < length; i++) { int y = ys[i]; drawLine(canvas, 0, y, width - 1, y, GRID_PAINT); } } } static { GRID_PAINT.setColor(Color.argb(50, 255, 255, 255)); } // Add/remove @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { super.addView(child, index, params); invalidateStructure(); } @Override public void removeView(View view) { super.removeView(view); invalidateStructure(); } @Override public void removeViewInLayout(View view) { super.removeViewInLayout(view); invalidateStructure(); } @Override public void removeViewsInLayout(int start, int count) { super.removeViewsInLayout(start, count); invalidateStructure(); } @Override public void removeViewAt(int index) { super.removeViewAt(index); invalidateStructure(); } // Measurement @Override protected void onMeasure(int widthSpec, int heightSpec) { invalidateValues(); // int width = MeasureSpec.getSize(widthSpec); // int widthMode = MeasureSpec.getMode(widthSpec); // int height = MeasureSpec.getSize(heightSpec); // int heightMode = MeasureSpec.getMode(heightSpec); // todo - handle widthSpec and heightSpec properly int computedWidth = getPaddingLeft() + mHorizontalAxis.getPref() + getPaddingRight(); int computedHeight = getPaddingTop() + mVerticalAxis.getPref() + getPaddingBottom(); setMeasuredDimension( resolveSizeAndState(computedWidth, widthSpec, 0), resolveSizeAndState(computedHeight, heightSpec, 0)); } private int protect(int alignment) { return alignment == UNDEFINED ? 0 : alignment; } private int getLocationIncludingMargin(Axis state, int index, boolean leading) { int margin = leading ? state.leadingMargins[index] : -state.trailingMargins[index]; return state.locations[index] + margin; } private int getMeasurement(View c, boolean horizontal, int measurementType) { LayoutParams lp = (LayoutParams) c.getLayoutParams(); // First check to see if the user has specified the size. // If so, return the specified size. int size = horizontal ? lp.width : lp.height; if (size >= 0) { return size; } // measureChild(c, 0, 0); c.measure(0, 0);// todo work out correct order of events for measurement calls int result = horizontal ? c.getMeasuredWidth() : c.getMeasuredHeight(); float weight = horizontal ? lp.columnWeight : lp.rowWeight; Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; if (weight != 0) { return result + axis.prefSizeOfWeightedComponent; } return result; } // Layout container @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { invalidateValues(); int targetWidth = r - l; int targetHeight = b - t; int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); mHorizontalAxis.layout(targetWidth - paddingLeft - paddingRight); mVerticalAxis.layout(targetHeight - paddingTop - paddingBottom); for (int i = 0, size = getChildCount(); i < size; i++) { View view = getChildAt(i); LayoutParams constraints = getLayoutParams(view); Interval hRange = constraints.columnGroup.span; Interval vRange = constraints.rowGroup.span; int x1 = getLocationIncludingMargin(mHorizontalAxis, hRange.min, true); int y1 = getLocationIncludingMargin(mVerticalAxis, vRange.min, true); int x2 = getLocationIncludingMargin(mHorizontalAxis, hRange.max, false); int y2 = getLocationIncludingMargin(mVerticalAxis, vRange.max, false); int cellWidth = x2 - x1; int cellHeight = y2 - y1; Bounds minMaxX = mHorizontalAxis.getGroupBounds().getValue(i); Bounds minMaxY = mVerticalAxis.getGroupBounds().getValue(i); int pWidth = getMeasurement(view, true, PRF); int pHeight = getMeasurement(view, false, PRF); Alignment hAlignment = constraints.columnGroup.alignment; Alignment vAlignment = constraints.rowGroup.alignment; int ddx = protect(hAlignment.getAlignmentValue(null, cellWidth - minMaxX.size())); int ddy = protect(vAlignment.getAlignmentValue(null, cellHeight - minMaxY.size())); int dx = ddx + -minMaxX.below - hAlignment.getAlignmentValue(view, pWidth); int dy = ddy + -minMaxY.below - vAlignment.getAlignmentValue(view, pHeight); int width = hAlignment.getSizeInCell(view, pWidth, cellWidth); int height = vAlignment.getSizeInCell(view, pHeight, cellHeight); int cx = paddingLeft + x1 + dx; int cy = paddingTop + y1 + dy; view.layout(cx, cy, cx + width, cy + height); } } // Inner classes private class Axis { private static final int MIN_VALUE = -1000000; private static final int MAX_VALUE = 1000000; private static final int UNVISITED = 0; private static final int PENDING = 1; private static final int COMPLETE = 2; public final boolean horizontal; public int count = UNDEFINED; public boolean countValid = false; public boolean countWasExplicitySet = false; PackedMap groupBounds; public boolean groupBoundsValid = false; PackedMap spanSizes; public boolean spanSizesValid = false; public int[] locations; public int[] leadingMargins; public int[] trailingMargins; public Arc[] arcs; public boolean arcsValid = false; private boolean mOrderPreserved = DEFAULT_ORDER_PRESERVED; public int prefSizeOfWeightedComponent; private Axis(boolean horizontal) { this.horizontal = horizontal; } private int maxIndex(boolean internal) { // note the number Integer.MIN_VALUE + 1 comes up in undefined cells int count = -1; for (int i = 0, size = getChildCount(); i < size; i++) { LayoutParams params = internal ? getLayoutParams1(getChildAt(i)) : getLayoutParams(getChildAt(i)); Group g = horizontal ? params.columnGroup : params.rowGroup; count = max(count, g.span.min); count = max(count, g.span.max); } return count == -1 ? UNDEFINED : count; } private int maxIndex1() { return maxIndex(true); } public int getCount() { if (!countWasExplicitySet && !countValid) { count = max(0, maxIndex(false)); // if there are no cells, the count is zero countValid = true; } return count; } public void setCount(int count) { this.count = count; this.countWasExplicitySet = count != UNDEFINED; } public boolean isOrderPreserved() { return mOrderPreserved; } public void setOrderPreserved(boolean orderPreserved) { mOrderPreserved = orderPreserved; invalidateStructure(); } private PackedMap createGroupBounds() { int N = getChildCount(); Group[] groups = new Group[N]; Bounds[] bounds = new Bounds[N]; for (int i = 0; i < N; i++) { LayoutParams lp = getLayoutParams(getChildAt(i)); Group group = horizontal ? lp.columnGroup : lp.rowGroup; groups[i] = group; bounds[i] = new Bounds(); } return new PackedMap(groups, bounds); } private void computeGroupBounds() { for (int i = 0; i < groupBounds.values.length; i++) { groupBounds.values[i].reset(); } for (int i = 0, size = getChildCount(); i < size; i++) { View c = getChildAt(i); LayoutParams lp = getLayoutParams(c); Group g = horizontal ? lp.columnGroup : lp.rowGroup; Bounds bounds = groupBounds.getValue(i); int dim = getMeasurement(c, horizontal, PRF); // todo test this works correctly when the returned value is UNDEFINED int below = g.alignment.getAlignmentValue(c, dim); int above = dim - below; bounds.include(-below, above); } } private PackedMap getGroupBounds() { if (groupBounds == null) { groupBounds = createGroupBounds(); } if (!groupBoundsValid) { computeGroupBounds(); groupBoundsValid = true; } return groupBounds; } // Add values computed by alignment - taking the max of all alignments in each span private PackedMap createSpanSizes() { PackedMap groupBounds = getGroupBounds(); int N = groupBounds.keys.length; Interval[] spans = new Interval[N]; Int[] values = new Int[N]; for (int i = 0; i < N; i++) { Interval key = groupBounds.keys[i].span; spans[i] = key; values[i] = new Int(); } return new PackedMap(spans, values); } private void computeSpanSizes() { Int[] spans = spanSizes.values; for (int i = 0; i < spans.length; i++) { spans[i].reset(); } Bounds[] bounds = getGroupBounds().values; // us get to trigger a re-evaluation for (int i = 0; i < bounds.length; i++) { int value = bounds[i].size(); Int valueHolder = spanSizes.getValue(i); valueHolder.value = max(valueHolder.value, value); } } private PackedMap getSpanSizes() { if (spanSizes == null) { spanSizes = createSpanSizes(); } if (!spanSizesValid) { computeSpanSizes(); spanSizesValid = true; } return spanSizes; } private void include(List arcs, Interval key, Int size, boolean maximizing) { key = maximizing ? key.inverse() : key; size = maximizing ? size.neg() : size; // this bit below should really be computed outside here - // its just to stop default (col>0) constraints obliterating valid entries for (Arc arc : arcs) { Interval span = arc.span; if (span.equals(key)) { return; } } arcs.add(new Arc(key, size)); } private void include2(List arcs, Interval span, Int min, Int max, boolean both, boolean maximizing) { include(arcs, span, min, maximizing); if (both) { include(arcs, span.inverse(), max.neg(), maximizing); } } private void include2(List arcs, Interval span, int min, int max, boolean both, boolean maximizing) { include2(arcs, span, new Int(min), new Int(max), both, maximizing); } // Group arcs by their first index, returning an array of arrays. // This is linear in the number of arcs. private Arc[][] index(Arc[] arcs) { int N = getCount() + 1;// the number of vertices Arc[][] result = new Arc[N][]; int[] sizes = new int[N]; for (Arc arc : arcs) { sizes[arc.span.min]++; } for (int i = 0; i < sizes.length; i++) { result[i] = new Arc[sizes[i]]; } // reuse the sizes array to hold the current last elements as we insert each arc Arrays.fill(sizes, 0); for (Arc arc : arcs) { int i = arc.span.min; result[i][sizes[i]++] = arc; } return result; } // todo do we always add first element? private Arc[] sort(final Arc[] arcs, int start) { final List result = new ArrayList(); new Object() { Arc[][] index = index(arcs); int[] visited = new int[getCount() + 1]; boolean completesCycle(int loc) { int state = visited[loc]; if (state == UNVISITED) { visited[loc] = PENDING; for (Arc arc : index[loc]) { Interval span = arc.span; // the recursive call if (completesCycle(span.max)) { // which arcs get set here is dependent on the order // in which we explore nodes arc.completesCycle = true; } result.add(arc); } visited[loc] = COMPLETE; } else if (state == PENDING) { return true; } else if (state == COMPLETE) { } return false; } }.completesCycle(start); Collections.reverse(result); assert arcs.length == result.size(); return result.toArray(new Arc[result.size()]); } private boolean[] findUsed(Collection arcs) { boolean[] result = new boolean[getCount()]; for (Arc arc : arcs) { Interval span = arc.span; int min = min(span.min, span.max); int max = max(span.min, span.max); for (int i = min; i < max; i++) { result[i] = true; } } return result; } // todo unify with findUsed above private Collection getSpacers() { List result = new ArrayList(); int N = getCount() + 1; int[] leadingEdgeCount = new int[N]; int[] trailingEdgeCount = new int[N]; for (int i = 0, size = getChildCount(); i < size; i++) { LayoutParams lp = getLayoutParams(getChildAt(i)); Group g = horizontal ? lp.columnGroup : lp.rowGroup; Interval span = g.span; leadingEdgeCount[span.min]++; trailingEdgeCount[span.max]++; } int lastTrailingEdge = 0; // treat the parent's edges like peer edges of the opposite type trailingEdgeCount[0] = 1; leadingEdgeCount[N - 1] = 1; for (int i = 0; i < N; i++) { if (trailingEdgeCount[i] > 0) { lastTrailingEdge = i; continue; // if this is also a leading edge, don't add a space of length zero } if (leadingEdgeCount[i] > 0) { result.add(new Interval(lastTrailingEdge, i)); } } return result; } private Arc[] createArcs(boolean maximizing) { List spanToSize = new ArrayList(); // Add all the preferred elements that were not defined by the user. PackedMap spanSizes = getSpanSizes(); for (int i = 0; i < spanSizes.keys.length; i++) { Interval key = spanSizes.keys[i]; Int value = spanSizes.values[i]; // todo remove value duplicate include2(spanToSize, key, value, value, accommodateBothMinAndMax, maximizing); } // Find redundant rows/cols and glue them together with 0-length arcs to link the tree boolean[] used = findUsed(spanToSize); for (int i = 0; i < getCount(); i++) { if (!used[i]) { Interval span = new Interval(i, i + 1); include(spanToSize, span, new Int(0), maximizing); include(spanToSize, span.inverse(), new Int(0), maximizing); } } if (mOrderPreserved) { // Add preferred gaps for (int i = 0; i < getCount(); i++) { if (used[i]) { include2(spanToSize, new Interval(i, i + 1), 0, 0, false, maximizing); } } } else { for (Interval gap : getSpacers()) { include2(spanToSize, gap, 0, 0, false, maximizing); } } Arc[] arcs = spanToSize.toArray(new Arc[spanToSize.size()]); return sort(arcs, maximizing ? getCount() : 0); } public Arc[] getArcs(boolean maximizing) { if (arcs == null) { arcs = createArcs(maximizing); } if (!arcsValid) { getSpanSizes(); arcsValid = true; } return arcs; } private boolean relax(int[] locations, Arc entry, boolean maximizing) { Interval span = entry.span; int u = span.min; int v = span.max; int value = entry.value.value; int candidate = locations[u] + value; if (maximizing ? candidate < locations[v] : candidate > locations[v]) { locations[v] = candidate; return true; } return false; } // Bellman-Ford variant private int[] solve(Arc[] arcs, int[] locations, boolean maximizing) { int N = getCount() + 1; // The number of vertices is the number of columns/rows + 1. boolean changed = false; // We take one extra pass over traditional Bellman-Ford (and omit their final step) for (int i = 0; i < N; i++) { changed = false; for (int j = 0, length = arcs.length; j < length; j++) { changed = changed | relax(locations, arcs[j], maximizing); } if (!changed) { if (DEBUG) { Log.d(TAG, "Iteration " + (maximizing ? "(max)" : "(min)") + " completed after " + (1 + i) + " steps out of " + N); } break; } } if (changed) { Log.d(TAG, "*** Algorithm failed to terminate ***"); } return locations; } private int[] init(int defaultValue, int min, int max) { int N = getCount() + 1; // The number of vertices is the number of columns/rows + 1. int[] locations = new int[N]; Arrays.fill(locations, defaultValue); locations[0] = min; locations[N - 1] = max; return locations; } private int[] computeMargins(boolean leading) { int[] result = new int[getCount() + 1]; for (int i = 0, size = getChildCount(); i < size; i++) { View c = getChildAt(i); LayoutParams lp = getLayoutParams(c); Group g = horizontal ? lp.columnGroup : lp.rowGroup; Interval span = g.span; int index = leading ? span.min : span.max; result[index] = max(result[index], getMargin(c, leading, horizontal)); } return result; } // has side effects private void computeLocations(int[] locations, boolean maximizing) { leadingMargins = computeMargins(true); trailingMargins = computeMargins(false); solve(getArcs(maximizing), locations, maximizing); // Add margins int delta = 0; for (int i = 0; i < getCount(); i++) { int margins = leadingMargins[i] + trailingMargins[i + 1]; delta += margins; locations[i + 1] += delta; } } private int size(int[] locations) { return locations[locations.length - 1] - locations[0]; } private int[] getLimit(boolean lowerBound, boolean maximizing) { int defaultValue = maximizing ? MAX_VALUE : MIN_VALUE; if (lowerBound) { // as long as it avoids overflow, the upper bound can be anything (including zero) int[] result = init(defaultValue, defaultValue, 1000); computeLocations(result, maximizing); int delta = result[0]; for (int i = 0; i < result.length; i++) { result[i] -= delta; } return result; } else { int[] result = init(defaultValue, 0, defaultValue); computeLocations(result, maximizing); return result; } } // External entry points private int getMin() { int[] mins = getLimit(maximizing, maximizing); return size(mins); } private int getPref() { return accommodateBothMinAndMax ? getMax() : getMin(); } private int getMax() { int[] maxs = getLimit(!maximizing, maximizing); return size(maxs); } private int totalMarginSize() { return sum(leadingMargins) + sum(trailingMargins); } private void layout(int targetSize) { int N = getCount() + 1; int min = getMin(); int max = getMax(); int clippedTargetSize = max(min(max, targetSize), min); // confine size to valid range if (DEBUG) { Log.d(TAG, "Computing sizes for target " + clippedTargetSize + " for " + (horizontal ? "col" : "row") + "s from: " + arcs); } int delta = clippedTargetSize - min; prefSizeOfWeightedComponent = delta; invalidateValues(); int defaultValue = maximizing ? MAX_VALUE : MIN_VALUE; locations = init(defaultValue, 0, clippedTargetSize - totalMarginSize()); computeLocations(locations, maximizing); prefSizeOfWeightedComponent = 0; if (DEBUG) { Log.d(TAG, "locations = " + Arrays.toString(locations)); int[] computedSizes = new int[N - 1]; for (int i = 0; i < N - 1; i++) { computedSizes[i] = locations[i + 1] - locations[i]; } Log.d(TAG, "sizes = " + Arrays.toString(computedSizes)); } } private void invalidateStructure() { countValid = false; groupBounds = null; spanSizes = null; invalidateValues(); } private void invalidateValues() { groupBoundsValid = false; spanSizesValid = false; arcsValid = false; } } /** * Layout information associated with each of the children of a GridLayout. *

* GridLayout supports both row and column spanning and arbitrary forms of alignment within * each cell group. The fundamental parameters associated with each cell group are * gathered into their vertical and horizontal components and stored * in the {@link #rowGroup} and {@link #columnGroup} layout parameters. * {@link Group Groups} are immutable structures and may be shared between the layout * parameters of different children. *

* The {@link Group#span span} fields of the row and column groups together specify * the four grid indices that delimit the cells of this cell group. *

* The {@link Group#alignment alignment} fields of the row and column groups together specify * both aspects of alignment within the cell group. It is also possible to specify a child's * alignment within its cell group by using the {@link GridLayout.LayoutParams#setGravity(int)} * method. *

* See {@link GridLayout} for a description of the conventions used by GridLayout * in reference to grid indices. * *

Default values

* * * * @attr ref android.R.styleable#GridLayout_Layout_layout_row * @attr ref android.R.styleable#GridLayout_Layout_layout_rowSpan * @attr ref android.R.styleable#GridLayout_Layout_layout_rowWeight * @attr ref android.R.styleable#GridLayout_Layout_layout_column * @attr ref android.R.styleable#GridLayout_Layout_layout_columnSpan * @attr ref android.R.styleable#GridLayout_Layout_layout_columnWeight * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity */ public static class LayoutParams extends MarginLayoutParams { // Default values private static final int DEFAULT_WIDTH = WRAP_CONTENT; private static final int DEFAULT_HEIGHT = WRAP_CONTENT; private static final int DEFAULT_MARGIN = UNDEFINED; private static final int DEFAULT_ROW = UNDEFINED; private static final int DEFAULT_COLUMN = UNDEFINED; private static final Interval DEFAULT_SPAN = new Interval(0, 1); private static final int DEFAULT_SPAN_SIZE = DEFAULT_SPAN.size(); private static final Alignment DEFAULT_HORIZONTAL_ALIGNMENT = LEFT; private static final Alignment DEFAULT_VERTCIAL_ALGIGNMENT = BASELINE; private static final Group DEFAULT_HORIZONTAL_GROUP = new Group(DEFAULT_SPAN, DEFAULT_HORIZONTAL_ALIGNMENT); private static final Group DEFAULT_VERTICAL_GROUP = new Group(DEFAULT_SPAN, DEFAULT_VERTCIAL_ALGIGNMENT); private static final int DEFAULT_WEIGHT = 0; // Misc private static final Rect CONTAINER_BOUNDS = new Rect(0, 0, 2, 2); private static final Alignment[] HORIZONTAL_ALIGNMENTS = { LEFT, CENTER, RIGHT }; private static final Alignment[] VERTICAL_ALIGNMENTS = { TOP, CENTER, BOTTOM }; // TypedArray indices private static final int MARGIN = styleable.ViewGroup_MarginLayout_layout_margin; private static final int LEFT_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginLeft; private static final int TOP_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginTop; private static final int RIGHT_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginRight; private static final int BOTTOM_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginBottom; private static final int COLUMN = styleable.GridLayout_Layout_layout_column; private static final int COLUMN_SPAN = styleable.GridLayout_Layout_layout_columnSpan; private static final int COLUMN_WEIGHT = styleable.GridLayout_Layout_layout_columnWeight; private static final int ROW = styleable.GridLayout_Layout_layout_row; private static final int ROW_SPAN = styleable.GridLayout_Layout_layout_rowSpan; private static final int ROW_WEIGHT = styleable.GridLayout_Layout_layout_rowWeight; private static final int GRAVITY = styleable.GridLayout_Layout_layout_gravity; // Instance variables /** * The group that specifies the vertical characteristics of the cell group * described by these layout parameters. */ public Group rowGroup; /** * The group that specifies the horizontal characteristics of the cell group * described by these layout parameters. */ public Group columnGroup; /** * The proportional space that should be taken by the associated row group * during excess space distribution. */ public float rowWeight; /** * The proportional space that should be taken by the associated column group * during excess space distribution. */ public float columnWeight; // Constructors private LayoutParams( int width, int height, int left, int top, int right, int bottom, Group rowGroup, Group columnGroup, float rowWeight, float columnWeight) { super(width, height); setMargins(left, top, right, bottom); this.rowGroup = rowGroup; this.columnGroup = columnGroup; this.rowWeight = rowWeight; this.columnWeight = columnWeight; } /** * Constructs a new LayoutParams instance for this rowGroup * and columnGroup. All other fields are initialized with * default values as defined in {@link LayoutParams}. * * @param rowGroup the rowGroup * @param columnGroup the columnGroup */ public LayoutParams(Group rowGroup, Group columnGroup) { this(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, rowGroup, columnGroup, DEFAULT_WEIGHT, DEFAULT_WEIGHT); } /** * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}. */ public LayoutParams() { this(DEFAULT_HORIZONTAL_GROUP, DEFAULT_VERTICAL_GROUP); } // Copying constructors /** * {@inheritDoc} */ public LayoutParams(ViewGroup.LayoutParams params) { super(params); } /** * {@inheritDoc} */ public LayoutParams(MarginLayoutParams params) { super(params); } /** * {@inheritDoc} */ public LayoutParams(LayoutParams that) { super(that); this.columnGroup = that.columnGroup; this.rowGroup = that.rowGroup; this.columnWeight = that.columnWeight; this.rowWeight = that.rowWeight; } // AttributeSet constructors private LayoutParams(Context context, AttributeSet attrs, int defaultGravity) { super(context, attrs); reInitSuper(context, attrs); init(context, attrs, defaultGravity); } /** * {@inheritDoc} * * Values not defined in the attribute set take the default values * defined in {@link LayoutParams}. */ public LayoutParams(Context context, AttributeSet attrs) { this(context, attrs, Gravity.NO_GRAVITY); } // Implementation private static boolean definesVertical(int gravity) { return gravity > 0 && (gravity & Gravity.VERTICAL_GRAVITY_MASK) != 0; } private static boolean definesHorizontal(int gravity) { return gravity > 0 && (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != 0; } private static T getAlignment(T[] alignments, T fill, int min, int max, boolean isUndefined, T defaultValue) { if (isUndefined) { return defaultValue; } return min != max ? fill : alignments[min]; } // Reinitialise the margins using a different default policy than MarginLayoutParams. // Here we use the value UNDEFINED (as distinct from zero) to represent the undefined state // so that a layout manager default can be accessed post set up. We need this as, at the // point of installation, we do not know how many rows/cols there are and therefore // which elements are positioned next to the container's trailing edges. We need to // know this as margins around the container's boundary should have different // defaults to those between peers. // This method could be parametrized and moved into MarginLayout. private void reInitSuper(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, styleable.ViewGroup_MarginLayout); try { int margin = a.getDimensionPixelSize(MARGIN, DEFAULT_MARGIN); this.leftMargin = a.getDimensionPixelSize(LEFT_MARGIN, margin); this.topMargin = a.getDimensionPixelSize(TOP_MARGIN, margin); this.rightMargin = a.getDimensionPixelSize(RIGHT_MARGIN, margin); this.bottomMargin = a.getDimensionPixelSize(BOTTOM_MARGIN, margin); } finally { a.recycle(); } } // Gravity. For conversion from the static the integers defined in the Gravity class, // use Gravity.apply() to apply gravity to a view of zero size and see where it ends up. private static Alignment getHorizontalAlignment(int gravity, int width) { Rect r = new Rect(0, 0, 0, 0); Gravity.apply(gravity, 0, 0, CONTAINER_BOUNDS, r); boolean fill = width == MATCH_PARENT; Alignment defaultAlignment = fill ? FILL : DEFAULT_HORIZONTAL_ALIGNMENT; return getAlignment(HORIZONTAL_ALIGNMENTS, FILL, r.left, r.right, !definesHorizontal(gravity), defaultAlignment); } private static Alignment getVerticalAlignment(int gravity, int height) { Rect r = new Rect(0, 0, 0, 0); Gravity.apply(gravity, 0, 0, CONTAINER_BOUNDS, r); boolean fill = height == MATCH_PARENT; Alignment defaultAlignment = fill ? FILL : DEFAULT_VERTCIAL_ALGIGNMENT; return getAlignment(VERTICAL_ALIGNMENTS, FILL, r.top, r.bottom, !definesVertical(gravity), defaultAlignment); } private void init(Context context, AttributeSet attrs, int defaultGravity) { TypedArray a = context.obtainStyledAttributes(attrs, styleable.GridLayout_Layout); try { int gravity = a.getInteger(GRAVITY, defaultGravity); int column = a.getInteger(COLUMN, DEFAULT_COLUMN); int width = a.getInteger(COLUMN_SPAN, DEFAULT_SPAN_SIZE); Interval colSpan = new Interval(column, column + width); this.columnGroup = new Group(colSpan, getHorizontalAlignment(gravity, width)); this.columnWeight = a.getFloat(COLUMN_WEIGHT, DEFAULT_WEIGHT); int row = a.getInteger(ROW, DEFAULT_ROW); int height = a.getInteger(ROW_SPAN, DEFAULT_SPAN_SIZE); Interval rowSpan = new Interval(row, row + height); this.rowGroup = new Group(rowSpan, getVerticalAlignment(gravity, height)); this.rowWeight = a.getFloat(ROW_WEIGHT, DEFAULT_WEIGHT); } finally { a.recycle(); } } /** * Describes how the child views are positioned. Default is LEFT | BASELINE. * * @param gravity the new gravity. See {@link android.view.Gravity}. * * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity */ public void setGravity(int gravity) { columnGroup = columnGroup.copyWriteAlignment(getHorizontalAlignment(gravity, width)); rowGroup = rowGroup.copyWriteAlignment(getVerticalAlignment(gravity, height)); } @Override protected void setBaseAttributes(TypedArray attributes, int widthAttr, int heightAttr) { this.width = attributes.getLayoutDimension(widthAttr, DEFAULT_WIDTH); this.height = attributes.getLayoutDimension(heightAttr, DEFAULT_HEIGHT); } private void setVerticalGroupSpan(Interval span) { rowGroup = rowGroup.copyWriteSpan(span); } private void setHorizontalGroupSpan(Interval span) { columnGroup = columnGroup.copyWriteSpan(span); } } private static class Arc { public final Interval span; public final Int value; public boolean completesCycle; public Arc(Interval span, Int value) { this.span = span; this.value = value; } @Override public String toString() { return span + " " + (completesCycle ? "+>" : "->") + " " + value; } } // A mutable Integer - used to avoid heap allocation during the layout operation private static class Int { public int value; private Int() { reset(); } private Int(int value) { this.value = value; } private void reset() { value = Integer.MIN_VALUE; } private Int neg() { // this should never be called throw new UnsupportedOperationException(); } } @SuppressWarnings(value = "unchecked") private static class PackedMap { public final int[] index; public final K[] keys; public final V[] values; private PackedMap(K[] keys, V[] values) { this.index = createIndex(keys); this.keys = index(keys, index); this.values = index(values, index); } private K getKey(int i) { return keys[index[i]]; } private V getValue(int i) { return values[index[i]]; } private static int[] createIndex(K[] keys) { int size = keys.length; int[] result = new int[size]; Map keyToIndex = new HashMap(); for (int i = 0; i < size; i++) { K key = keys[i]; Integer index = keyToIndex.get(key); if (index == null) { index = keyToIndex.size(); keyToIndex.put(key, index); } result[i] = index; } return result; } private static int max(int[] a, int valueIfEmpty) { int result = valueIfEmpty; for (int i = 0, length = a.length; i < length; i++) { result = Math.max(result, a[i]); } return result; } private static K[] index(K[] keys, int[] index) { int size = keys.length; Class componentType = keys.getClass().getComponentType(); K[] result = (K[]) Array.newInstance(componentType, max(index, -1) + 1); // this overwrite duplicates, retaining the last equivalent entry for (int i = 0; i < size; i++) { result[index[i]] = keys[i]; } return result; } } private static class Bounds { public int below; public int above; private Bounds(int below, int above) { this.below = below; this.above = above; } private Bounds() { reset(); } private void reset() { below = Integer.MAX_VALUE; above = Integer.MIN_VALUE; } private void include(int below, int above) { this.below = min(this.below, below); this.above = max(this.above, above); } private int size() { return above - below; } @Override public String toString() { return "Bounds{" + "below=" + below + ", above=" + above + '}'; } } /** * An Interval represents a contiguous range of values that lie between * the interval's {@link #min} and {@link #max} values. *

* Intervals are immutable so may be passed as values and used as keys in hash tables. * It is not necessary to have multiple instances of Intervals which have the same * {@link #min} and {@link #max} values. *

* Intervals are often written as [min, max] and represent the set of values * x such that min <= x < max. */ public static class Interval { /** * The minimum value. */ public final int min; /** * The maximum value. */ public final int max; /** * Construct a new Interval, interval, where: *

    *
  • interval.min = min
  • *
  • interval.max = max
  • *
* * @param min the minimum value. * @param max the maximum value. */ public Interval(int min, int max) { this.min = min; this.max = max; } private int size() { return max - min; } private Interval inverse() { return new Interval(max, min); } /** * Returns true if the {@link #getClass class}, {@link #min} and {@link #max} properties * of this Interval and the supplied parameter are pairwise equal; false otherwise. * * @param that the object to compare this interval with. * * @return {@code true} if the specified object is equal to this * {@code Interval}; {@code false} otherwise. */ @Override public boolean equals(Object that) { if (this == that) { return true; } if (that == null || getClass() != that.getClass()) { return false; } Interval interval = (Interval) that; if (max != interval.max) { return false; } if (min != interval.min) { return false; } return true; } @Override public int hashCode() { int result = min; result = 31 * result + max; return result; } @Override public String toString() { return "[" + min + ", " + max + "]"; } } /** * A group specifies either the horizontal or vertical characteristics of a group of * cells. *

* Groups are immutable and so may be shared between views with the same * span and alignment. */ public static class Group { /** * The {@link Interval#min min} and {@link Interval#max max} values of * a span specify the grid indices of the leading and trailing edges of * the cell group. *

* See {@link GridLayout} for a description of the conventions used by GridLayout * for grid indices. */ public final Interval span; /** * Specifies how cells should be aligned in this group. * For row groups, this specifies the vertical alignment. * For column groups, this specifies the horizontal alignment. */ public final Alignment alignment; /** * Construct a new Group, group, where: *

    *
  • group.span = span
  • *
  • group.alignment = alignment
  • *
* * @param span the span. * @param alignment the alignment. */ public Group(Interval span, Alignment alignment) { this.span = span; this.alignment = alignment; } /** * Construct a new Group, group, where: *
    *
  • group.span = [min, max]
  • *
  • group.alignment = alignment
  • *
* * @param min the minimum. * @param max the maximum. * @param alignment the alignment. */ public Group(int min, int max, Alignment alignment) { this(new Interval(min, max), alignment); } /** * Construct a new Group, group, where: *
    *
  • group.span = [min, min + 1]
  • *
  • group.alignment = alignment
  • *
* * @param min the minimum. * @param alignment the alignment. */ public Group(int min, Alignment alignment) { this(min, min + 1, alignment); } private Group copyWriteSpan(Interval span) { return new Group(span, alignment); } private Group copyWriteAlignment(Alignment alignment) { return new Group(span, alignment); } /** * Returns true if the {@link #getClass class}, {@link #alignment} and {@link #span} * properties of this Group and the supplied parameter are pairwise equal; false otherwise. * * @param that the object to compare this group with. * * @return {@code true} if the specified object is equal to this * {@code Group}; {@code false} otherwise. */ @Override public boolean equals(Object that) { if (this == that) { return true; } if (that == null || getClass() != that.getClass()) { return false; } Group group = (Group) that; if (!alignment.equals(group.alignment)) { return false; } if (!span.equals(group.span)) { return false; } return true; } @Override public int hashCode() { int result = span.hashCode(); result = 31 * result + alignment.hashCode(); return result; } } // Alignments /** * Alignments specify where a view should be placed within a cell group and * what size it should be. *

* The {@link LayoutParams} class contains a {@link LayoutParams#rowGroup rowGroup} * and a {@link LayoutParams#columnGroup columnGroup} each of which contains an * {@link Group#alignment alignment}. Overall placement of the view in the cell * group is specified by the two alignments which act along each axis independently. *

* An Alignment implementation must define the {@link #getAlignmentValue(View, int)} * to return the appropriate value for the type of alignment being defined. * The enclosing algorithms position the children * so that the values returned from the alignment * are the same for all of the views in a group. *

* The GridLayout class defines the most common alignments used in general layout: * {@link #TOP}, {@link #LEFT}, {@link #BOTTOM}, {@link #RIGHT}, {@link #CENTER}, {@link * #BASELINE} and {@link #FILL}. */ public static interface Alignment { /** * Returns an alignment value. In the case of vertical alignments the value * returned should indicate the distance from the top of the view to the * alignment location. * For horizontal alignments measurement is made from the left edge of the component. * * @param view the view to which this alignment should be applied. * @param viewSize the measured size of the view. * @return the alignment value. */ public int getAlignmentValue(View view, int viewSize); /** * Returns the size of the view specified by this alignment. * In the case of vertical alignments this method should return a height; for * horizontal alignments this method should return the width. * * @param view the view to which this alignment should be applied. * @param viewSize the measured size of the view. * @param cellSize the size of the cell into which this view will be placed. * @return the aligned size. */ public int getSizeInCell(View view, int viewSize, int cellSize); } private static abstract class AbstractAlignment implements Alignment { public int getSizeInCell(View view, int viewSize, int cellSize) { return viewSize; } } private static final Alignment LEADING = new AbstractAlignment() { public int getAlignmentValue(View view, int viewSize) { return 0; } }; private static final Alignment TRAILING = new AbstractAlignment() { public int getAlignmentValue(View view, int viewSize) { return viewSize; } }; /** * Indicates that a view should be aligned with the top * edges of the other views in its cell group. */ public static final Alignment TOP = LEADING; /** * Indicates that a view should be aligned with the bottom * edges of the other views in its cell group. */ public static final Alignment BOTTOM = TRAILING; /** * Indicates that a view should be aligned with the right * edges of the other views in its cell group. */ public static final Alignment RIGHT = TRAILING; /** * Indicates that a view should be aligned with the left * edges of the other views in its cell group. */ public static final Alignment LEFT = LEADING; /** * Indicates that a view should be centered with the other views in its cell group. * This constant may be used in both {@link LayoutParams#rowGroup rowGroups} and {@link * LayoutParams#columnGroup columnGroups}. */ public static final Alignment CENTER = new AbstractAlignment() { public int getAlignmentValue(View view, int viewSize) { return viewSize >> 1; } }; /** * Indicates that a view should be aligned with the baselines * of the other views in its cell group. * This constant may only be used as an alignment in {@link LayoutParams#rowGroup rowGroups}. * * @see View#getBaseline() */ public static final Alignment BASELINE = new AbstractAlignment() { public int getAlignmentValue(View view, int viewSize) { if (view == null) { return UNDEFINED; } // todo do we need to call measure first? int baseline = view.getBaseline(); return baseline == -1 ? UNDEFINED : baseline; } }; /** * Indicates that a view should expanded to fit the boundaries of its cell group. * This constant may be used in both {@link LayoutParams#rowGroup rowGroups} and * {@link LayoutParams#columnGroup columnGroups}. */ public static final Alignment FILL = new Alignment() { public int getAlignmentValue(View view, int viewSize) { return UNDEFINED; } public int getSizeInCell(View view, int viewSize, int cellSize) { return cellSize; } }; }