src.android.widget.GridLayout Maven / Gradle / Ivy
Show all versions of android-all Show documentation
/*
* 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 static android.view.Gravity.AXIS_PULL_AFTER;
import static android.view.Gravity.AXIS_PULL_BEFORE;
import static android.view.Gravity.AXIS_SPECIFIED;
import static android.view.Gravity.AXIS_X_SHIFT;
import static android.view.Gravity.AXIS_Y_SHIFT;
import static android.view.Gravity.HORIZONTAL_GRAVITY_MASK;
import static android.view.Gravity.RELATIVE_LAYOUT_DIRECTION;
import static android.view.Gravity.VERTICAL_GRAVITY_MASK;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Paint;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.util.LogPrinter;
import android.util.Pair;
import android.util.Printer;
import android.view.Gravity;
import android.view.RemotableViewMethod;
import android.view.View;
import android.view.ViewGroup;
import android.view.inspector.InspectableProperty;
import android.widget.RemoteViews.RemoteView;
import com.android.internal.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 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 with {@code N} columns
* has {@code N + 1} grid indices that run from {@code 0}
* through {@code N} inclusive. Regardless of how GridLayout is
* configured, grid index {@code 0} is fixed to the leading edge of the
* container and grid index {@code N} is fixed to its trailing edge
* (after padding is taken into account).
*
*
Row and Column Specs
*
* Children occupy one or more contiguous cells, as defined
* by their {@link GridLayout.LayoutParams#rowSpec rowSpec} and
* {@link GridLayout.LayoutParams#columnSpec columnSpec} layout parameters.
* Each spec defines 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 a child does not specify 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 prevailing UI style guide for the platform.
* Each of the margins so defined may be independently overridden by an assignment
* to the appropriate layout parameter.
* Default values will generally produce a reasonable spacing between components
* but values may change between different releases of the platform.
*
* Excess Space Distribution
*
* As of API 21, GridLayout's distribution of excess space accomodates the principle of weight.
* In the event that no weights are specified, the previous conventions are respected and
* columns and rows are taken as flexible if their views specify some form of alignment
* within their groups.
*
* The flexibility of a view is therefore influenced by its alignment which is,
* in turn, typically defined by setting the
* {@link LayoutParams#setGravity(int) gravity} property of the child's layout parameters.
* If either a weight or alignment were defined along a given axis then the component
* is taken as flexible in that direction. If no weight or alignment was set,
* the component is instead assumed to be inflexible.
*
* Multiple components in the same row or column group are
* considered to act in parallel. Such a
* group is flexible only if all of the components
* within it are flexible. Row and column groups that sit either side of a common boundary
* are instead considered to act in series. The composite group made of these two
* elements is flexible if one of its elements is flexible.
*
* To make a column stretch, make sure all of the components inside it define a
* weight or a gravity. To prevent a column from stretching, ensure that one of the components
* in the column does not define a weight or a gravity.
*
* When the principle of flexibility does not provide complete disambiguation,
* GridLayout's algorithms favour rows and columns that are closer to its right
* and bottom edges. To be more precise, GridLayout treats each of its layout
* parameters as a constraint in the a set of variables that define the grid-lines along a
* given axis. During layout, GridLayout solves the constraints so as to return the unique
* solution to those constraints for which all variables are less-than-or-equal-to
* the corresponding value in any other valid solution.
*
*
Interpretation of GONE
*
* For layout purposes, GridLayout treats views whose visibility status is
* {@link View#GONE GONE}, as having zero width and height. This is subtly different from
* the policy of ignoring views that are marked as GONE outright. If, for example, a gone-marked
* view was alone in a column, that column would itself collapse to zero width if and only if
* no gravity was defined on the view. If gravity was defined, then the gone-marked
* view has no effect on the layout and the container should be laid out as if the view
* had never been added to it. GONE views are taken to have zero weight during excess space
* distribution.
*
* These statements apply equally to rows as well as columns, and to groups of rows or columns.
*
*
* 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
*/
@RemoteView
public class GridLayout extends ViewGroup {
// Public constants
/** @hide */
@IntDef(prefix = { "HORIZONTAL", "VERTICAL" }, value = {
HORIZONTAL,
VERTICAL
})
@Retention(RetentionPolicy.SOURCE)
public @interface Orientation {}
/**
* The horizontal orientation.
*/
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
/**
* The vertical orientation.
*/
public static final int VERTICAL = LinearLayout.VERTICAL;
/**
* The constant used to indicate that a value is undefined.
* Fields can use this value to indicate that their values
* have not yet been set. Similarly, methods can return this value
* to indicate that there is no suitable value that the implementation
* can return.
* The value used for the constant (currently {@link Integer#MIN_VALUE}) is
* intended to avoid confusion between valid values whose sign may not be known.
*/
public static final int UNDEFINED = Integer.MIN_VALUE;
/** @hide */
@IntDef(prefix = { "ALIGN_" }, value = {
ALIGN_BOUNDS,
ALIGN_MARGINS
})
@Retention(RetentionPolicy.SOURCE)
public @interface AlignmentMode {}
/**
* This constant is an {@link #setAlignmentMode(int) alignmentMode}.
* When the {@code alignmentMode} is set to {@link #ALIGN_BOUNDS}, alignment
* is made between the edges of each component's raw
* view boundary: i.e. the area delimited by the component's:
* {@link android.view.View#getTop() top},
* {@link android.view.View#getLeft() left},
* {@link android.view.View#getBottom() bottom} and
* {@link android.view.View#getRight() right} properties.
*
* For example, when {@code GridLayout} is in {@link #ALIGN_BOUNDS} mode,
* children that belong to a row group that uses {@link #TOP} alignment will
* all return the same value when their {@link android.view.View#getTop()}
* method is called.
*
* @see #setAlignmentMode(int)
*/
public static final int ALIGN_BOUNDS = 0;
/**
* This constant is an {@link #setAlignmentMode(int) alignmentMode}.
* When the {@code alignmentMode} is set to {@link #ALIGN_MARGINS},
* the bounds of each view are extended outwards, according
* to their margins, before the edges of the resulting rectangle are aligned.
*
* For example, when {@code GridLayout} is in {@link #ALIGN_MARGINS} mode,
* the quantity {@code top - layoutParams.topMargin} is the same for all children that
* belong to a row group that uses {@link #TOP} alignment.
*
* @see #setAlignmentMode(int)
*/
public static final int ALIGN_MARGINS = 1;
// Misc constants
static final int MAX_SIZE = 100000;
static final int DEFAULT_CONTAINER_MARGIN = 0;
static final int UNINITIALIZED_HASH = 0;
static final Printer LOG_PRINTER = new LogPrinter(Log.DEBUG, GridLayout.class.getName());
static final Printer NO_PRINTER = new Printer() {
@Override
public void println(String x) {
}
};
// 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 = true;
private static final int DEFAULT_ALIGNMENT_MODE = ALIGN_MARGINS;
// TypedArray indices
private static final int ORIENTATION = R.styleable.GridLayout_orientation;
private static final int ROW_COUNT = R.styleable.GridLayout_rowCount;
private static final int COLUMN_COUNT = R.styleable.GridLayout_columnCount;
private static final int USE_DEFAULT_MARGINS = R.styleable.GridLayout_useDefaultMargins;
private static final int ALIGNMENT_MODE = R.styleable.GridLayout_alignmentMode;
private static final int ROW_ORDER_PRESERVED = R.styleable.GridLayout_rowOrderPreserved;
private static final int COLUMN_ORDER_PRESERVED = R.styleable.GridLayout_columnOrderPreserved;
// Instance variables
final Axis mHorizontalAxis = new Axis(true);
final Axis mVerticalAxis = new Axis(false);
int mOrientation = DEFAULT_ORIENTATION;
boolean mUseDefaultMargins = DEFAULT_USE_DEFAULT_MARGINS;
int mAlignmentMode = DEFAULT_ALIGNMENT_MODE;
int mDefaultGap;
int mLastLayoutParamsHashCode = UNINITIALIZED_HASH;
Printer mPrinter = LOG_PRINTER;
// Constructors
public GridLayout(Context context) {
this(context, null);
}
public GridLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GridLayout(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public GridLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mDefaultGap = context.getResources().getDimensionPixelOffset(R.dimen.default_gap);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.GridLayout, defStyleAttr, defStyleRes);
saveAttributeDataForStyleable(context, R.styleable.GridLayout,
attrs, a, defStyleAttr, defStyleRes);
try {
setRowCount(a.getInt(ROW_COUNT, DEFAULT_COUNT));
setColumnCount(a.getInt(COLUMN_COUNT, DEFAULT_COUNT));
setOrientation(a.getInt(ORIENTATION, DEFAULT_ORIENTATION));
setUseDefaultMargins(a.getBoolean(USE_DEFAULT_MARGINS, DEFAULT_USE_DEFAULT_MARGINS));
setAlignmentMode(a.getInt(ALIGNMENT_MODE, DEFAULT_ALIGNMENT_MODE));
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}
*
* @see #setOrientation(int)
*
* @attr ref android.R.styleable#GridLayout_orientation
*/
@Orientation
@InspectableProperty(enumMapping = {
@InspectableProperty.EnumEntry(value = HORIZONTAL, name = "horizontal"),
@InspectableProperty.EnumEntry(value = VERTICAL, name = "vertical")
})
public int getOrientation() {
return mOrientation;
}
/**
*
* GridLayout uses the orientation property for two purposes:
*
* -
* To control the 'direction' in which default row/column indices are generated
* when they are not specified in a component's layout parameters.
*
* -
* To control which axis should be processed first during the layout operation:
* when orientation is {@link #HORIZONTAL} the horizontal axis is laid out first.
*
*
*
* The order in which axes are laid out is important if, for example, the height of
* one of GridLayout's children is dependent on its width - and its width is, in turn,
* dependent on the widths of other components.
*
* If your layout contains a {@link TextView} (or derivative:
* {@code Button}, {@code EditText}, {@code CheckBox}, etc.) which is
* in multi-line mode (the default) it is normally best to leave GridLayout's
* orientation as {@code HORIZONTAL} - because {@code TextView} is capable of
* deriving its height for a given width, but not the other way around.
*
* Other than the effects above, orientation does not affect the actual layout operation of
* GridLayout, so it's fine to leave GridLayout in {@code HORIZONTAL} mode even if
* the height of the intended layout greatly exceeds its width.
*
* The default value of this property is {@link #HORIZONTAL}.
*
* @param orientation either {@link #HORIZONTAL} or {@link #VERTICAL}
*
* @see #getOrientation()
*
* @attr ref android.R.styleable#GridLayout_orientation
*/
public void setOrientation(@Orientation int orientation) {
if (this.mOrientation != orientation) {
this.mOrientation = orientation;
invalidateStructure();
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#rowSpec}.
*
* @return the current number of rows
*
* @see #setRowCount(int)
* @see LayoutParams#rowSpec
*
* @attr ref android.R.styleable#GridLayout_rowCount
*/
@InspectableProperty
public int getRowCount() {
return mVerticalAxis.getCount();
}
/**
* 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#rowSpec
*
* @attr ref android.R.styleable#GridLayout_rowCount
*/
@RemotableViewMethod
public void setRowCount(int rowCount) {
mVerticalAxis.setCount(rowCount);
invalidateStructure();
requestLayout();
}
/**
* 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#columnSpec}.
*
* @return the current number of columns
*
* @see #setColumnCount(int)
* @see LayoutParams#columnSpec
*
* @attr ref android.R.styleable#GridLayout_columnCount
*/
@InspectableProperty
public int getColumnCount() {
return mHorizontalAxis.getCount();
}
/**
* 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#columnSpec
*
* @attr ref android.R.styleable#GridLayout_columnCount
*/
@RemotableViewMethod
public void setColumnCount(int columnCount) {
mHorizontalAxis.setCount(columnCount);
invalidateStructure();
requestLayout();
}
/**
* Returns whether or not this GridLayout will allocate default margins when no
* corresponding layout parameters are defined.
*
* @return {@code true} if default margins should be allocated
*
* @see #setUseDefaultMargins(boolean)
*
* @attr ref android.R.styleable#GridLayout_useDefaultMargins
*/
@InspectableProperty
public boolean getUseDefaultMargins() {
return mUseDefaultMargins;
}
/**
* When {@code 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 {@code false}, the default value of all margins is zero.
*
* When setting to {@code true}, consider setting the value of the
* {@link #setAlignmentMode(int) alignmentMode}
* property to {@link #ALIGN_BOUNDS}.
*
* The default value of this property is {@code false}.
*
* @param useDefaultMargins use {@code true} to make GridLayout allocate default margins
*
* @see #getUseDefaultMargins()
* @see #setAlignmentMode(int)
*
* @see ViewGroup.MarginLayoutParams#leftMargin
* @see ViewGroup.MarginLayoutParams#topMargin
* @see ViewGroup.MarginLayoutParams#rightMargin
* @see ViewGroup.MarginLayoutParams#bottomMargin
*
* @attr ref android.R.styleable#GridLayout_useDefaultMargins
*/
public void setUseDefaultMargins(boolean useDefaultMargins) {
this.mUseDefaultMargins = useDefaultMargins;
requestLayout();
}
/**
* Returns the alignment mode.
*
* @return the alignment mode; either {@link #ALIGN_BOUNDS} or {@link #ALIGN_MARGINS}
*
* @see #ALIGN_BOUNDS
* @see #ALIGN_MARGINS
*
* @see #setAlignmentMode(int)
*
* @attr ref android.R.styleable#GridLayout_alignmentMode
*/
@AlignmentMode
@InspectableProperty(enumMapping = {
@InspectableProperty.EnumEntry(value = ALIGN_BOUNDS, name = "alignBounds"),
@InspectableProperty.EnumEntry(value = ALIGN_MARGINS, name = "alignMargins"),
})
public int getAlignmentMode() {
return mAlignmentMode;
}
/**
* Sets the alignment mode to be used for all of the alignments between the
* children of this container.
*
* The default value of this property is {@link #ALIGN_MARGINS}.
*
* @param alignmentMode either {@link #ALIGN_BOUNDS} or {@link #ALIGN_MARGINS}
*
* @see #ALIGN_BOUNDS
* @see #ALIGN_MARGINS
*
* @see #getAlignmentMode()
*
* @attr ref android.R.styleable#GridLayout_alignmentMode
*/
@RemotableViewMethod
public void setAlignmentMode(@AlignmentMode int alignmentMode) {
this.mAlignmentMode = alignmentMode;
requestLayout();
}
/**
* Returns whether or not row boundaries are ordered by their grid indices.
*
* @return {@code true} if row boundaries must appear in the order of their indices,
* {@code false} otherwise
*
* @see #setRowOrderPreserved(boolean)
*
* @attr ref android.R.styleable#GridLayout_rowOrderPreserved
*/
@InspectableProperty
public boolean isRowOrderPreserved() {
return mVerticalAxis.isOrderPreserved();
}
/**
* When this property is {@code true}, GridLayout is forced to place the row boundaries
* so that their associated grid indices are in ascending order in the view.
*
* When this property is {@code false} GridLayout is at liberty to place the vertical row
* boundaries in whatever order best fits the given constraints.
*
* The default value of this property is {@code true}.
* @param rowOrderPreserved {@code 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);
invalidateStructure();
requestLayout();
}
/**
* Returns whether or not column boundaries are ordered by their grid indices.
*
* @return {@code true} if column boundaries must appear in the order of their indices,
* {@code false} otherwise
*
* @see #setColumnOrderPreserved(boolean)
*
* @attr ref android.R.styleable#GridLayout_columnOrderPreserved
*/
@InspectableProperty
public boolean isColumnOrderPreserved() {
return mHorizontalAxis.isOrderPreserved();
}
/**
* When this property is {@code true}, GridLayout is forced to place the column boundaries
* so that their associated grid indices are in ascending order in the view.
*
* When this property is {@code false} GridLayout is at liberty to place the horizontal column
* boundaries in whatever order best fits the given constraints.
*
* The default value of this property is {@code true}.
*
* @param columnOrderPreserved use {@code 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);
invalidateStructure();
requestLayout();
}
/**
* Return the printer that will log diagnostics from this layout.
*
* @see #setPrinter(android.util.Printer)
*
* @return the printer associated with this view
*
* @hide
*/
public Printer getPrinter() {
return mPrinter;
}
/**
* Set the printer that will log diagnostics from this layout.
* The default value is created by {@link android.util.LogPrinter}.
*
* @param printer the printer associated with this layout
*
* @see #getPrinter()
*
* @hide
*/
public void setPrinter(Printer printer) {
this.mPrinter = (printer == null) ? NO_PRINTER : printer;
}
// Static utility methods
static int max2(int[] a, int valueIfEmpty) {
int result = valueIfEmpty;
for (int i = 0, N = a.length; i < N; i++) {
result = Math.max(result, a[i]);
}
return result;
}
@SuppressWarnings("unchecked")
static T[] append(T[] a, T[] b) {
T[] result = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length + b.length);
System.arraycopy(a, 0, result, 0, a.length);
System.arraycopy(b, 0, result, a.length, b.length);
return result;
}
static Alignment getAlignment(int gravity, boolean horizontal) {
int mask = horizontal ? HORIZONTAL_GRAVITY_MASK : VERTICAL_GRAVITY_MASK;
int shift = horizontal ? AXIS_X_SHIFT : AXIS_Y_SHIFT;
int flags = (gravity & mask) >> shift;
switch (flags) {
case (AXIS_SPECIFIED | AXIS_PULL_BEFORE):
return horizontal ? LEFT : TOP;
case (AXIS_SPECIFIED | AXIS_PULL_AFTER):
return horizontal ? RIGHT : BOTTOM;
case (AXIS_SPECIFIED | AXIS_PULL_BEFORE | AXIS_PULL_AFTER):
return FILL;
case AXIS_SPECIFIED:
return CENTER;
case (AXIS_SPECIFIED | AXIS_PULL_BEFORE | RELATIVE_LAYOUT_DIRECTION):
return START;
case (AXIS_SPECIFIED | AXIS_PULL_AFTER | RELATIVE_LAYOUT_DIRECTION):
return END;
default:
return UNDEFINED_ALIGNMENT;
}
}
/** @noinspection UnusedParameters*/
private int getDefaultMargin(View c, boolean horizontal, boolean leading) {
if (c.getClass() == Space.class) {
return 0;
}
return mDefaultGap / 2;
}
private int getDefaultMargin(View c, boolean isAtEdge, boolean horizontal, boolean leading) {
return /*isAtEdge ? DEFAULT_CONTAINER_MARGIN :*/ getDefaultMargin(c, horizontal, leading);
}
private int getDefaultMargin(View c, LayoutParams p, boolean horizontal, boolean leading) {
if (!mUseDefaultMargins) {
return 0;
}
Spec spec = horizontal ? p.columnSpec : p.rowSpec;
Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
Interval span = spec.span;
boolean leading1 = (horizontal && isLayoutRtl()) ? !leading : leading;
boolean isAtEdge = leading1 ? (span.min == 0) : (span.max == axis.getCount());
return getDefaultMargin(c, isAtEdge, horizontal, leading);
}
int getMargin1(View view, boolean horizontal, boolean leading) {
LayoutParams lp = getLayoutParams(view);
int margin = horizontal ?
(leading ? lp.leftMargin : lp.rightMargin) :
(leading ? lp.topMargin : lp.bottomMargin);
return margin == UNDEFINED ? getDefaultMargin(view, lp, horizontal, leading) : margin;
}
private int getMargin(View view, boolean horizontal, boolean leading) {
if (mAlignmentMode == ALIGN_MARGINS) {
return getMargin1(view, horizontal, leading);
} else {
Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
int[] margins = leading ? axis.getLeadingMargins() : axis.getTrailingMargins();
LayoutParams lp = getLayoutParams(view);
Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
int index = leading ? spec.span.min : spec.span.max;
return margins[index];
}
}
private int getTotalMargin(View child, boolean horizontal) {
return getMargin(child, horizontal, true) + getMargin(child, horizontal, false);
}
private static boolean fits(int[] a, int value, int start, int end) {
if (end > a.length) {
return false;
}
for (int i = start; i < end; i++) {
if (a[i] > value) {
return false;
}
}
return true;
}
private static void procrusteanFill(int[] a, int start, int end, int value) {
int length = a.length;
Arrays.fill(a, Math.min(start, length), Math.min(end, length), value);
}
private static void setCellGroup(LayoutParams lp, int row, int rowSpan, int col, int colSpan) {
lp.setRowSpecSpan(new Interval(row, row + rowSpan));
lp.setColumnSpecSpan(new Interval(col, col + colSpan));
}
// Logic to avert infinite loops by ensuring that the cells can be placed somewhere.
private static int clip(Interval minorRange, boolean minorWasDefined, int count) {
int size = minorRange.size();
if (count == 0) {
return size;
}
int min = minorWasDefined ? min(minorRange.min, count) : 0;
return min(size, count - min);
}
// install default indices for cells that don't define them
private void validateLayoutParams() {
final boolean horizontal = (mOrientation == HORIZONTAL);
final Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
final int count = (axis.definedCount != UNDEFINED) ? axis.definedCount : 0;
int major = 0;
int minor = 0;
int[] maxSizes = new int[count];
for (int i = 0, N = getChildCount(); i < N; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
final Spec majorSpec = horizontal ? lp.rowSpec : lp.columnSpec;
final Interval majorRange = majorSpec.span;
final boolean majorWasDefined = majorSpec.startDefined;
final int majorSpan = majorRange.size();
if (majorWasDefined) {
major = majorRange.min;
}
final Spec minorSpec = horizontal ? lp.columnSpec : lp.rowSpec;
final Interval minorRange = minorSpec.span;
final boolean minorWasDefined = minorSpec.startDefined;
final int minorSpan = clip(minorRange, minorWasDefined, count);
if (minorWasDefined) {
minor = minorRange.min;
}
if (count != 0) {
// Find suitable row/col values when at least one is undefined.
if (!majorWasDefined || !minorWasDefined) {
while (!fits(maxSizes, major, minor, minor + minorSpan)) {
if (minorWasDefined) {
major++;
} else {
if (minor + minorSpan <= count) {
minor++;
} else {
minor = 0;
major++;
}
}
}
}
procrusteanFill(maxSizes, minor, minor + minorSpan, major + majorSpan);
}
if (horizontal) {
setCellGroup(lp, major, majorSpan, minor, minorSpan);
} else {
setCellGroup(lp, minor, minorSpan, major, majorSpan);
}
minor = minor + minorSpan;
}
}
private void invalidateStructure() {
mLastLayoutParamsHashCode = UNINITIALIZED_HASH;
mHorizontalAxis.invalidateStructure();
mVerticalAxis.invalidateStructure();
// This can end up being done twice. Better twice than not at all.
invalidateValues();
}
private void invalidateValues() {
// Need null check because requestLayout() is called in View's initializer,
// before we are set up.
if (mHorizontalAxis != null && mVerticalAxis != null) {
mHorizontalAxis.invalidateValues();
mVerticalAxis.invalidateValues();
}
}
/** @hide */
@Override
protected void onSetLayoutParams(View child, ViewGroup.LayoutParams layoutParams) {
super.onSetLayoutParams(child, layoutParams);
if (!checkLayoutParams(layoutParams)) {
handleInvalidParams("supplied LayoutParams are of the wrong type");
}
invalidateStructure();
}
final LayoutParams getLayoutParams(View c) {
return (LayoutParams) c.getLayoutParams();
}
private static void handleInvalidParams(String msg) {
throw new IllegalArgumentException(msg + ". ");
}
private void checkLayoutParams(LayoutParams lp, boolean horizontal) {
String groupName = horizontal ? "column" : "row";
Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
Interval span = spec.span;
if (span.min != UNDEFINED && span.min < 0) {
handleInvalidParams(groupName + " indices must be positive");
}
Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
int count = axis.definedCount;
if (count != UNDEFINED) {
if (span.max > count) {
handleInvalidParams(groupName +
" indices (start + span) mustn't exceed the " + groupName + " count");
}
if (span.size() > count) {
handleInvalidParams(groupName + " span mustn't exceed the " + groupName + " count");
}
}
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
if (!(p instanceof LayoutParams)) {
return false;
}
LayoutParams lp = (LayoutParams) p;
checkLayoutParams(lp, true);
checkLayoutParams(lp, false);
return true;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams();
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
if (sPreserveMarginParamsInLayoutParamConversion) {
if (lp instanceof LayoutParams) {
return new LayoutParams((LayoutParams) lp);
} else if (lp instanceof MarginLayoutParams) {
return new LayoutParams((MarginLayoutParams) lp);
}
}
return new LayoutParams(lp);
}
// Draw grid
private void drawLine(Canvas graphics, int x1, int y1, int x2, int y2, Paint paint) {
if (isLayoutRtl()) {
int width = getWidth();
graphics.drawLine(width - x1, y1, width - x2, y2, paint);
} else {
graphics.drawLine(x1, y1, x2, y2, paint);
}
}
/**
* @hide
*/
@Override
protected void onDebugDrawMargins(Canvas canvas, Paint paint) {
// Apply defaults, so as to remove UNDEFINED values
LayoutParams lp = new LayoutParams();
for (int i = 0; i < getChildCount(); i++) {
View c = getChildAt(i);
lp.setMargins(
getMargin1(c, true, true),
getMargin1(c, false, true),
getMargin1(c, true, false),
getMargin1(c, false, false));
lp.onDebugDraw(c, canvas, paint);
}
}
/**
* @hide
*/
@Override
protected void onDebugDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.argb(50, 255, 255, 255));
Insets insets = getOpticalInsets();
int top = getPaddingTop() + insets.top;
int left = getPaddingLeft() + insets.left;
int right = getWidth() - getPaddingRight() - insets.right;
int bottom = getHeight() - getPaddingBottom() - insets.bottom;
int[] xs = mHorizontalAxis.locations;
if (xs != null) {
for (int i = 0, length = xs.length; i < length; i++) {
int x = left + xs[i];
drawLine(canvas, x, top, x, bottom, paint);
}
}
int[] ys = mVerticalAxis.locations;
if (ys != null) {
for (int i = 0, length = ys.length; i < length; i++) {
int y = top + ys[i];
drawLine(canvas, left, y, right, y, paint);
}
}
super.onDebugDraw(canvas);
}
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
invalidateStructure();
}
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
invalidateStructure();
}
/**
* We need to call invalidateStructure() when a child's GONE flag changes state.
* This implementation is a catch-all, invalidating on any change in the visibility flags.
*
* @hide
*/
@Override
protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
super.onChildVisibilityChanged(child, oldVisibility, newVisibility);
if (oldVisibility == GONE || newVisibility == GONE) {
invalidateStructure();
}
}
private int computeLayoutParamsHashCode() {
int result = 1;
for (int i = 0, N = getChildCount(); i < N; i++) {
View c = getChildAt(i);
if (c.getVisibility() == View.GONE) continue;
LayoutParams lp = (LayoutParams) c.getLayoutParams();
result = 31 * result + lp.hashCode();
}
return result;
}
private void consistencyCheck() {
if (mLastLayoutParamsHashCode == UNINITIALIZED_HASH) {
validateLayoutParams();
mLastLayoutParamsHashCode = computeLayoutParamsHashCode();
} else if (mLastLayoutParamsHashCode != computeLayoutParamsHashCode()) {
mPrinter.println("The fields of some layout parameters were modified in between "
+ "layout operations. Check the javadoc for GridLayout.LayoutParams#rowSpec.");
invalidateStructure();
consistencyCheck();
}
}
// Measurement
// Note: padding has already been removed from the supplied specs
private void measureChildWithMargins2(View child, int parentWidthSpec, int parentHeightSpec,
int childWidth, int childHeight) {
int childWidthSpec = getChildMeasureSpec(parentWidthSpec,
getTotalMargin(child, true), childWidth);
int childHeightSpec = getChildMeasureSpec(parentHeightSpec,
getTotalMargin(child, false), childHeight);
child.measure(childWidthSpec, childHeightSpec);
}
// Note: padding has already been removed from the supplied specs
private void measureChildrenWithMargins(int widthSpec, int heightSpec, boolean firstPass) {
for (int i = 0, N = getChildCount(); i < N; i++) {
View c = getChildAt(i);
if (c.getVisibility() == View.GONE) continue;
LayoutParams lp = getLayoutParams(c);
if (firstPass) {
measureChildWithMargins2(c, widthSpec, heightSpec, lp.width, lp.height);
} else {
boolean horizontal = (mOrientation == HORIZONTAL);
Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
if (spec.getAbsoluteAlignment(horizontal) == FILL) {
Interval span = spec.span;
Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
int[] locations = axis.getLocations();
int cellSize = locations[span.max] - locations[span.min];
int viewSize = cellSize - getTotalMargin(c, horizontal);
if (horizontal) {
measureChildWithMargins2(c, widthSpec, heightSpec, viewSize, lp.height);
} else {
measureChildWithMargins2(c, widthSpec, heightSpec, lp.width, viewSize);
}
}
}
}
}
static int adjust(int measureSpec, int delta) {
return makeMeasureSpec(
MeasureSpec.getSize(measureSpec + delta), MeasureSpec.getMode(measureSpec));
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
consistencyCheck();
/** If we have been called by {@link View#measure(int, int)}, one of width or height
* is likely to have changed. We must invalidate if so. */
invalidateValues();
int hPadding = getPaddingLeft() + getPaddingRight();
int vPadding = getPaddingTop() + getPaddingBottom();
int widthSpecSansPadding = adjust( widthSpec, -hPadding);
int heightSpecSansPadding = adjust(heightSpec, -vPadding);
measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, true);
int widthSansPadding;
int heightSansPadding;
// Use the orientation property to decide which axis should be laid out first.
if (mOrientation == HORIZONTAL) {
widthSansPadding = mHorizontalAxis.getMeasure(widthSpecSansPadding);
measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, false);
heightSansPadding = mVerticalAxis.getMeasure(heightSpecSansPadding);
} else {
heightSansPadding = mVerticalAxis.getMeasure(heightSpecSansPadding);
measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, false);
widthSansPadding = mHorizontalAxis.getMeasure(widthSpecSansPadding);
}
int measuredWidth = Math.max(widthSansPadding + hPadding, getSuggestedMinimumWidth());
int measuredHeight = Math.max(heightSansPadding + vPadding, getSuggestedMinimumHeight());
setMeasuredDimension(
resolveSizeAndState(measuredWidth, widthSpec, 0),
resolveSizeAndState(measuredHeight, heightSpec, 0));
}
private int getMeasurement(View c, boolean horizontal) {
return horizontal ? c.getMeasuredWidth() : c.getMeasuredHeight();
}
final int getMeasurementIncludingMargin(View c, boolean horizontal) {
if (c.getVisibility() == View.GONE) {
return 0;
}
return getMeasurement(c, horizontal) + getTotalMargin(c, horizontal);
}
@Override
public void requestLayout() {
super.requestLayout();
invalidateValues();
}
// Layout container
/**
* {@inheritDoc}
*/
/*
The layout operation is implemented by delegating the heavy lifting to the
to the mHorizontalAxis and mVerticalAxis instances of the internal Axis class.
Together they compute the locations of the vertical and horizontal lines of
the grid (respectively!).
This method is then left with the simpler task of applying margins, gravity
and sizing to each child view and then placing it in its cell.
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
consistencyCheck();
int targetWidth = right - left;
int targetHeight = bottom - top;
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
mHorizontalAxis.layout(targetWidth - paddingLeft - paddingRight);
mVerticalAxis.layout(targetHeight - paddingTop - paddingBottom);
int[] hLocations = mHorizontalAxis.getLocations();
int[] vLocations = mVerticalAxis.getLocations();
for (int i = 0, N = getChildCount(); i < N; i++) {
View c = getChildAt(i);
if (c.getVisibility() == View.GONE) continue;
LayoutParams lp = getLayoutParams(c);
Spec columnSpec = lp.columnSpec;
Spec rowSpec = lp.rowSpec;
Interval colSpan = columnSpec.span;
Interval rowSpan = rowSpec.span;
int x1 = hLocations[colSpan.min];
int y1 = vLocations[rowSpan.min];
int x2 = hLocations[colSpan.max];
int y2 = vLocations[rowSpan.max];
int cellWidth = x2 - x1;
int cellHeight = y2 - y1;
int pWidth = getMeasurement(c, true);
int pHeight = getMeasurement(c, false);
Alignment hAlign = columnSpec.getAbsoluteAlignment(true);
Alignment vAlign = rowSpec.getAbsoluteAlignment(false);
Bounds boundsX = mHorizontalAxis.getGroupBounds().getValue(i);
Bounds boundsY = mVerticalAxis.getGroupBounds().getValue(i);
// Gravity offsets: the location of the alignment group relative to its cell group.
int gravityOffsetX = hAlign.getGravityOffset(c, cellWidth - boundsX.size(true));
int gravityOffsetY = vAlign.getGravityOffset(c, cellHeight - boundsY.size(true));
int leftMargin = getMargin(c, true, true);
int topMargin = getMargin(c, false, true);
int rightMargin = getMargin(c, true, false);
int bottomMargin = getMargin(c, false, false);
int sumMarginsX = leftMargin + rightMargin;
int sumMarginsY = topMargin + bottomMargin;
// Alignment offsets: the location of the view relative to its alignment group.
int alignmentOffsetX = boundsX.getOffset(this, c, hAlign, pWidth + sumMarginsX, true);
int alignmentOffsetY = boundsY.getOffset(this, c, vAlign, pHeight + sumMarginsY, false);
int width = hAlign.getSizeInCell(c, pWidth, cellWidth - sumMarginsX);
int height = vAlign.getSizeInCell(c, pHeight, cellHeight - sumMarginsY);
int dx = x1 + gravityOffsetX + alignmentOffsetX;
int cx = !isLayoutRtl() ? paddingLeft + leftMargin + dx :
targetWidth - width - paddingRight - rightMargin - dx;
int cy = paddingTop + y1 + gravityOffsetY + alignmentOffsetY + topMargin;
if (width != c.getMeasuredWidth() || height != c.getMeasuredHeight()) {
c.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
}
c.layout(cx, cy, cx + width, cy + height);
}
}
@Override
public CharSequence getAccessibilityClassName() {
return GridLayout.class.getName();
}
// Inner classes
/*
This internal class houses the algorithm for computing the locations of grid lines;
along either the horizontal or vertical axis. A GridLayout uses two instances of this class -
distinguished by the "horizontal" flag which is true for the horizontal axis and false
for the vertical one.
*/
final class Axis {
private static final int NEW = 0;
private static final int PENDING = 1;
private static final int COMPLETE = 2;
public final boolean horizontal;
public int definedCount = UNDEFINED;
private int maxIndex = UNDEFINED;
PackedMap groupBounds;
public boolean groupBoundsValid = false;
PackedMap forwardLinks;
public boolean forwardLinksValid = false;
PackedMap backwardLinks;
public boolean backwardLinksValid = false;
public int[] leadingMargins;
public boolean leadingMarginsValid = false;
public int[] trailingMargins;
public boolean trailingMarginsValid = false;
public Arc[] arcs;
public boolean arcsValid = false;
public int[] locations;
public boolean locationsValid = false;
public boolean hasWeights;
public boolean hasWeightsValid = false;
public int[] deltas;
boolean orderPreserved = DEFAULT_ORDER_PRESERVED;
private MutableInt parentMin = new MutableInt(0);
private MutableInt parentMax = new MutableInt(-MAX_SIZE);
private Axis(boolean horizontal) {
this.horizontal = horizontal;
}
private int calculateMaxIndex() {
// the number Integer.MIN_VALUE + 1 comes up in undefined cells
int result = -1;
for (int i = 0, N = getChildCount(); i < N; i++) {
View c = getChildAt(i);
LayoutParams params = getLayoutParams(c);
Spec spec = horizontal ? params.columnSpec : params.rowSpec;
Interval span = spec.span;
result = max(result, span.min);
result = max(result, span.max);
result = max(result, span.size());
}
return result == -1 ? UNDEFINED : result;
}
private int getMaxIndex() {
if (maxIndex == UNDEFINED) {
maxIndex = max(0, calculateMaxIndex()); // use zero when there are no children
}
return maxIndex;
}
public int getCount() {
return max(definedCount, getMaxIndex());
}
public void setCount(int count) {
if (count != UNDEFINED && count < getMaxIndex()) {
handleInvalidParams((horizontal ? "column" : "row") +
"Count must be greater than or equal to the maximum of all grid indices " +
"(and spans) defined in the LayoutParams of each child");
}
this.definedCount = count;
}
public boolean isOrderPreserved() {
return orderPreserved;
}
public void setOrderPreserved(boolean orderPreserved) {
this.orderPreserved = orderPreserved;
invalidateStructure();
}
private PackedMap createGroupBounds() {
Assoc assoc = Assoc.of(Spec.class, Bounds.class);
for (int i = 0, N = getChildCount(); i < N; i++) {
View c = getChildAt(i);
// we must include views that are GONE here, see introductory javadoc
LayoutParams lp = getLayoutParams(c);
Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
Bounds bounds = spec.getAbsoluteAlignment(horizontal).getBounds();
assoc.put(spec, bounds);
}
return assoc.pack();
}
private void computeGroupBounds() {
Bounds[] values = groupBounds.values;
for (int i = 0; i < values.length; i++) {
values[i].reset();
}
for (int i = 0, N = getChildCount(); i < N; i++) {
View c = getChildAt(i);
// we must include views that are GONE here, see introductory javadoc
LayoutParams lp = getLayoutParams(c);
Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
int size = getMeasurementIncludingMargin(c, horizontal) +
((spec.weight == 0) ? 0 : getDeltas()[i]);
groupBounds.getValue(i).include(GridLayout.this, c, spec, this, size);
}
}
public 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 createLinks(boolean min) {
Assoc result = Assoc.of(Interval.class, MutableInt.class);
Spec[] keys = getGroupBounds().keys;
for (int i = 0, N = keys.length; i < N; i++) {
Interval span = min ? keys[i].span : keys[i].span.inverse();
result.put(span, new MutableInt());
}
return result.pack();
}
private void computeLinks(PackedMap links, boolean min) {
MutableInt[] spans = links.values;
for (int i = 0; i < spans.length; i++) {
spans[i].reset();
}
// Use getter to trigger a re-evaluation
Bounds[] bounds = getGroupBounds().values;
for (int i = 0; i < bounds.length; i++) {
int size = bounds[i].size(min);
MutableInt valueHolder = links.getValue(i);
// this effectively takes the max() of the minima and the min() of the maxima
valueHolder.value = max(valueHolder.value, min ? size : -size);
}
}
private PackedMap getForwardLinks() {
if (forwardLinks == null) {
forwardLinks = createLinks(true);
}
if (!forwardLinksValid) {
computeLinks(forwardLinks, true);
forwardLinksValid = true;
}
return forwardLinks;
}
private PackedMap getBackwardLinks() {
if (backwardLinks == null) {
backwardLinks = createLinks(false);
}
if (!backwardLinksValid) {
computeLinks(backwardLinks, false);
backwardLinksValid = true;
}
return backwardLinks;
}
private void include(List arcs, Interval key, MutableInt size,
boolean ignoreIfAlreadyPresent) {
/*
Remove self referential links.
These appear:
. as parental constraints when GridLayout has no children
. when components have been marked as GONE
*/
if (key.size() == 0) {
return;
}
// this bit below should really be computed outside here -
// its just to stop default (row/col > 0) constraints obliterating valid entries
if (ignoreIfAlreadyPresent) {
for (Arc arc : arcs) {
Interval span = arc.span;
if (span.equals(key)) {
return;
}
}
}
arcs.add(new Arc(key, size));
}
private void include(List arcs, Interval key, MutableInt size) {
include(arcs, key, size, true);
}
// Group arcs by their first vertex, returning an array of arrays.
// This is linear in the number of arcs.
Arc[][] groupArcsByFirstVertex(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;
}
private Arc[] topologicalSort(final Arc[] arcs) {
return new Object() {
Arc[] result = new Arc[arcs.length];
int cursor = result.length - 1;
Arc[][] arcsByVertex = groupArcsByFirstVertex(arcs);
int[] visited = new int[getCount() + 1];
void walk(int loc) {
switch (visited[loc]) {
case NEW: {
visited[loc] = PENDING;
for (Arc arc : arcsByVertex[loc]) {
walk(arc.span.max);
result[cursor--] = arc;
}
visited[loc] = COMPLETE;
break;
}
case PENDING: {
// le singe est dans l'arbre
assert false;
break;
}
case COMPLETE: {
break;
}
}
}
Arc[] sort() {
for (int loc = 0, N = arcsByVertex.length; loc < N; loc++) {
walk(loc);
}
assert cursor == -1;
return result;
}
}.sort();
}
private Arc[] topologicalSort(List arcs) {
return topologicalSort(arcs.toArray(new Arc[arcs.size()]));
}
private void addComponentSizes(List result, PackedMap links) {
for (int i = 0; i < links.keys.length; i++) {
Interval key = links.keys[i];
include(result, key, links.values[i], false);
}
}
private Arc[] createArcs() {
List mins = new ArrayList();
List maxs = new ArrayList();
// Add the minimum values from the components.
addComponentSizes(mins, getForwardLinks());
// Add the maximum values from the components.
addComponentSizes(maxs, getBackwardLinks());
// Add ordering constraints to prevent row/col sizes from going negative
if (orderPreserved) {
// Add a constraint for every row/col
for (int i = 0; i < getCount(); i++) {
include(mins, new Interval(i, i + 1), new MutableInt(0));
}
}
// Add the container constraints. Use the version of include that allows
// duplicate entries in case a child spans the entire grid.
int N = getCount();
include(mins, new Interval(0, N), parentMin, false);
include(maxs, new Interval(N, 0), parentMax, false);
// Sort
Arc[] sMins = topologicalSort(mins);
Arc[] sMaxs = topologicalSort(maxs);
return append(sMins, sMaxs);
}
private void computeArcs() {
// getting the links validates the values that are shared by the arc list
getForwardLinks();
getBackwardLinks();
}
public Arc[] getArcs() {
if (arcs == null) {
arcs = createArcs();
}
if (!arcsValid) {
computeArcs();
arcsValid = true;
}
return arcs;
}
private boolean relax(int[] locations, Arc entry) {
if (!entry.valid) {
return false;
}
Interval span = entry.span;
int u = span.min;
int v = span.max;
int value = entry.value.value;
int candidate = locations[u] + value;
if (candidate > locations[v]) {
locations[v] = candidate;
return true;
}
return false;
}
private void init(int[] locations) {
Arrays.fill(locations, 0);
}
private String arcsToString(List arcs) {
String var = horizontal ? "x" : "y";
StringBuilder result = new StringBuilder();
boolean first = true;
for (Arc arc : arcs) {
if (first) {
first = false;
} else {
result = result.append(", ");
}
int src = arc.span.min;
int dst = arc.span.max;
int value = arc.value.value;
result.append((src < dst) ?
var + dst + "-" + var + src + ">=" + value :
var + src + "-" + var + dst + "<=" + -value);
}
return result.toString();
}
private void logError(String axisName, Arc[] arcs, boolean[] culprits0) {
List culprits = new ArrayList();
List removed = new ArrayList();
for (int c = 0; c < arcs.length; c++) {
Arc arc = arcs[c];
if (culprits0[c]) {
culprits.add(arc);
}
if (!arc.valid) {
removed.add(arc);
}
}
mPrinter.println(axisName + " constraints: " + arcsToString(culprits) +
" are inconsistent; permanently removing: " + arcsToString(removed) + ". ");
}
/*
Bellman-Ford variant - modified to reduce typical running time from O(N^2) to O(N)
GridLayout converts its requirements into a system of linear constraints of the
form:
x[i] - x[j] < a[k]
Where the x[i] are variables and the a[k] are constants.
For example, if the variables were instead labeled x, y, z we might have:
x - y < 17
y - z < 23
z - x < 42
This is a special case of the Linear Programming problem that is, in turn,
equivalent to the single-source shortest paths problem on a digraph, for
which the O(n^2) Bellman-Ford algorithm the most commonly used general solution.
*/
private boolean solve(Arc[] arcs, int[] locations) {
return solve(arcs, locations, true);
}
private boolean solve(Arc[] arcs, int[] locations, boolean modifyOnError) {
String axisName = horizontal ? "horizontal" : "vertical";
int N = getCount() + 1; // The number of vertices is the number of columns/rows + 1.
boolean[] originalCulprits = null;
for (int p = 0; p < arcs.length; p++) {
init(locations);
// We take one extra pass over traditional Bellman-Ford (and omit their final step)
for (int i = 0; i < N; i++) {
boolean changed = false;
for (int j = 0, length = arcs.length; j < length; j++) {
changed |= relax(locations, arcs[j]);
}
if (!changed) {
if (originalCulprits != null) {
logError(axisName, arcs, originalCulprits);
}
return true;
}
}
if (!modifyOnError) {
return false; // cannot solve with these constraints
}
boolean[] culprits = new boolean[arcs.length];
for (int i = 0; i < N; i++) {
for (int j = 0, length = arcs.length; j < length; j++) {
culprits[j] |= relax(locations, arcs[j]);
}
}
if (p == 0) {
originalCulprits = culprits;
}
for (int i = 0; i < arcs.length; i++) {
if (culprits[i]) {
Arc arc = arcs[i];
// Only remove max values, min values alone cannot be inconsistent
if (arc.span.min < arc.span.max) {
continue;
}
arc.valid = false;
break;
}
}
}
return true;
}
private void computeMargins(boolean leading) {
int[] margins = leading ? leadingMargins : trailingMargins;
for (int i = 0, N = getChildCount(); i < N; i++) {
View c = getChildAt(i);
if (c.getVisibility() == View.GONE) continue;
LayoutParams lp = getLayoutParams(c);
Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
Interval span = spec.span;
int index = leading ? span.min : span.max;
margins[index] = max(margins[index], getMargin1(c, horizontal, leading));
}
}
// External entry points
public int[] getLeadingMargins() {
if (leadingMargins == null) {
leadingMargins = new int[getCount() + 1];
}
if (!leadingMarginsValid) {
computeMargins(true);
leadingMarginsValid = true;
}
return leadingMargins;
}
public int[] getTrailingMargins() {
if (trailingMargins == null) {
trailingMargins = new int[getCount() + 1];
}
if (!trailingMarginsValid) {
computeMargins(false);
trailingMarginsValid = true;
}
return trailingMargins;
}
private boolean solve(int[] a) {
return solve(getArcs(), a);
}
private boolean computeHasWeights() {
for (int i = 0, N = getChildCount(); i < N; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = getLayoutParams(child);
Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
if (spec.weight != 0) {
return true;
}
}
return false;
}
private boolean hasWeights() {
if (!hasWeightsValid) {
hasWeights = computeHasWeights();
hasWeightsValid = true;
}
return hasWeights;
}
public int[] getDeltas() {
if (deltas == null) {
deltas = new int[getChildCount()];
}
return deltas;
}
private void shareOutDelta(int totalDelta, float totalWeight) {
Arrays.fill(deltas, 0);
for (int i = 0, N = getChildCount(); i < N; i++) {
final View c = getChildAt(i);
if (c.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = getLayoutParams(c);
Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
float weight = spec.weight;
if (weight != 0) {
int delta = Math.round((weight * totalDelta / totalWeight));
deltas[i] = delta;
// the two adjustments below are to counter the above rounding and avoid
// off-by-ones at the end
totalDelta -= delta;
totalWeight -= weight;
}
}
}
private void solveAndDistributeSpace(int[] a) {
Arrays.fill(getDeltas(), 0);
solve(a);
int deltaMax = parentMin.value * getChildCount() + 1; //exclusive
if (deltaMax < 2) {
return; //don't have any delta to distribute
}
int deltaMin = 0; //inclusive
float totalWeight = calculateTotalWeight();
int validDelta = -1; //delta for which a solution exists
boolean validSolution = true;
// do a binary search to find the max delta that won't conflict with constraints
while(deltaMin < deltaMax) {
// cast to long to prevent overflow.
final int delta = (int) (((long) deltaMin + deltaMax) / 2);
invalidateValues();
shareOutDelta(delta, totalWeight);
validSolution = solve(getArcs(), a, false);
if (validSolution) {
validDelta = delta;
deltaMin = delta + 1;
} else {
deltaMax = delta;
}
}
if (validDelta > 0 && !validSolution) {
// last solution was not successful but we have a successful one. Use it.
invalidateValues();
shareOutDelta(validDelta, totalWeight);
solve(a);
}
}
private float calculateTotalWeight() {
float totalWeight = 0f;
for (int i = 0, N = getChildCount(); i < N; i++) {
View c = getChildAt(i);
if (c.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = getLayoutParams(c);
Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
totalWeight += spec.weight;
}
return totalWeight;
}
private void computeLocations(int[] a) {
if (!hasWeights()) {
solve(a);
} else {
solveAndDistributeSpace(a);
}
if (!orderPreserved) {
// Solve returns the smallest solution to the constraint system for which all
// values are positive. One value is therefore zero - though if the row/col
// order is not preserved this may not be the first vertex. For consistency,
// translate all the values so that they measure the distance from a[0]; the
// leading edge of the parent. After this transformation some values may be
// negative.
int a0 = a[0];
for (int i = 0, N = a.length; i < N; i++) {
a[i] = a[i] - a0;
}
}
}
public int[] getLocations() {
if (locations == null) {
int N = getCount() + 1;
locations = new int[N];
}
if (!locationsValid) {
computeLocations(locations);
locationsValid = true;
}
return locations;
}
private int size(int[] locations) {
// The parental edges are attached to vertices 0 and N - even when order is not
// being preserved and other vertices fall outside this range. Measure the distance
// between vertices 0 and N, assuming that locations[0] = 0.
return locations[getCount()];
}
private void setParentConstraints(int min, int max) {
parentMin.value = min;
parentMax.value = -max;
locationsValid = false;
}
private int getMeasure(int min, int max) {
setParentConstraints(min, max);
return size(getLocations());
}
public int getMeasure(int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED: {
return getMeasure(0, MAX_SIZE);
}
case MeasureSpec.EXACTLY: {
return getMeasure(size, size);
}
case MeasureSpec.AT_MOST: {
return getMeasure(0, size);
}
default: {
assert false;
return 0;
}
}
}
public void layout(int size) {
setParentConstraints(size, size);
getLocations();
}
public void invalidateStructure() {
maxIndex = UNDEFINED;
groupBounds = null;
forwardLinks = null;
backwardLinks = null;
leadingMargins = null;
trailingMargins = null;
arcs = null;
locations = null;
deltas = null;
hasWeightsValid = false;
invalidateValues();
}
public void invalidateValues() {
groupBoundsValid = false;
forwardLinksValid = false;
backwardLinksValid = false;
leadingMarginsValid = false;
trailingMarginsValid = false;
arcsValid = false;
locationsValid = 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 #rowSpec} and {@link #columnSpec} layout parameters.
* {@link GridLayout.Spec Specs} are immutable structures
* and may be shared between the layout parameters of different children.
*
* The row and column specs contain the leading and trailing indices along each axis
* and together specify the four grid indices that delimit the cells of this cell group.
*
* The alignment properties of the row and column specs 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.
*
* The weight property is also included in Spec and specifies the proportion of any
* excess space that is due to the associated view.
*
*
WRAP_CONTENT and MATCH_PARENT
*
* Because the default values of the {@link #width} and {@link #height}
* properties are both {@link #WRAP_CONTENT}, this value never needs to be explicitly
* declared in the layout parameters of GridLayout's children. In addition,
* GridLayout does not distinguish the special size value {@link #MATCH_PARENT} from
* {@link #WRAP_CONTENT}. A component's ability to expand to the size of the parent is
* instead controlled by the principle of flexibility,
* as discussed in {@link GridLayout}.
*
* Summary
*
* You should not need to use either of the special size values:
* {@code WRAP_CONTENT} or {@code MATCH_PARENT} when configuring the children of
* a GridLayout.
*
* Default values
*
*
* - {@link #width} = {@link #WRAP_CONTENT}
* - {@link #height} = {@link #WRAP_CONTENT}
* - {@link #topMargin} = 0 when
* {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
* {@code false}; otherwise {@link #UNDEFINED}, to
* indicate that a default value should be computed on demand.
* - {@link #leftMargin} = 0 when
* {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
* {@code false}; otherwise {@link #UNDEFINED}, to
* indicate that a default value should be computed on demand.
* - {@link #bottomMargin} = 0 when
* {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
* {@code false}; otherwise {@link #UNDEFINED}, to
* indicate that a default value should be computed on demand.
* - {@link #rightMargin} = 0 when
* {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
* {@code false}; otherwise {@link #UNDEFINED}, to
* indicate that a default value should be computed on demand.
* - {@link #rowSpec}
.row
= {@link #UNDEFINED}
* - {@link #rowSpec}
.rowSpan
= 1
* - {@link #rowSpec}
.alignment
= {@link #BASELINE}
* - {@link #rowSpec}
.weight
= 0
* - {@link #columnSpec}
.column
= {@link #UNDEFINED}
* - {@link #columnSpec}
.columnSpan
= 1
* - {@link #columnSpec}
.alignment
= {@link #START}
* - {@link #columnSpec}
.weight
= 0
*
*
* See {@link GridLayout} for a more complete description of the conventions
* used by GridLayout in the interpretation of the properties of this class.
*
* @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(UNDEFINED, UNDEFINED + 1);
private static final int DEFAULT_SPAN_SIZE = DEFAULT_SPAN.size();
// TypedArray indices
private static final int MARGIN = R.styleable.ViewGroup_MarginLayout_layout_margin;
private static final int LEFT_MARGIN = R.styleable.ViewGroup_MarginLayout_layout_marginLeft;
private static final int TOP_MARGIN = R.styleable.ViewGroup_MarginLayout_layout_marginTop;
private static final int RIGHT_MARGIN =
R.styleable.ViewGroup_MarginLayout_layout_marginRight;
private static final int BOTTOM_MARGIN =
R.styleable.ViewGroup_MarginLayout_layout_marginBottom;
private static final int COLUMN = R.styleable.GridLayout_Layout_layout_column;
private static final int COLUMN_SPAN = R.styleable.GridLayout_Layout_layout_columnSpan;
private static final int COLUMN_WEIGHT = R.styleable.GridLayout_Layout_layout_columnWeight;
private static final int ROW = R.styleable.GridLayout_Layout_layout_row;
private static final int ROW_SPAN = R.styleable.GridLayout_Layout_layout_rowSpan;
private static final int ROW_WEIGHT = R.styleable.GridLayout_Layout_layout_rowWeight;
private static final int GRAVITY = R.styleable.GridLayout_Layout_layout_gravity;
// Instance variables
/**
* The spec that defines the vertical characteristics of the cell group
* described by these layout parameters.
* If an assignment is made to this field after a measurement or layout operation
* has already taken place, a call to
* {@link ViewGroup#setLayoutParams(ViewGroup.LayoutParams)}
* must be made to notify GridLayout of the change. GridLayout is normally able
* to detect when code fails to observe this rule, issue a warning and take steps to
* compensate for the omission. This facility is implemented on a best effort basis
* and should not be relied upon in production code - so it is best to include the above
* calls to remove the warnings as soon as it is practical.
*/
public Spec rowSpec = Spec.UNDEFINED;
/**
* The spec that defines the horizontal characteristics of the cell group
* described by these layout parameters.
* If an assignment is made to this field after a measurement or layout operation
* has already taken place, a call to
* {@link ViewGroup#setLayoutParams(ViewGroup.LayoutParams)}
* must be made to notify GridLayout of the change. GridLayout is normally able
* to detect when code fails to observe this rule, issue a warning and take steps to
* compensate for the omission. This facility is implemented on a best effort basis
* and should not be relied upon in production code - so it is best to include the above
* calls to remove the warnings as soon as it is practical.
*/
public Spec columnSpec = Spec.UNDEFINED;
// Constructors
private LayoutParams(
int width, int height,
int left, int top, int right, int bottom,
Spec rowSpec, Spec columnSpec) {
super(width, height);
setMargins(left, top, right, bottom);
this.rowSpec = rowSpec;
this.columnSpec = columnSpec;
}
/**
* Constructs a new LayoutParams instance for this rowSpec
* and columnSpec
. All other fields are initialized with
* default values as defined in {@link LayoutParams}.
*
* @param rowSpec the rowSpec
* @param columnSpec the columnSpec
*/
public LayoutParams(Spec rowSpec, Spec columnSpec) {
this(DEFAULT_WIDTH, DEFAULT_HEIGHT,
DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN,
rowSpec, columnSpec);
}
/**
* Constructs a new LayoutParams with default values as defined in {@link LayoutParams}.
*/
public LayoutParams() {
this(Spec.UNDEFINED, Spec.UNDEFINED);
}
// Copying constructors
/**
* {@inheritDoc}
*/
public LayoutParams(ViewGroup.LayoutParams params) {
super(params);
}
/**
* {@inheritDoc}
*/
public LayoutParams(MarginLayoutParams params) {
super(params);
}
/**
* Copy constructor. Clones the width, height, margin values, row spec,
* and column spec of the source.
*
* @param source The layout params to copy from.
*/
public LayoutParams(LayoutParams source) {
super(source);
this.rowSpec = source.rowSpec;
this.columnSpec = source.columnSpec;
}
// AttributeSet constructors
/**
* {@inheritDoc}
*
* Values not defined in the attribute set take the default values
* defined in {@link LayoutParams}.
*/
public LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
reInitSuper(context, attrs);
init(context, attrs);
}
// Implementation
// 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, R.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();
}
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridLayout_Layout);
try {
int gravity = a.getInt(GRAVITY, Gravity.NO_GRAVITY);
int column = a.getInt(COLUMN, DEFAULT_COLUMN);
int colSpan = a.getInt(COLUMN_SPAN, DEFAULT_SPAN_SIZE);
float colWeight = a.getFloat(COLUMN_WEIGHT, Spec.DEFAULT_WEIGHT);
this.columnSpec = spec(column, colSpan, getAlignment(gravity, true), colWeight);
int row = a.getInt(ROW, DEFAULT_ROW);
int rowSpan = a.getInt(ROW_SPAN, DEFAULT_SPAN_SIZE);
float rowWeight = a.getFloat(ROW_WEIGHT, Spec.DEFAULT_WEIGHT);
this.rowSpec = spec(row, rowSpan, getAlignment(gravity, false), rowWeight);
} finally {
a.recycle();
}
}
/**
* Describes how the child views are positioned. Default is {@code LEFT | BASELINE}.
* See {@link Gravity}.
*
* @param gravity the new gravity value
*
* @attr ref android.R.styleable#GridLayout_Layout_layout_gravity
*/
public void setGravity(int gravity) {
rowSpec = rowSpec.copyWriteAlignment(getAlignment(gravity, false));
columnSpec = columnSpec.copyWriteAlignment(getAlignment(gravity, true));
}
@Override
protected void setBaseAttributes(TypedArray attributes, int widthAttr, int heightAttr) {
this.width = attributes.getLayoutDimension(widthAttr, DEFAULT_WIDTH);
this.height = attributes.getLayoutDimension(heightAttr, DEFAULT_HEIGHT);
}
final void setRowSpecSpan(Interval span) {
rowSpec = rowSpec.copyWriteSpan(span);
}
final void setColumnSpecSpan(Interval span) {
columnSpec = columnSpec.copyWriteSpan(span);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LayoutParams that = (LayoutParams) o;
if (!columnSpec.equals(that.columnSpec)) return false;
if (!rowSpec.equals(that.rowSpec)) return false;
return true;
}
@Override
public int hashCode() {
int result = rowSpec.hashCode();
result = 31 * result + columnSpec.hashCode();
return result;
}
}
/*
In place of a HashMap from span to Int, use an array of key/value pairs - stored in Arcs.
Add the mutables completesCycle flag to avoid creating another hash table for detecting cycles.
*/
final static class Arc {
public final Interval span;
public final MutableInt value;
public boolean valid = true;
public Arc(Interval span, MutableInt value) {
this.span = span;
this.value = value;
}
@Override
public String toString() {
return span + " " + (!valid ? "+>" : "->") + " " + value;
}
}
// A mutable Integer - used to avoid heap allocation during the layout operation
final static class MutableInt {
public int value;
public MutableInt() {
reset();
}
public MutableInt(int value) {
this.value = value;
}
public void reset() {
value = Integer.MIN_VALUE;
}
@Override
public String toString() {
return Integer.toString(value);
}
}
final static class Assoc extends ArrayList> {
private final Class keyType;
private final Class valueType;
private Assoc(Class keyType, Class valueType) {
this.keyType = keyType;
this.valueType = valueType;
}
public static Assoc of(Class keyType, Class valueType) {
return new Assoc(keyType, valueType);
}
public void put(K key, V value) {
add(Pair.create(key, value));
}
@SuppressWarnings(value = "unchecked")
public PackedMap pack() {
int N = size();
K[] keys = (K[]) Array.newInstance(keyType, N);
V[] values = (V[]) Array.newInstance(valueType, N);
for (int i = 0; i < N; i++) {
keys[i] = get(i).first;
values[i] = get(i).second;
}
return new PackedMap(keys, values);
}
}
/*
This data structure is used in place of a Map where we have an index that refers to the order
in which each key/value pairs were added to the map. In this case we store keys and values
in arrays of a length that is equal to the number of unique keys. We also maintain an
array of indexes from insertion order to the compacted arrays of keys and values.
Note that behavior differs from that of a LinkedHashMap in that repeated entries
*do* get added multiples times. So the length of index is equals to the number of
items added.
This is useful in the GridLayout class where we can rely on the order of children not
changing during layout - to use integer-based lookup for our internal structures
rather than using (and storing) an implementation of Map.
*/
@SuppressWarnings(value = "unchecked")
final 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 = compact(keys, index);
this.values = compact(values, index);
}
public 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;
}
/*
Create a compact array of keys or values using the supplied index.
*/
private static K[] compact(K[] a, int[] index) {
int size = a.length;
Class> componentType = a.getClass().getComponentType();
K[] result = (K[]) Array.newInstance(componentType, max2(index, -1) + 1);
// this overwrite duplicates, retaining the last equivalent entry
for (int i = 0; i < size; i++) {
result[index[i]] = a[i];
}
return result;
}
}
/*
For each group (with a given alignment) we need to store the amount of space required
before the alignment point and the amount of space required after it. One side of this
calculation is always 0 for START and END alignments but we don't make use of this.
For CENTER and BASELINE alignments both sides are needed and in the BASELINE case no
simple optimisations are possible.
The general algorithm therefore is to create a Map (actually a PackedMap) from
group to Bounds and to loop through all Views in the group taking the maximum
of the values for each View.
*/
static class Bounds {
public int before;
public int after;
public int flexibility; // we're flexible iff all included specs are flexible
private Bounds() {
reset();
}
protected void reset() {
before = Integer.MIN_VALUE;
after = Integer.MIN_VALUE;
flexibility = CAN_STRETCH; // from the above, we're flexible when empty
}
protected void include(int before, int after) {
this.before = max(this.before, before);
this.after = max(this.after, after);
}
protected int size(boolean min) {
if (!min) {
if (canStretch(flexibility)) {
return MAX_SIZE;
}
}
return before + after;
}
protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean horizontal) {
return before - a.getAlignmentValue(c, size, gl.getLayoutMode());
}
protected final void include(GridLayout gl, View c, Spec spec, Axis axis, int size) {
this.flexibility &= spec.getFlexibility();
boolean horizontal = axis.horizontal;
Alignment alignment = spec.getAbsoluteAlignment(axis.horizontal);
// todo test this works correctly when the returned value is UNDEFINED
int before = alignment.getAlignmentValue(c, size, gl.getLayoutMode());
include(before, size - before);
}
@Override
public String toString() {
return "Bounds{" +
"before=" + before +
", after=" + after +
'}';
}
}
/**
* 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 {@code [min, max]} and represent the set of values
* {@code x} such that {@code min <= x < max}.
*/
final static class Interval {
/**
* The minimum value.
*/
public final int min;
/**
* The maximum value.
*/
public final int max;
/**
* Construct a new Interval, {@code interval}, where:
*
* - {@code interval.min = min}
* - {@code 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;
}
int size() {
return max - min;
}
Interval inverse() {
return new Interval(max, min);
}
/**
* Returns {@code true} if the {@link #getClass class},
* {@link #min} and {@link #max} properties of this Interval and the
* supplied parameter are pairwise equal; {@code 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(@Nullable 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;
}
//noinspection RedundantIfStatement
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 Spec defines the horizontal or vertical characteristics of a group of
* cells. Each spec. defines the grid indices and alignment
* along the appropriate axis.
*
* The grid indices are the leading and trailing edges of this cell group.
* See {@link GridLayout} for a description of the conventions used by GridLayout
* for grid indices.
*
* The alignment property 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.
*
* Use the following static methods to create specs:
*
* - {@link #spec(int)}
* - {@link #spec(int, int)}
* - {@link #spec(int, Alignment)}
* - {@link #spec(int, int, Alignment)}
* - {@link #spec(int, float)}
* - {@link #spec(int, int, float)}
* - {@link #spec(int, Alignment, float)}
* - {@link #spec(int, int, Alignment, float)}
*
*
*/
public static class Spec {
static final Spec UNDEFINED = spec(GridLayout.UNDEFINED);
static final float DEFAULT_WEIGHT = 0;
final boolean startDefined;
final Interval span;
final Alignment alignment;
final float weight;
private Spec(boolean startDefined, Interval span, Alignment alignment, float weight) {
this.startDefined = startDefined;
this.span = span;
this.alignment = alignment;
this.weight = weight;
}
private Spec(boolean startDefined, int start, int size, Alignment alignment, float weight) {
this(startDefined, new Interval(start, start + size), alignment, weight);
}
private Alignment getAbsoluteAlignment(boolean horizontal) {
if (alignment != UNDEFINED_ALIGNMENT) {
return alignment;
}
if (weight == 0f) {
return horizontal ? START : BASELINE;
}
return FILL;
}
final Spec copyWriteSpan(Interval span) {
return new Spec(startDefined, span, alignment, weight);
}
final Spec copyWriteAlignment(Alignment alignment) {
return new Spec(startDefined, span, alignment, weight);
}
final int getFlexibility() {
return (alignment == UNDEFINED_ALIGNMENT && weight == 0) ? INFLEXIBLE : CAN_STRETCH;
}
/**
* Returns {@code true} if the {@code class}, {@code alignment} and {@code span}
* properties of this Spec and the supplied parameter are pairwise equal,
* {@code false} otherwise.
*
* @param that the object to compare this spec with
*
* @return {@code true} if the specified object is equal to this
* {@code Spec}; {@code false} otherwise
*/
@Override
public boolean equals(@Nullable Object that) {
if (this == that) {
return true;
}
if (that == null || getClass() != that.getClass()) {
return false;
}
Spec spec = (Spec) that;
if (!alignment.equals(spec.alignment)) {
return false;
}
//noinspection RedundantIfStatement
if (!span.equals(spec.span)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = span.hashCode();
result = 31 * result + alignment.hashCode();
return result;
}
}
/**
* Return a Spec, {@code spec}, where:
*
* - {@code spec.span = [start, start + size]}
* - {@code spec.alignment = alignment}
* - {@code spec.weight = weight}
*
*
* To leave the start index undefined, use the value {@link #UNDEFINED}.
*
* @param start the start
* @param size the size
* @param alignment the alignment
* @param weight the weight
*/
public static Spec spec(int start, int size, Alignment alignment, float weight) {
return new Spec(start != UNDEFINED, start, size, alignment, weight);
}
/**
* Equivalent to: {@code spec(start, 1, alignment, weight)}.
*
* @param start the start
* @param alignment the alignment
* @param weight the weight
*/
public static Spec spec(int start, Alignment alignment, float weight) {
return spec(start, 1, alignment, weight);
}
/**
* Equivalent to: {@code spec(start, 1, default_alignment, weight)} -
* where {@code default_alignment} is specified in
* {@link android.widget.GridLayout.LayoutParams}.
*
* @param start the start
* @param size the size
* @param weight the weight
*/
public static Spec spec(int start, int size, float weight) {
return spec(start, size, UNDEFINED_ALIGNMENT, weight);
}
/**
* Equivalent to: {@code spec(start, 1, weight)}.
*
* @param start the start
* @param weight the weight
*/
public static Spec spec(int start, float weight) {
return spec(start, 1, weight);
}
/**
* Equivalent to: {@code spec(start, size, alignment, 0f)}.
*
* @param start the start
* @param size the size
* @param alignment the alignment
*/
public static Spec spec(int start, int size, Alignment alignment) {
return spec(start, size, alignment, Spec.DEFAULT_WEIGHT);
}
/**
* Return a Spec, {@code spec}, where:
*
* - {@code spec.span = [start, start + 1]}
* - {@code spec.alignment = alignment}
*
*
* To leave the start index undefined, use the value {@link #UNDEFINED}.
*
* @param start the start index
* @param alignment the alignment
*
* @see #spec(int, int, Alignment)
*/
public static Spec spec(int start, Alignment alignment) {
return spec(start, 1, alignment);
}
/**
* Return a Spec, {@code spec}, where:
*
* - {@code spec.span = [start, start + size]}
*
*
* To leave the start index undefined, use the value {@link #UNDEFINED}.
*
* @param start the start
* @param size the size
*
* @see #spec(int, Alignment)
*/
public static Spec spec(int start, int size) {
return spec(start, size, UNDEFINED_ALIGNMENT);
}
/**
* Return a Spec, {@code spec}, where:
*
* - {@code spec.span = [start, start + 1]}
*
*
* To leave the start index undefined, use the value {@link #UNDEFINED}.
*
* @param start the start index
*
* @see #spec(int, int)
*/
public static Spec spec(int start) {
return spec(start, 1);
}
/**
* 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#rowSpec rowSpec}
* and a {@link LayoutParams#columnSpec columnSpec} each of which contains an
* {@code alignment}. Overall placement of the view in the cell
* group is specified by the two alignments which act along each axis independently.
*
* The GridLayout class defines the most common alignments used in general layout:
* {@link #TOP}, {@link #LEFT}, {@link #BOTTOM}, {@link #RIGHT}, {@link #START},
* {@link #END}, {@link #CENTER}, {@link #BASELINE} and {@link #FILL}.
*/
/*
* An Alignment implementation must define {@link #getAlignmentValue(View, int, int)},
* to return the appropriate value for the type of alignment being defined.
* The enclosing algorithms position the children
* so that the locations defined by the alignment values
* are the same for all of the views in a group.
*
*/
public static abstract class Alignment {
Alignment() {
}
abstract int getGravityOffset(View view, int cellDelta);
/**
* 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
* @param mode the basis of alignment: CLIP or OPTICAL
* @return the alignment value
*/
abstract int getAlignmentValue(View view, int viewSize, int mode);
/**
* 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.
*
* The default implementation returns {@code viewSize}.
*
* @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
*/
int getSizeInCell(View view, int viewSize, int cellSize) {
return viewSize;
}
Bounds getBounds() {
return new Bounds();
}
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
static final Alignment UNDEFINED_ALIGNMENT = new Alignment() {
@Override
int getGravityOffset(View view, int cellDelta) {
return UNDEFINED;
}
@Override
public int getAlignmentValue(View view, int viewSize, int mode) {
return UNDEFINED;
}
};
/**
* Indicates that a view should be aligned with the start
* edges of the other views in its cell group.
*/
private static final Alignment LEADING = new Alignment() {
@Override
int getGravityOffset(View view, int cellDelta) {
return 0;
}
@Override
public int getAlignmentValue(View view, int viewSize, int mode) {
return 0;
}
};
/**
* Indicates that a view should be aligned with the end
* edges of the other views in its cell group.
*/
private static final Alignment TRAILING = new Alignment() {
@Override
int getGravityOffset(View view, int cellDelta) {
return cellDelta;
}
@Override
public int getAlignmentValue(View view, int viewSize, int mode) {
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 start
* edges of the other views in its cell group.
*/
public static final Alignment START = LEADING;
/**
* Indicates that a view should be aligned with the end
* edges of the other views in its cell group.
*/
public static final Alignment END = TRAILING;
private static Alignment createSwitchingAlignment(final Alignment ltr, final Alignment rtl) {
return new Alignment() {
@Override
int getGravityOffset(View view, int cellDelta) {
return (!view.isLayoutRtl() ? ltr : rtl).getGravityOffset(view, cellDelta);
}
@Override
public int getAlignmentValue(View view, int viewSize, int mode) {
return (!view.isLayoutRtl() ? ltr : rtl).getAlignmentValue(view, viewSize, mode);
}
};
}
/**
* Indicates that a view should be aligned with the left
* edges of the other views in its cell group.
*/
public static final Alignment LEFT = createSwitchingAlignment(START, END);
/**
* Indicates that a view should be aligned with the right
* edges of the other views in its cell group.
*/
public static final Alignment RIGHT = createSwitchingAlignment(END, START);
/**
* Indicates that a view should be centered with the other views in its cell group.
* This constant may be used in both {@link LayoutParams#rowSpec rowSpecs} and {@link
* LayoutParams#columnSpec columnSpecs}.
*/
public static final Alignment CENTER = new Alignment() {
@Override
int getGravityOffset(View view, int cellDelta) {
return cellDelta >> 1;
}
@Override
public int getAlignmentValue(View view, int viewSize, int mode) {
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#rowSpec rowSpecs}.
*
* @see View#getBaseline()
*/
public static final Alignment BASELINE = new Alignment() {
@Override
int getGravityOffset(View view, int cellDelta) {
return 0; // baseline gravity is top
}
@Override
public int getAlignmentValue(View view, int viewSize, int mode) {
if (view.getVisibility() == GONE) {
return 0;
}
int baseline = view.getBaseline();
return baseline == -1 ? UNDEFINED : baseline;
}
@Override
public Bounds getBounds() {
return new Bounds() {
/*
In a baseline aligned row in which some components define a baseline
and some don't, we need a third variable to properly account for all
the sizes. This tracks the maximum size of all the components -
including those that don't define a baseline.
*/
private int size;
@Override
protected void reset() {
super.reset();
size = Integer.MIN_VALUE;
}
@Override
protected void include(int before, int after) {
super.include(before, after);
size = max(size, before + after);
}
@Override
protected int size(boolean min) {
return max(super.size(min), size);
}
@Override
protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean hrz) {
return max(0, super.getOffset(gl, c, a, size, hrz));
}
};
}
};
/**
* Indicates that a view should expanded to fit the boundaries of its cell group.
* This constant may be used in both {@link LayoutParams#rowSpec rowSpecs} and
* {@link LayoutParams#columnSpec columnSpecs}.
*/
public static final Alignment FILL = new Alignment() {
@Override
int getGravityOffset(View view, int cellDelta) {
return 0;
}
@Override
public int getAlignmentValue(View view, int viewSize, int mode) {
return UNDEFINED;
}
@Override
public int getSizeInCell(View view, int viewSize, int cellSize) {
return cellSize;
}
};
static boolean canStretch(int flexibility) {
return (flexibility & CAN_STRETCH) != 0;
}
private static final int INFLEXIBLE = 0;
private static final int CAN_STRETCH = 2;
}