blob: e0c916fa4525a8ef6426928f7f8fd337da8a6b33 [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;
import java.util.Arrays;
/**
* A container of ConstraintWidget that can layout its children
*/
public class ConstraintWidgetContainer extends WidgetContainer {
private static final boolean USE_THREAD = false;
private static final boolean DEBUG = false;
private static final boolean USE_SNAPSHOT = true;
protected LinearSystem mSystem = new LinearSystem();
protected LinearSystem mBackgroundSystem = null;
private Snapshot mSnapshot;
static boolean ALLOW_ROOT_GROUP = true;
int mWrapWidth;
int mWrapHeight;
int mPaddingLeft;
int mPaddingTop;
int mPaddingRight;
int mPaddingBottom;
/*-----------------------------------------------------------------------*/
// Construction
/*-----------------------------------------------------------------------*/
/**
* Default constructor
*/
public ConstraintWidgetContainer() {
}
/**
* Constructor
*
* @param x x position
* @param y y position
* @param width width of the layout
* @param height height of the layout
*/
public ConstraintWidgetContainer(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 ConstraintWidgetContainer(int width, int height) {
super(width, height);
}
/**
* Specify the xml type for the container
*
* @return
*/
@Override
public String getType() {
return "ConstraintLayout";
}
@Override
public void reset() {
mSystem.reset();
if (USE_THREAD && mBackgroundSystem != null) {
mBackgroundSystem.reset();
}
mPaddingLeft = 0;
mPaddingRight = 0;
mPaddingTop = 0;
mPaddingBottom = 0;
super.reset();
}
/**
* Set a new ConstraintWidgetContainer containing the list of supplied
* children. The dimensions of the container will be the bounding box
* containing all the children.
*
* @param container the container instance
* @param name the name / id of the container
* @param widgets the list of widgets we want to move inside the container
* @param padding if padding > 0, the container returned will be enlarged by this amount
* @return
*/
public static ConstraintWidgetContainer createContainer(ConstraintWidgetContainer
container, String name, ArrayList<ConstraintWidget> widgets, int padding) {
Rectangle bounds = getBounds(widgets);
if (bounds.width == 0 || bounds.height == 0) {
return null;
}
if (padding > 0) {
int maxPadding = Math.min(bounds.x, bounds.y);
if (padding > maxPadding) {
padding = maxPadding;
}
bounds.grow(padding, padding);
}
container.setOrigin(bounds.x, bounds.y);
container.setDimension(bounds.width, bounds.height);
container.setDebugName(name);
ConstraintWidget parent = widgets.get(0).getParent();
for (int i = 0, widgetsSize = widgets.size(); i < widgetsSize; i++) {
final ConstraintWidget widget = widgets.get(i);
if (widget.getParent() != parent) {
continue; // only allow widgets sharing a parent to be counted
}
container.add(widget);
widget.setX(widget.getX() - bounds.x);
widget.setY(widget.getY() - bounds.y);
}
return container;
}
/*-----------------------------------------------------------------------*/
// Overloaded methods from ConstraintWidget
/*-----------------------------------------------------------------------*/
/**
* Add this widget to the solver
*
* @param system the solver we want to add the widget to
*/
public void addChildrenToSolver(LinearSystem system, int group) {
addToSolver(system, group);
final int count = mChildren.size();
for (int i = 0; i < count; i++) {
ConstraintWidget widget = mChildren.get(i);
if (widget instanceof ConstraintWidgetContainer) {
DimensionBehaviour horizontalBehaviour = widget.getHorizontalDimensionBehaviour();
DimensionBehaviour verticalBehaviour = widget.getVerticalDimensionBehaviour();
if (horizontalBehaviour == DimensionBehaviour.WRAP_CONTENT) {
widget.setHorizontalDimensionBehaviour(DimensionBehaviour.FIXED);
}
if (verticalBehaviour == DimensionBehaviour.WRAP_CONTENT) {
widget.setVerticalDimensionBehaviour(DimensionBehaviour.FIXED);
}
widget.addToSolver(system, group);
if (horizontalBehaviour == DimensionBehaviour.WRAP_CONTENT) {
widget.setHorizontalDimensionBehaviour(horizontalBehaviour);
}
if (verticalBehaviour == DimensionBehaviour.WRAP_CONTENT) {
widget.setVerticalDimensionBehaviour(verticalBehaviour);
}
} else {
widget.addToSolver(system, group);
}
}
}
/**
* Update the frame of the layout and its children from the solver
*
* @param system the solver we get the values from.
*/
public void updateChildrenFromSolver(LinearSystem system, int group) {
updateFromSolver(system, group);
final int count = mChildren.size();
for (int i = 0; i < count; i++) {
ConstraintWidget widget = mChildren.get(i);
widget.updateFromSolver(system, group);
}
}
/**
* Set the padding on this container. It will apply to the position of the children.
*
* @param left left padding
* @param top top padding
* @param right right padding
* @param bottom bottom padding
*/
public void setPadding(int left, int top, int right, int bottom) {
mPaddingLeft = left;
mPaddingTop = top;
mPaddingRight = right;
mPaddingBottom = bottom;
}
/**
* Layout the tree of widgets
*/
@Override
public void layout() {
int prex = mX;
int prey = mY;
int prew = getWidth();
int preh = getHeight();
if (mParent != null && USE_SNAPSHOT) {
if (mSnapshot == null) {
mSnapshot = new Snapshot(this);
}
mSnapshot.updateFrom(this);
// We clear ourselves of external anchors as
// well as repositioning us to (0, 0)
// before inserting us in the solver, so that our
// children's positions get computed relative to us.
setX(mPaddingLeft);
setY(mPaddingTop);
resetAnchors();
resetSolverVariables(mSystem.getCache());
} else {
mX = 0;
mY = 0;
}
// Before we solve our system, we should call layout() on any
// of our children that is a container.
final int count = mChildren.size();
for (int i = 0; i < count; i++) {
ConstraintWidget widget = mChildren.get(i);
if (widget instanceof WidgetContainer) {
((WidgetContainer) widget).layout();
}
}
// Now let's solve our system as usual
try {
mSystem.reset();
if (DEBUG) {
setDebugSolverName(mSystem, getDebugName());
for (int i = 0; i < count; i++) {
ConstraintWidget widget = mChildren.get(i);
if (widget.getDebugName() != null) {
widget.setDebugSolverName(mSystem, widget.getDebugName());
}
}
}
addChildrenToSolver(mSystem, ConstraintAnchor.ANY_GROUP);
mSystem.minimize();
} catch (Exception e) {
e.printStackTrace();
}
updateChildrenFromSolver(mSystem, ConstraintAnchor.ANY_GROUP);
if (mParent != null && USE_SNAPSHOT) {
int width = getWidth();
int height = getHeight();
// Let's restore our state...
mSnapshot.applyTo(this);
setWidth(width + mPaddingLeft + mPaddingRight);
setHeight(height + mPaddingTop + mPaddingBottom);
} else {
mX = prex;
mY = prey;
setWidth(prew);
setHeight(preh);
}
resetSolverVariables(mSystem.getCache());
if (this == getRootConstraintContainer()) {
updateDrawPosition();
}
}
/**
* set the anchor to the group value if it less than my current group value
* True if i was able to set it.
* recurse to other if you were set.
*
* @param anchor
* @return
*/
static int setGroup(ConstraintAnchor anchor, int group) {
int oldGroup = anchor.mGroup;
if (anchor.mOwner.getParent() == null) {
return group;
}
if (oldGroup <= group) {
return oldGroup;
}
anchor.mGroup = group;
ConstraintAnchor opposite = anchor.getOpposite();
ConstraintAnchor target = anchor.mTarget;
group = (opposite != null) ? setGroup(opposite, group) : group;
group = (target != null) ? setGroup(target, group) : group;
group = (opposite != null) ? setGroup(opposite, group) : group;
anchor.mGroup = group;
return group;
}
public int layoutFindGroupsSimple() {
final int size = mChildren.size();
for (int j = 0; j < size; j++) {
ConstraintWidget widget = mChildren.get(j);
widget.mLeft.mGroup = 0;
widget.mRight.mGroup = 0;
widget.mTop.mGroup = 1;
widget.mBottom.mGroup = 1;
widget.mBaseline.mGroup = 1;
}
return 2;
}
/**
* This recursively walks the tree of connected components
* calculating there distance to the left,right,top and bottom
* @param widget
*/
public void findWrapRecursive(ConstraintWidget widget) {
int w = widget.getWrapWidth();
int distToRight = w;
int distToLeft = w;
ConstraintWidget leftWidget = null;
ConstraintWidget rightWidget = null;
widget.mVisited = true;
if (!(widget.mRight.isConnected() || (widget.mLeft.isConnected()))) {
distToLeft += widget.getX();
} else {
if (widget.mRight.mTarget != null) {
rightWidget = widget.mRight.mTarget.getOwner();
distToRight += widget.mRight.getMargin();
if (!rightWidget.isRoot() && !rightWidget.mVisited) {
findWrapRecursive(rightWidget);
}
}
if (widget.mLeft.isConnected()) {
leftWidget = widget.mLeft.mTarget.getOwner();
distToLeft += widget.mLeft.getMargin();
if (!leftWidget.isRoot() && !leftWidget.mVisited) {
findWrapRecursive(leftWidget);
}
}
if (widget.mRight.mTarget != null && !rightWidget.isRoot()) {
if (widget.mRight.mTarget.mType == ConstraintAnchor.Type.RIGHT) {
distToRight += rightWidget.mDistToRight - rightWidget.getWrapWidth();
} else if (widget.mRight.mTarget.getType() == ConstraintAnchor.Type.LEFT) {
distToRight += rightWidget.mDistToRight;
}
}
if (widget.mLeft.mTarget != null && !leftWidget.isRoot()) {
if (widget.mLeft.mTarget.getType() == ConstraintAnchor.Type.LEFT) {
distToLeft += leftWidget.mDistToLeft - leftWidget.getWrapWidth();
} else if (widget.mLeft.mTarget.getType() == ConstraintAnchor.Type.RIGHT) {
distToLeft += leftWidget.mDistToLeft;
}
}
}
widget.mDistToLeft = distToLeft;
widget.mDistToRight = distToRight;
// VERTICAL
int h = widget.getWrapHeight();
int distToTop = h;
int distToBottom = h;
ConstraintWidget topWidget = null;
if (!(widget.mBaseline.mTarget != null || widget.mTop.mTarget != null || widget.mBottom.mTarget != null)) {
distToTop += widget.getY();
} else {
if (widget.mBaseline.isConnected()) {
ConstraintWidget baseLineWidget = widget.mBaseline.mTarget.getOwner();
if (!baseLineWidget.mVisited) {
findWrapRecursive(baseLineWidget);
}
if (baseLineWidget.mDistToBottom > distToBottom) {
distToBottom = baseLineWidget.mDistToBottom;
}
if (baseLineWidget.mDistToTop > distToTop) {
distToTop = baseLineWidget.mDistToTop;
}
widget.mDistToTop = distToTop;
widget.mDistToBottom = distToBottom;
return; // if baseline connected no need to look at top or bottom
}
if (widget.mTop.isConnected()) {
topWidget = widget.mTop.mTarget.getOwner();
distToTop += widget.mTop.getMargin();
if (!topWidget.isRoot() && !topWidget.mVisited) {
findWrapRecursive(topWidget);
}
}
ConstraintWidget bottomWidget = null;
if (widget.mBottom.isConnected()) {
bottomWidget = widget.mBottom.mTarget.getOwner();
distToBottom += widget.mBottom.getMargin();
if (!bottomWidget.isRoot() && !bottomWidget.mVisited) {
findWrapRecursive(bottomWidget);
}
}
// TODO add center connection logic
if (widget.mTop.mTarget != null && !topWidget.isRoot()) {
if (widget.mTop.mTarget.getType() == ConstraintAnchor.Type.TOP) {
distToTop += topWidget.mDistToTop - topWidget.getWrapHeight();
} else if (widget.mTop.mTarget.getType() == ConstraintAnchor.Type.BOTTOM) {
distToTop += topWidget.mDistToTop;
}
}
if (widget.mBottom.mTarget != null && !bottomWidget.isRoot()) {
if (widget.mBottom.mTarget.getType() == ConstraintAnchor.Type.BOTTOM) {
distToBottom += bottomWidget.mDistToBottom - bottomWidget.getWrapHeight();
} else if (widget.mBottom.mTarget.getType() == ConstraintAnchor.Type.TOP) {
distToBottom += bottomWidget.mDistToBottom;
}
}
}
widget.mDistToTop = distToTop;
widget.mDistToBottom = distToBottom;
}
/**
* calculates the wrapContent size.
*
* @param children
*/
public void findWrapSize(ArrayList<ConstraintWidget> children) {
int maxTopDist = 0;
int maxLeftDist = 0;
int maxRightDist = 0;
int maxBottomDist = 0;
int maxConnectWidth = 0;
int maxConnectHeight = 0;
final int size = children.size();
for (int j = 0; j < size; j++) {
ConstraintWidget widget = children.get(j);
if (widget.isRoot()) {
continue;
}
if (!widget.mVisited) {
findWrapRecursive(widget);
}
int connectWidth = widget.mDistToLeft + widget.mDistToRight - widget.getWrapWidth();
int connectHeight = widget.mDistToTop + widget.mDistToBottom - widget.getWrapHeight();
maxLeftDist = Math.max(maxLeftDist, widget.mDistToLeft);
maxRightDist = Math.max(maxRightDist, widget.mDistToRight);
maxBottomDist = Math.max(maxBottomDist, widget.mDistToBottom);
maxTopDist = Math.max(maxTopDist, widget.mDistToTop);
maxConnectWidth = Math.max(maxConnectWidth, connectWidth);
maxConnectHeight = Math.max(maxConnectHeight, connectHeight);
}
int max = Math.max(maxLeftDist, maxRightDist);
mWrapWidth = Math.max(max, maxConnectWidth);
max = Math.max(maxTopDist, maxBottomDist);
mWrapHeight = Math.max(max, maxConnectHeight);
for (int j = 0; j < size; j++) {
children.get(j).mVisited = false;
}
}
/**
* Find groups
*/
public int layoutFindGroups() {
ConstraintAnchor.Type[] dir = {
ConstraintAnchor.Type.LEFT, ConstraintAnchor.Type.RIGHT, ConstraintAnchor.Type.TOP,
ConstraintAnchor.Type.BASELINE, ConstraintAnchor.Type.BOTTOM
};
int label = 1;
final int size = mChildren.size();
for (int j = 0; j < size; j++) {
ConstraintWidget widget = mChildren.get(j);
ConstraintAnchor anchor = null;
anchor = widget.mLeft;
if (anchor.mTarget != null) {
if (setGroup(anchor, label) == label) {
label++;
}
} else {
anchor.mGroup = ConstraintAnchor.ANY_GROUP;
}
anchor = widget.mTop;
if (anchor.mTarget != null) {
if (setGroup(anchor, label) == label) {
label++;
}
} else {
anchor.mGroup = ConstraintAnchor.ANY_GROUP;
}
anchor = widget.mRight;
if (anchor.mTarget != null) {
if (setGroup(anchor, label) == label) {
label++;
}
} else {
anchor.mGroup = ConstraintAnchor.ANY_GROUP;
}
anchor = widget.mBottom;
if (anchor.mTarget != null) {
if (setGroup(anchor, label) == label) {
label++;
}
} else {
anchor.mGroup = ConstraintAnchor.ANY_GROUP;
}
anchor = widget.mBaseline;
if (anchor.mTarget != null) {
if (setGroup(anchor, label) == label) {
label++;
}
} else {
anchor.mGroup = ConstraintAnchor.ANY_GROUP;
}
}
boolean notDone = true;
int count = 0;
int fix = 0;
// This cleans up the misses of the previous step
// It is a brute force algorithm that is related to bubble sort O(N*Log(N))
while (notDone) {
notDone = false;
count++;
for (int j = 0; j < size; j++) {
ConstraintWidget widget = mChildren.get(j);
for (int i = 0; i < dir.length; i++) {
ConstraintAnchor.Type type = dir[i];
ConstraintAnchor anchor = null;
switch (type) {
case LEFT: {
anchor = widget.mLeft;
}
break;
case TOP: {
anchor = widget.mTop;
}
break;
case RIGHT: {
anchor = widget.mRight;
}
break;
case BOTTOM: {
anchor = widget.mBottom;
}
break;
case BASELINE: {
anchor = widget.mBaseline;
}
break;
}
ConstraintAnchor target = anchor.mTarget;
if (target == null) {
continue;
}
if (target.mOwner.getParent() != null && target.mGroup != anchor.mGroup) {
target.mGroup = anchor.mGroup = (anchor.mGroup > target.mGroup) ? target.mGroup : anchor.mGroup;
fix++;
notDone = true;
}
ConstraintAnchor opposite = target.getOpposite();
if (opposite != null && opposite.mGroup != anchor.mGroup) {
opposite.mGroup = anchor.mGroup = (anchor.mGroup > opposite.mGroup) ? opposite.mGroup : anchor.mGroup;
fix++;
notDone = true;
}
}
}
}
// This remaps the groups to a compact range
int index = 0;
int[] table = new int[mChildren.size() * dir.length + 1];
Arrays.fill(table, -1);
for (int j = 0; j < size; j++) {
ConstraintWidget widget = mChildren.get(j);
ConstraintAnchor anchor = null;
anchor = widget.mLeft;
if (anchor.mGroup != ConstraintAnchor.ANY_GROUP) {
int g = anchor.mGroup;
if (table[g] == -1) {
table[g] = index++;
}
anchor.mGroup = table[g];
}
anchor = widget.mTop;
if (anchor.mGroup != ConstraintAnchor.ANY_GROUP) {
int g = anchor.mGroup;
if (table[g] == -1) {
table[g] = index++;
}
anchor.mGroup = table[g];
}
anchor = widget.mRight;
if (anchor.mGroup != ConstraintAnchor.ANY_GROUP) {
int g = anchor.mGroup;
if (table[g] == -1) {
table[g] = index++;
}
anchor.mGroup = table[g];
}
anchor = widget.mBottom;
if (anchor.mGroup != ConstraintAnchor.ANY_GROUP) {
int g = anchor.mGroup;
if (table[g] == -1) {
table[g] = index++;
}
anchor.mGroup = table[g];
}
anchor = widget.mBaseline;
if (anchor.mGroup != ConstraintAnchor.ANY_GROUP) {
int g = anchor.mGroup;
if (table[g] == -1) {
table[g] = index++;
}
anchor.mGroup = table[g];
}
}
return index;
}
/**
* Layout by groups
*/
public void layoutWithGroup(int numOfGroups) {
int prex = mX;
int prey = mY;
if (mParent != null && USE_SNAPSHOT) {
if (mSnapshot == null) {
mSnapshot = new Snapshot(this);
}
mSnapshot.updateFrom(this);
// We clear ourselves of external anchors as
// well as repositioning us to (0, 0)
// before inserting us in the solver, so that our
// children's positions get computed relative to us.
mX = 0;
mY = 0;
resetAnchors();
resetSolverVariables(mSystem.getCache());
} else {
mX = 0;
mY = 0;
}
// Before we solve our system, we should call layout() on any
// of our children that is a container.
final int count = mChildren.size();
for (int i = 0; i < count; i++) {
ConstraintWidget widget = mChildren.get(i);
if (widget instanceof WidgetContainer) {
((WidgetContainer) widget).layout();
}
}
mLeft.mGroup = 0;
mRight.mGroup = 0;
mTop.mGroup = 1;
mBottom.mGroup = 1;
mSystem.reset();
if (USE_THREAD) {
if (mBackgroundSystem == null) {
mBackgroundSystem = new LinearSystem();
} else {
mBackgroundSystem.reset();
}
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
addToSolver(mBackgroundSystem, 1);
mBackgroundSystem.minimize();
updateFromSolver(mBackgroundSystem, 1);
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
try {
addToSolver(mSystem, 0);
mSystem.minimize();
updateFromSolver(mSystem, 0);
} catch (Exception e) {
e.printStackTrace();
}
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
updateFromSolver(mSystem, ConstraintAnchor.APPLY_GROUP_RESULTS);
} else {
for (int i = 0; i < numOfGroups; i++) {
try {
addToSolver(mSystem, i);
mSystem.minimize();
updateFromSolver(mSystem, i);
} catch (Exception e) {
e.printStackTrace();
}
updateFromSolver(mSystem, ConstraintAnchor.APPLY_GROUP_RESULTS);
}
}
if (mParent != null && USE_SNAPSHOT) {
int width = getWidth();
int height = getHeight();
// Let's restore our state...
mSnapshot.applyTo(this);
setWidth(width);
setHeight(height);
} else {
mX = prex;
mY = prey;
}
if (this == getRootConstraintContainer()) {
updateDrawPosition();
}
}
/**
* Returns true if the widget is animating
*
* @return
*/
@Override
public boolean isAnimating() {
if (super.isAnimating()) {
return true;
}
for (int i = 0, mChildrenSize = mChildren.size(); i < mChildrenSize; i++) {
final ConstraintWidget widget = mChildren.get(i);
if (widget.isAnimating()) {
return true;
}
}
return false;
}
/**
* Indicates if the container knows how to layout its content on its own
*
* @return true if the container does the layout, false otherwise
*/
public boolean handlesInternalConstraints() {
return false;
}
/*-----------------------------------------------------------------------*/
// Guidelines
/*-----------------------------------------------------------------------*/
/**
* Accessor to the vertical guidelines contained in the table.
*
* @return array of guidelines
*/
public ArrayList<Guideline> getVerticalGuidelines() {
ArrayList<Guideline> guidelines = new ArrayList<>();
for (int i = 0, mChildrenSize = mChildren.size(); i < mChildrenSize; i++) {
final ConstraintWidget widget = mChildren.get(i);
if (widget instanceof Guideline) {
Guideline guideline = (Guideline) widget;
if (guideline.getOrientation() == Guideline.VERTICAL) {
guidelines.add(guideline);
}
}
}
return guidelines;
}
/**
* Accessor to the horizontal guidelines contained in the table.
*
* @return array of guidelines
*/
public ArrayList<Guideline> getHorizontalGuidelines() {
ArrayList<Guideline> guidelines = new ArrayList<>();
for (int i = 0, mChildrenSize = mChildren.size(); i < mChildrenSize; i++) {
final ConstraintWidget widget = mChildren.get(i);
if (widget instanceof Guideline) {
Guideline guideline = (Guideline) widget;
if (guideline.getOrientation() == Guideline.HORIZONTAL) {
guidelines.add(guideline);
}
}
}
return guidelines;
}
public LinearSystem getSystem() {
return mSystem;
}
}