blob: 1f48990ff7fbd19f371b58bbc753edf4306d4e7b [file] [log] [blame]
/*
* Copyright (C) 2018 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.widgets.ConstraintWidget.DimensionBehaviour;
import java.util.ArrayList;
import java.util.List;
/**
* Class to do widget constraints analysis.
* <p>
* Identify groups of widgets independent from each other.
* TODO: Identify Chains here instead.
*/
public class Analyzer {
private Analyzer() {
}
/**
* Find groups of constrained widgets.
* <p>
* Used to simplify the resolution process to layout the widgets when using optimizations.
* Wrap_content layouts require measuring the final size, groups are identified when
* the layout can be measured.
*
* @param layoutWidget Layout to analyze.
*/
public static void determineGroups(ConstraintWidgetContainer layoutWidget) {
if ((layoutWidget.getOptimizationLevel() & Optimizer.OPTIMIZATION_GROUPS) != Optimizer.OPTIMIZATION_GROUPS) {
singleGroup(layoutWidget);
return;
}
layoutWidget.mSkipSolver = true;
layoutWidget.mGroupsWrapOptimized = false;
layoutWidget.mHorizontalWrapOptimized = false;
layoutWidget.mVerticalWrapOptimized = false;
final List<ConstraintWidget> widgets = layoutWidget.mChildren;
final List<ConstraintWidgetGroup> widgetGroups = layoutWidget.mWidgetGroups;
boolean horizontalWrapContent = layoutWidget.getHorizontalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT;
boolean verticalWrapContent = layoutWidget.getVerticalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT;
boolean hasWrapContent = horizontalWrapContent || verticalWrapContent;
widgetGroups.clear();
for (ConstraintWidget widget : widgets) {
widget.mBelongingGroup = null;
widget.mGroupsToSolver = false;
widget.resetResolutionNodes();
}
for (ConstraintWidget widget : widgets) {
if (widget.mBelongingGroup == null) {
if (!determineGroups(widget, widgetGroups, hasWrapContent)) {
singleGroup(layoutWidget);
layoutWidget.mSkipSolver = false;
return;
}
}
}
int measuredWidth = 0;
int measuredHeight = 0;
// Resolve solvable widgets.
for (ConstraintWidgetGroup group : widgetGroups) {
measuredWidth = Math.max(measuredWidth,
getMaxDimension(group, ConstraintWidget.HORIZONTAL));
measuredHeight = Math.max(measuredHeight,
getMaxDimension(group, ConstraintWidget.VERTICAL));
}
// Change container to fixed and set resolved dimensions.
if (horizontalWrapContent) {
layoutWidget.setHorizontalDimensionBehaviour(DimensionBehaviour.FIXED);
layoutWidget.setWidth(measuredWidth);
layoutWidget.mGroupsWrapOptimized = true;
layoutWidget.mHorizontalWrapOptimized = true;
layoutWidget.mWrapFixedWidth = measuredWidth;
}
if (verticalWrapContent) {
layoutWidget.setVerticalDimensionBehaviour(DimensionBehaviour.FIXED);
layoutWidget.setHeight(measuredHeight);
layoutWidget.mGroupsWrapOptimized = true;
layoutWidget.mVerticalWrapOptimized = true;
layoutWidget.mWrapFixedHeight = measuredHeight;
}
setPosition(widgetGroups, ConstraintWidget.HORIZONTAL, layoutWidget.getWidth());
setPosition(widgetGroups, ConstraintWidget.VERTICAL, layoutWidget.getHeight());
}
/**
* @param widget Widget being traversed.
* @param widgetGroups Starting list to contain the widgets in this group.
* @param hasWrapContent Indicating if any dimension of the parent is in wrap_content.
* @return False if the group can't be optimized in any way.
*/
private static boolean determineGroups(ConstraintWidget widget,
List<ConstraintWidgetGroup> widgetGroups, boolean hasWrapContent) {
ConstraintWidgetGroup traverseList = new ConstraintWidgetGroup(new ArrayList<ConstraintWidget>(), true);
widgetGroups.add(traverseList);
return traverse(widget, traverseList, widgetGroups, hasWrapContent);
}
/**
* Recursive function to traverse constrained widgets.
* The objective is to maintain in a single list all the widgets that can be reached through
* their constraints except for their parent.
*
* @param widget Widget being traversed.
* @param upperGroup List being passed down, originally by {@link #determineGroups(ConstraintWidget, List, boolean)}.
* @param widgetGroups List of widget groups identified.
* @param hasWrapContent Indicates if the layout has any dimension as wrap_content.
* @return If the group analysis failed or can't be done.
*/
private static boolean traverse(ConstraintWidget widget, ConstraintWidgetGroup upperGroup,
List<ConstraintWidgetGroup> widgetGroups, boolean hasWrapContent) {
if (widget == null) {
return true;
}
widget.mOptimizerMeasured = false;
ConstraintWidgetContainer layoutWidget = (ConstraintWidgetContainer) widget.getParent();
if (widget.mBelongingGroup == null) {
// If it hasn't been assigned to a group.
widget.mOptimizerMeasurable = true;
upperGroup.mConstrainedGroup.add(widget);
widget.mBelongingGroup = upperGroup;
// Determine if group is measurable.
if (widget.mLeft.mTarget == null
&& widget.mRight.mTarget == null
&& widget.mTop.mTarget == null
&& widget.mBottom.mTarget == null
&& widget.mBaseline.mTarget == null
&& widget.mCenter.mTarget == null) {
invalidate(layoutWidget, widget, upperGroup);
if (hasWrapContent) {
return false;
}
}
// Check if it has vertical bias.
if (widget.mTop.mTarget != null && widget.mBottom.mTarget != null) {
// Allow if it has no wrap content in that dimension an constrained to the parent.
boolean wrap = layoutWidget.getVerticalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT;
if (hasWrapContent) {
invalidate(layoutWidget, widget, upperGroup);
return false;
} else if (!(widget.mTop.mTarget.mOwner == widget.getParent()
&& widget.mBottom.mTarget.mOwner == widget.getParent())) {
invalidate(layoutWidget, widget, upperGroup);
}
}
// Check if it has horizontal bias.
if (widget.mLeft.mTarget != null && widget.mRight.mTarget != null) {
// Allow if it has no wrap content in that dimension an constrained to the parent.
boolean wrap = layoutWidget.getHorizontalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT;
if (hasWrapContent) {
invalidate(layoutWidget, widget, upperGroup);
return false;
} else if (!(widget.mLeft.mTarget.mOwner == widget.getParent()
&& widget.mRight.mTarget.mOwner == widget.getParent())) {
invalidate(layoutWidget, widget, upperGroup);
}
}
if ((widget.getHorizontalDimensionBehaviour() == DimensionBehaviour.MATCH_CONSTRAINT
^ widget.getVerticalDimensionBehaviour() == DimensionBehaviour.MATCH_CONSTRAINT)
&& widget.mDimensionRatio != 0.0f) {
// Calculate dimension.
resolveDimensionRatio(widget);
} else if (!(widget.getHorizontalDimensionBehaviour() != DimensionBehaviour.MATCH_CONSTRAINT
&& widget.getVerticalDimensionBehaviour() != DimensionBehaviour.MATCH_CONSTRAINT)) {
invalidate(layoutWidget, widget, upperGroup);
if (hasWrapContent) {
return false;
}
}
// Is Horizontal start
if (((widget.mLeft.mTarget == null && widget.mRight.mTarget == null)
|| (widget.mLeft.mTarget != null && widget.mLeft.mTarget.mOwner == widget.mParent && widget.mRight.mTarget == null)
|| (widget.mRight.mTarget != null && widget.mRight.mTarget.mOwner == widget.mParent && widget.mLeft.mTarget == null)
|| (widget.mLeft.mTarget != null && widget.mLeft.mTarget.mOwner == widget.mParent
&& widget.mRight.mTarget != null && widget.mRight.mTarget.mOwner == widget.mParent))
&& (widget.mCenter.mTarget == null)) {
if (!(widget instanceof Guideline) && !(widget instanceof Helper)) {
upperGroup.mStartHorizontalWidgets.add(widget);
}
}
// Is Vertical start
if (((widget.mTop.mTarget == null && widget.mBottom.mTarget == null)
|| (widget.mTop.mTarget != null && widget.mTop.mTarget.mOwner == widget.mParent && widget.mBottom.mTarget == null)
|| (widget.mBottom.mTarget != null && widget.mBottom.mTarget.mOwner == widget.mParent && widget.mTop.mTarget == null)
|| (widget.mTop.mTarget != null && widget.mTop.mTarget.mOwner == widget.mParent
&& widget.mBottom.mTarget != null && widget.mBottom.mTarget.mOwner == widget.mParent))
&& (widget.mCenter.mTarget == null && widget.mBaseline.mTarget == null)) {
if (!(widget instanceof Guideline) && !(widget instanceof Helper)) {
upperGroup.mStartVerticalWidgets.add(widget);
}
}
} else {
// If it has, join the list and re-assign. Remove joint list from mWidgetGroups (if its a different list)
if (widget.mBelongingGroup != upperGroup) {
upperGroup.mConstrainedGroup.addAll(widget.mBelongingGroup.mConstrainedGroup);
upperGroup.mStartHorizontalWidgets.addAll(widget.mBelongingGroup.mStartHorizontalWidgets);
upperGroup.mStartVerticalWidgets.addAll(widget.mBelongingGroup.mStartVerticalWidgets);
if (widget.mBelongingGroup.mSkipSolver == false) {
upperGroup.mSkipSolver = false;
}
widgetGroups.remove(widget.mBelongingGroup);
for (ConstraintWidget auxWidget : widget.mBelongingGroup.mConstrainedGroup) {
auxWidget.mBelongingGroup = upperGroup;
}
}
return true;
}
// Proceed to traverse widgets, start with HelperWidgets since they contain multiple widgets.
if (widget instanceof Helper) {
invalidate(layoutWidget, widget, upperGroup);
if (hasWrapContent) {
return false;
}
final Helper hWidget = (Helper) widget;
for (int widgetsCount = 0; widgetsCount < hWidget.mWidgetsCount; widgetsCount++) {
if (!traverse(hWidget.mWidgets[widgetsCount], upperGroup, widgetGroups, hasWrapContent)) {
return false;
}
}
}
// We traverse every anchor, for wrap_content we ignore center (circular constraints).
final int anchorsSize = widget.mListAnchors.length;
for (int i = 0; i < anchorsSize; i++) {
final ConstraintAnchor anchor = widget.mListAnchors[i];
if (anchor.mTarget != null && anchor.mTarget.mOwner != widget.getParent()) {
if (anchor.mType == ConstraintAnchor.Type.CENTER) {
invalidate(layoutWidget, widget, upperGroup);
if (hasWrapContent) {
return false;
}
} else {
setConnection(anchor);
}
if (!traverse(anchor.mTarget.mOwner, upperGroup, widgetGroups, hasWrapContent)) {
return false;
}
}
}
return true;
}
private static void invalidate(ConstraintWidgetContainer layoutWidget, ConstraintWidget widget, ConstraintWidgetGroup group) {
group.mSkipSolver = false;
layoutWidget.mSkipSolver = false;
widget.mOptimizerMeasurable = false;
}
/**
* Obtain the max length of a {@link ConstraintWidgetGroup} on a specific orientation.
* Length is saved on the group for future use as well.
*
* @param group Group of widgets being measured.
* @param orientation Orientation being measured.
* @return Max dimension in the group.
*/
private static int getMaxDimension(ConstraintWidgetGroup group, int orientation) {
int dimension = 0;
int offset = orientation * 2;
List<ConstraintWidget> startWidgets = group.getStartWidgets(orientation);
final int size = startWidgets.size();
for (int i = 0; i < size; i++) {
ConstraintWidget widget = startWidgets.get(i);
boolean topLeftFlow = widget.mListAnchors[offset + 1].mTarget == null
|| (widget.mListAnchors[offset].mTarget != null
&& widget.mListAnchors[offset + 1].mTarget != null);
dimension = Math.max(dimension, getMaxDimensionTraversal(widget, orientation, topLeftFlow, 0));
}
group.mGroupDimensions[orientation] = dimension;
return dimension;
}
/**
* Traverse from a widget at the start of a tree (a widget constrained to any side of their parent),
* find the maximum length of the tree.
* Avoids cases when a widget's dimension shouldn't be considered.
*
* @param widget Widget being traversed.
* @param orientation Dimension being measured (HORIZONTAL/VERTICAL).
* @param topLeftFlow Indicates if the tree starts at the top or left of the container.
* @param depth How far the widget is from the start of the tree.
* @return Max dimension from the widget being traversed.
*/
private static int getMaxDimensionTraversal(ConstraintWidget widget, int orientation, boolean topLeftFlow, int depth) {
// Start and end offset used to point to the correct anchors according to the flow
// of the widget at the start of the tree.
if (!widget.mOptimizerMeasurable) {
return 0;
}
int startOffset;
int endOffset;
int dimension = 0;
int dimensionPre = 0;
int dimensionPost = 0;
final int flow;
final int baselinePreDistance;
final int baselinePostDistance;
// If it has baseline, the dimensions change, despite maintaining the flow.
final boolean hasBaseline = widget.mBaseline.mTarget != null && orientation == ConstraintWidget.VERTICAL;
if (topLeftFlow) {
baselinePreDistance = widget.getBaselineDistance();
baselinePostDistance = widget.getHeight() - widget.getBaselineDistance();
startOffset = orientation * 2;
endOffset = startOffset + 1;
} else {
baselinePreDistance = widget.getHeight() - widget.getBaselineDistance();
baselinePostDistance = widget.getBaselineDistance();
endOffset = orientation * 2;
startOffset = endOffset + 1;
}
// Define the correct flow of direction. left -> right or left <- right.
// If the flow is going opposite from the startWidget, lengths and margin subtract.
if (widget.mListAnchors[endOffset].mTarget != null && widget.mListAnchors[startOffset].mTarget == null) {
flow = -1;
int aux = startOffset;
startOffset = endOffset;
endOffset = aux;
} else {
flow = 1;
}
if (hasBaseline) {
depth -= baselinePreDistance;
}
// Get position from horizontal/vertical bias.
dimension = widget.mListAnchors[startOffset].getMargin() * flow + getParentBiasOffset(widget, orientation);
int downDepth = dimension + depth;
int postTemp = ((orientation == ConstraintWidget.HORIZONTAL) ? widget.getWidth() : widget.getHeight()) * flow;
for (ResolutionNode targetNode : widget.mListAnchors[startOffset].getResolutionNode().dependents) {
final ResolutionAnchor anchor = (ResolutionAnchor) targetNode;
dimensionPre = Math.max(dimensionPre, getMaxDimensionTraversal(anchor.myAnchor.mOwner, orientation, topLeftFlow, downDepth));
}
for (ResolutionNode targetNode : widget.mListAnchors[endOffset].getResolutionNode().dependents) {
final ResolutionAnchor anchor = (ResolutionAnchor) targetNode;
dimensionPost = Math.max(dimensionPost, getMaxDimensionTraversal(anchor.myAnchor.mOwner, orientation, topLeftFlow, postTemp + downDepth));
}
if (hasBaseline) {
dimensionPre -= baselinePreDistance;
dimensionPost += baselinePostDistance;
} else {
dimensionPost += ((orientation == ConstraintWidget.HORIZONTAL) ? widget.getWidth() : widget.getHeight()) * flow;
}
// Baseline, only add distance from baseline to bottom instead of entire height.
int dimensionBaseline = 0;
if (orientation == ConstraintWidget.VERTICAL) {
for (ResolutionNode targetNode : widget.mBaseline.getResolutionNode().dependents) {
final ResolutionAnchor anchor = (ResolutionAnchor) targetNode;
if (flow == 1) {
dimensionBaseline = Math.max(dimensionBaseline, getMaxDimensionTraversal(anchor.myAnchor.mOwner, orientation, topLeftFlow, baselinePreDistance + downDepth));
} else {
dimensionBaseline = Math.max(dimensionBaseline, getMaxDimensionTraversal(anchor.myAnchor.mOwner, orientation, topLeftFlow, (baselinePostDistance * flow) + downDepth));
}
}
if (widget.mBaseline.getResolutionNode().dependents.size() > 0 && !hasBaseline) {
if (flow == 1) {
dimensionBaseline += baselinePreDistance;
} else {
dimensionBaseline -= baselinePostDistance;
}
}
}
int distanceBeforeWidget = dimension;
dimension += Math.max(dimensionPre, Math.max(dimensionPost, dimensionBaseline));
int leftTop = depth + distanceBeforeWidget;
int end = leftTop + postTemp;
if (flow == -1) {
int aux = end;
end = leftTop;
leftTop = aux;
}
if (topLeftFlow) {
Optimizer.setOptimizedWidget(widget, orientation, leftTop);
widget.setFrame(leftTop, end, orientation);
} else {
widget.mBelongingGroup.addWidgetsToSet(widget, orientation);
widget.setRelativePositioning(leftTop, orientation);
}
// Assuming widgets with only one dimension on Match_constraint would be measurable.
if (widget.getDimensionBehaviour(orientation) == DimensionBehaviour.MATCH_CONSTRAINT
&& widget.mDimensionRatio != 0.0f) {
widget.mBelongingGroup.addWidgetsToSet(widget, orientation);
}
// Assuming is not measurable when the parent is on wrap_content.
if (widget.mListAnchors[startOffset].mTarget != null
&& widget.mListAnchors[endOffset].mTarget != null) {
final ConstraintWidget parent = widget.getParent();
if (widget.mListAnchors[startOffset].mTarget.mOwner == parent
&& widget.mListAnchors[endOffset].mTarget.mOwner == parent) {
widget.mBelongingGroup.addWidgetsToSet(widget, orientation);
}
}
return dimension;
}
private static void setConnection(ConstraintAnchor originAnchor) {
ResolutionNode originNode = originAnchor.getResolutionNode();
if (originAnchor.mTarget != null && originAnchor.mTarget.mTarget != originAnchor) {
// Go to Owner and add the dependent.
originAnchor.mTarget.getResolutionNode().addDependent(originNode);
}
}
/**
* Used when the Analyzer cannot simplify in independent groups.
* This will make it so all widgets are included in the same group.
*
* @param layoutWidget ConstrainedWidgetContainer being analyzed.
*/
private static void singleGroup(ConstraintWidgetContainer layoutWidget) {
layoutWidget.mWidgetGroups.clear();
layoutWidget.mWidgetGroups.add(0, new ConstraintWidgetGroup(layoutWidget.mChildren));
}
/**
* Update widgets positions.
* Necessary for widgets dependent on the right/bottom side of the Container.
*
* @param groups Groups of widgets being updated.
* @param orientation Dimension to update on the widgets.
* @param containerLength Length of the widget container.
*/
public static void setPosition(List<ConstraintWidgetGroup> groups, int orientation, int containerLength) {
final int groupsSize = groups.size();
for (int i = 0; i < groupsSize; i++) {
ConstraintWidgetGroup group = groups.get(i);
for (ConstraintWidget widget : group.getWidgetsToSet(orientation)) {
// We can only update those that we can measure.
if (widget.mOptimizerMeasurable) {
updateSizeDependentWidgets(widget, orientation, containerLength);
}
}
}
}
/**
* Update the final layout position of widgets that depend on the size of the container.
* Exception for dimension-ratio as a work-around.
*
* @param widget Widget being updated.
* @param orientation Orientation being updated.
* @param containerLength The final container dimension in the orientation.
*/
private static void updateSizeDependentWidgets(ConstraintWidget widget, int orientation, int containerLength) {
final int end;
final int start;
final int offset = orientation * 2;
ConstraintAnchor startAnchor = widget.mListAnchors[offset];
ConstraintAnchor endAnchor = widget.mListAnchors[offset + 1];
boolean hasBias = startAnchor.mTarget != null && endAnchor.mTarget != null;
if (hasBias) {
start = getParentBiasOffset(widget, orientation) + startAnchor.getMargin();
Optimizer.setOptimizedWidget(widget, orientation, start);
return;
}
/*
* ConstraintLayout::internalMeasureChildren() workaround (it would reset the widget's
* dimension even if it was set beforehand).
* It is assumed that the left/top anchor has been resolved. Since only the dimension is being reset.
*/
if (widget.mDimensionRatio != 0.0f && widget.getDimensionBehaviour(orientation) == DimensionBehaviour.MATCH_CONSTRAINT) {
int length = resolveDimensionRatio(widget);
start = (int) widget.mListAnchors[offset].getResolutionNode().resolvedOffset;
end = start + length;
endAnchor.getResolutionNode().resolvedTarget = startAnchor.getResolutionNode();
endAnchor.getResolutionNode().resolvedOffset = length;
endAnchor.getResolutionNode().state = ResolutionNode.RESOLVED;
widget.setFrame(start, end, orientation);
return;
}
end = containerLength - widget.getRelativePositioning(orientation);
start = end - widget.getLength(orientation);
widget.setFrame(start, end, orientation);
Optimizer.setOptimizedWidget(widget, orientation, start);
}
/**
* Get the offset of a widget with bias exclusively with the parent.
* Offset is the distance from the left/top side of the parent to the start of the widget.
*
* @param orientation Orientation for the offset.
* @return The distance from the root based on the bias (does not include margin distance). 0 if it can't be calculated.
*/
private static int getParentBiasOffset(ConstraintWidget widget, int orientation) {
int offset = orientation * 2;
ConstraintAnchor startAnchor = widget.mListAnchors[offset];
ConstraintAnchor endAnchor = widget.mListAnchors[offset + 1];
if (startAnchor.mTarget != null && startAnchor.mTarget.mOwner == widget.mParent
&& endAnchor.mTarget != null && endAnchor.mTarget.mOwner == widget.mParent) {
int length = 0;
int widgetDimension = 0;
float bias = 0.0f;
length = widget.mParent.getLength(orientation);
bias = (orientation == ConstraintWidget.HORIZONTAL) ? widget.mHorizontalBiasPercent :
widget.mVerticalBiasPercent;
widgetDimension = widget.getLength(orientation);
length = length - startAnchor.getMargin() - endAnchor.getMargin();
length = length - widgetDimension;
length = ((int) ((float) length * bias));
return length;
} else {
return 0;
}
}
/**
* Calculate the widget's dimension based on dimension ratio.
*
* @return The dimension calculated.
*/
private static int resolveDimensionRatio(ConstraintWidget widget) {
int length = ConstraintWidget.UNKNOWN;
if (widget.getHorizontalDimensionBehaviour() == DimensionBehaviour.MATCH_CONSTRAINT) {
if (widget.mDimensionRatioSide == ConstraintWidget.HORIZONTAL) {
length = (int) ((float) widget.getHeight() * widget.mDimensionRatio);
} else {
length = (int) ((float) widget.getHeight() / widget.mDimensionRatio);
}
widget.setWidth(length);
} else if (widget.getVerticalDimensionBehaviour() == DimensionBehaviour.MATCH_CONSTRAINT) {
if (widget.mDimensionRatioSide == ConstraintWidget.VERTICAL) {
length = (int) ((float) widget.getWidth() * widget.mDimensionRatio);
} else {
length = (int) ((float) widget.getWidth() / widget.mDimensionRatio);
}
widget.setHeight(length);
}
return length;
}
}