blob: a434aef00de9b584c489333af930ae85b608ec8b [file] [log] [blame]
/*
* Copyright (C) 2015 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.support.constraint.solver.widgets;
import android.support.constraint.solver.LinearSystem;
import java.util.ArrayList;
/**
* Implements a table-like layout. The table can grow automatically either horizontally
* or vertically, depending on the given orientation (by default it will grow vertically).
*/
public class ConstraintTableLayout extends ConstraintWidgetContainer {
private boolean mVerticalGrowth = true; // will grow vertically, with number of columns set.
private int mNumCols = 0;
private int mNumRows = 0;
private int mPadding = 8;
/**
* Internal utility class representing an horizontal slice of the table.
*/
class HorizontalSlice {
ConstraintWidget top;
ConstraintWidget bottom;
int padding;
}
/**
* Internal utility class representing a vertical slice of the table.
*/
class VerticalSlice {
ConstraintWidget left;
ConstraintWidget right;
int alignment = ALIGN_LEFT;
int padding;
}
private ArrayList<VerticalSlice> mVerticalSlices = new ArrayList<>();
private ArrayList<HorizontalSlice> mHorizontalSlices = new ArrayList<>();
private ArrayList<Guideline> mVerticalGuidelines = new ArrayList<>();
private ArrayList<Guideline> mHorizontalGuidelines = new ArrayList<>();
public static final int ALIGN_CENTER = 0;
public static final int ALIGN_LEFT = 1;
public static final int ALIGN_RIGHT = 2;
private static final int ALIGN_FULL = 3;
/**
* Default constructor
*/
public ConstraintTableLayout() {
}
/**
* Constructor
*
* @param x x position
* @param y y position
* @param width width of the layout
* @param height height of the layout
*/
public ConstraintTableLayout(int x, int y, int width, int height) {
super(x, y, width, height);
}
/**
* Constructor
*
* @param width width of the layout
* @param height height of the layout
*/
public ConstraintTableLayout(int width, int height) {
super(width, height);
}
/**
* Specify the xml type for the container
*
* @return
*/
@Override
public String getType() {
return "ConstraintTableLayout";
}
/**
* Accessor returning the number of rows in the table
*
* @return number of rows
*/
public int getNumRows() {
return mNumRows;
}
/**
* Accessor returning the number of rows in the table
*
* @return number of rows
*/
public int getNumCols() {
return mNumCols;
}
/**
* Accessor returning the padding of the table
*
* @return internal padding
*/
public int getPadding() {
return mPadding;
}
/**
* Return a string representation of the current columns alignment.
*
* @return string representing the columns alignment
*/
public String getColumnsAlignmentRepresentation() {
final int numSlices = mVerticalSlices.size();
String result = "";
for (int i = 0; i < numSlices; i++) {
VerticalSlice slice = mVerticalSlices.get(i);
if (slice.alignment == ALIGN_LEFT) {
result += "L";
} else if (slice.alignment == ALIGN_CENTER) {
result += "C";
} else if (slice.alignment == ALIGN_FULL) {
result += "F";
} else if (slice.alignment == ALIGN_RIGHT) {
result += "R";
}
}
return result;
}
/**
* Return a string representation of the given column's alignment
*
* @param column the column index
* @return string representing the column's alignment
*/
public String getColumnAlignmentRepresentation(int column) {
VerticalSlice slice = mVerticalSlices.get(column);
if (slice.alignment == ALIGN_LEFT) {
return "L";
} else if (slice.alignment == ALIGN_CENTER) {
return "C";
} else if (slice.alignment == ALIGN_FULL) {
return "F";
} else if (slice.alignment == ALIGN_RIGHT) {
return "R";
}
return "!";
}
/**
* Set the number of columns desired. Will only apply if the table is configured
* to grow vertically -- the number of rows will be automatically derived from the
* number of columns and the number of children.
*
* @param num
*/
public void setNumCols(int num) {
if (mVerticalGrowth && mNumCols != num) {
mNumCols = num;
setVerticalSlices();
setTableDimensions();
}
}
/**
* Set the number of rows desired. Will only apply if the table is configured
* to grow horizontally -- the number of columns will be automatically derived from the
* number of rows and the number of children.
*
* @param num the number of desired rows.
*/
public void setNumRows(int num) {
if (!mVerticalGrowth && mNumCols != num) {
mNumRows = num;
setHorizontalSlices();
setTableDimensions();
}
}
/**
* Return the growth type for the table.
*
* @return true if the table grow vertically when new items are added (i.e., add more rows),
* and false if the table grow horizontally (i.e., add more columns)
*/
public boolean isVerticalGrowth() {
return mVerticalGrowth;
}
/**
* Set the growth type for the table, either vertical or horizontal.
*
* @param value true to set a vertical growth (the default)
*/
public void setVerticalGrowth(boolean value) {
mVerticalGrowth = value;
}
/**
* Set the value used for internal padding surrounding the cells.
*
* @param padding the padding value.
*/
public void setPadding(int padding) {
if (padding > 1) {
mPadding = padding;
}
}
/**
* Set the alignment (left/center/right) for the given column. The column are numeroted
* starting from zero.
*
* @param column the column number
* @param alignment the alignment type.
*/
public void setColumnAlignment(int column, int alignment) {
if (column < mVerticalSlices.size()) {
VerticalSlice slice = mVerticalSlices.get(column);
slice.alignment = alignment;
setChildrenConnections();
}
}
/**
* Cycle the alignment (left/center/right) for the given column.
*
* @param column the column number
*/
public void cycleColumnAlignment(int column) {
VerticalSlice slice = mVerticalSlices.get(column);
switch (slice.alignment) {
case ALIGN_LEFT: {
slice.alignment = ALIGN_CENTER;
} break;
case ALIGN_RIGHT: {
slice.alignment = ALIGN_LEFT;
} break;
case ALIGN_CENTER: {
slice.alignment = ALIGN_RIGHT;
} break;
}
setChildrenConnections();
}
/**
* Set the alignment (left/center/right) for columns, given a string representation.
*
* @param alignment
*/
public void setColumnAlignment(String alignment) {
for (int i = 0, n = alignment.length(); i < n; i++) {
char c = alignment.charAt(i);
if (c == 'L') {
setColumnAlignment(i, ALIGN_LEFT);
} else if (c == 'C') {
setColumnAlignment(i, ALIGN_CENTER);
} else if (c == 'F') {
setColumnAlignment(i, ALIGN_FULL);
} else if (c == 'R') {
setColumnAlignment(i, ALIGN_RIGHT);
} else {
setColumnAlignment(i, ALIGN_CENTER);
}
}
}
/**
* Accessor to the vertical guidelines contained in the table.
*
* @return array of guidelines
*/
@Override
public ArrayList<Guideline> getVerticalGuidelines() {
return mVerticalGuidelines;
}
/**
* Accessor to the horizontal guidelines contained in the table.
*
* @return array of guidelines
*/
@Override
public ArrayList<Guideline> getHorizontalGuidelines() {
return mHorizontalGuidelines;
}
/**
* Add the layout and its children to the solver
*
* @param system the solver we want to add the widget to
*/
@Override
public void addToSolver(LinearSystem system) {
super.addToSolver(system);
int count = mChildren.size();
if (count == 0) {
return;
}
setTableDimensions();
// We don't want to add guidelines on a different system than our own
if (system == mSystem) {
int num = mVerticalGuidelines.size();
for (int i = 0; i < num; i++) {
Guideline guideline = mVerticalGuidelines.get(i);
guideline.setPositionRelaxed(
getHorizontalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT);
guideline.addToSolver(system);
}
num = mHorizontalGuidelines.size();
for (int i = 0; i < num; i++) {
Guideline guideline = mHorizontalGuidelines.get(i);
guideline.setPositionRelaxed(
getVerticalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT);
guideline.addToSolver(system);
}
for (int i = 0; i < count; i++) {
ConstraintWidget child = mChildren.get(i);
child.addToSolver(system);
}
}
}
/**
* Will set up the dimensions of the table given the growth orientatio
* (horizontal or vertical growth) and the set numbers of rows or columns.
*/
public void setTableDimensions() {
int extra = 0;
int count = mChildren.size();
for (int i = 0; i < count; i++) {
ConstraintWidget widget = mChildren.get(i);
extra += widget.getContainerItemSkip();
}
count += extra;
if (mVerticalGrowth) {
if (mNumCols == 0) {
setNumCols(1);
}
int rows = count / mNumCols;
if (rows * mNumCols < count) {
rows++;
}
if (mNumRows == rows
&& (mVerticalGuidelines.size() == mNumCols - 1)) {
return;
}
mNumRows = rows;
setHorizontalSlices();
} else {
if (mNumRows == 0) {
setNumRows(1);
}
int cols = count / mNumRows;
if (cols * mNumRows < count) {
cols++;
}
if (mNumCols == cols
&& (mHorizontalGuidelines.size() == mNumRows - 1)) {
return;
}
mNumCols = cols;
setVerticalSlices();
}
setChildrenConnections();
}
/**
* Debug utility function to setup the names of the internal guidelines.
*
* @param s solver used
* @param name name of the widget
*/
@Override
public void setDebugSolverName(LinearSystem s, String name) {
system = s;
super.setDebugSolverName(s, name);
updateDebugSolverNames();
}
private LinearSystem system = null;
private void updateDebugSolverNames() {
if (system == null) {
return;
}
int num = mVerticalGuidelines.size();
for (int i = 0; i < num; i++) {
mVerticalGuidelines.get(i).setDebugSolverName(system, getDebugName() + ".VG" + i);
}
num = mHorizontalGuidelines.size();
for (int i = 0; i < num; i++) {
mHorizontalGuidelines.get(i).setDebugSolverName(system, getDebugName() + ".HG" + i);
}
}
/**
* Setup the vertical slices of the table. The internal ones need to create
* a right-side guideline.
*/
private void setVerticalSlices() {
mVerticalSlices.clear();
ConstraintWidget previous = this;
float increment = 100 / (float) mNumCols;
float percent = increment;
for (int i = 0; i < mNumCols; i++) {
VerticalSlice slice = new VerticalSlice();
slice.left = previous;
if (i < mNumCols - 1) {
Guideline guideline = new Guideline();
guideline.setOrientation(Guideline.VERTICAL);
guideline.setParent(this);
guideline.setGuidePercent((int) percent);
percent += increment;
slice.right = guideline;
mVerticalGuidelines.add(guideline);
} else {
slice.right = this;
}
previous = slice.right;
mVerticalSlices.add(slice);
}
updateDebugSolverNames();
}
/**
* Setup the horizontal slices of the table. The internal ones need to create
* a bottom-side guideline.
*/
private void setHorizontalSlices() {
mHorizontalSlices.clear();
float increment = 100 / (float) mNumRows;
float percent = increment;
ConstraintWidget previous = this;
for (int i = 0; i < mNumRows; i++) {
HorizontalSlice slice = new HorizontalSlice();
slice.top = previous;
if (i < mNumRows - 1) {
Guideline guideline = new Guideline();
guideline.setOrientation(Guideline.HORIZONTAL);
guideline.setParent(this);
guideline.setGuidePercent((int) percent);
percent += increment;
slice.bottom = guideline;
mHorizontalGuidelines.add(guideline);
} else {
slice.bottom = this;
}
previous = slice.bottom;
mHorizontalSlices.add(slice);
}
updateDebugSolverNames();
}
/**
* Set the children constraints (place them in the rows and columns)
*/
private void setChildrenConnections() {
int count = mChildren.size();
int index = 0;
for (int i = 0; i < count; i++) {
ConstraintWidget target = mChildren.get(i);
index += target.getContainerItemSkip();
int col = index % mNumCols;
int row = index / mNumCols;
HorizontalSlice horizontalSlice = mHorizontalSlices.get(row);
VerticalSlice verticalSlice = mVerticalSlices.get(col);
ConstraintWidget targetLeft = verticalSlice.left;
ConstraintWidget targetRight = verticalSlice.right;
ConstraintWidget targetTop = horizontalSlice.top;
ConstraintWidget targetBottom = horizontalSlice.bottom;
target.getAnchor(ConstraintAnchor.Type.LEFT)
.connect(targetLeft.getAnchor(ConstraintAnchor.Type.LEFT), mPadding);
if (targetRight instanceof Guideline) {
target.getAnchor(ConstraintAnchor.Type.RIGHT)
.connect(targetRight.getAnchor(ConstraintAnchor.Type.LEFT), mPadding);
} else {
target.getAnchor(ConstraintAnchor.Type.RIGHT)
.connect(targetRight.getAnchor(ConstraintAnchor.Type.RIGHT), mPadding);
}
// target.getAnchor(ConstraintAnchor.Type.LEFT).setConnectionType(ConstraintAnchor.ConnectionType.STRICT);
// target.getAnchor(ConstraintAnchor.Type.RIGHT).setConnectionType(ConstraintAnchor.ConnectionType.STRICT);
switch (verticalSlice.alignment) {
case ALIGN_FULL: {
target.setHorizontalDimensionBehaviour(DimensionBehaviour.MATCH_CONSTRAINT);
}
break;
case ALIGN_LEFT: {
target.getAnchor(ConstraintAnchor.Type.LEFT).setStrength(
ConstraintAnchor.Strength.STRONG);
target.getAnchor(ConstraintAnchor.Type.RIGHT).setStrength(
ConstraintAnchor.Strength.WEAK);
}
break;
case ALIGN_RIGHT: {
target.getAnchor(ConstraintAnchor.Type.LEFT).setStrength(
ConstraintAnchor.Strength.WEAK);
target.getAnchor(ConstraintAnchor.Type.RIGHT).setStrength(
ConstraintAnchor.Strength.STRONG);
}
break;
}
target.getAnchor(ConstraintAnchor.Type.TOP)
.connect(targetTop.getAnchor(ConstraintAnchor.Type.TOP), mPadding);
if (targetBottom instanceof Guideline) {
target.getAnchor(ConstraintAnchor.Type.BOTTOM)
.connect(targetBottom.getAnchor(ConstraintAnchor.Type.TOP), mPadding);
} else {
target.getAnchor(ConstraintAnchor.Type.BOTTOM)
.connect(targetBottom.getAnchor(ConstraintAnchor.Type.BOTTOM), mPadding);
}
index++;
}
}
/**
* Update the frame of the layout and its children from the solver
*
* @param system the solver we get the values from.
*/
@Override
public void updateFromSolver(LinearSystem system) {
super.updateFromSolver(system);
// We don't want to update guidelines on a different system than our own
if (system == mSystem) {
int num = mVerticalGuidelines.size();
for (int i = 0; i < num; i++) {
Guideline guideline = mVerticalGuidelines.get(i);
guideline.updateFromSolver(system);
}
num = mHorizontalGuidelines.size();
for (int i = 0; i < num; i++) {
Guideline guideline = mHorizontalGuidelines.get(i);
guideline.updateFromSolver(system);
}
}
}
/**
* The table layout manages the positions of its children
*
* @return true
*/
@Override
public boolean handlesInternalConstraints() {
return true;
}
/**
* Recompute the percentage positions of the guidelines given their current position
*/
public void computeGuidelinesPercentPositions() {
int num = mVerticalGuidelines.size();
for (int i = 0; i < num; i++) {
mVerticalGuidelines.get(i).inferRelativePercentPosition();
}
num = mHorizontalGuidelines.size();
for (int i = 0; i < num; i++) {
mHorizontalGuidelines.get(i).inferRelativePercentPosition();
}
}
}