diff options
Diffstat (limited to 'solver/src/main/java/android/support/constraint/solver/widgets/Analyzer.java')
-rw-r--r-- | solver/src/main/java/android/support/constraint/solver/widgets/Analyzer.java | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/Analyzer.java b/solver/src/main/java/android/support/constraint/solver/widgets/Analyzer.java new file mode 100644 index 0000000..1f48990 --- /dev/null +++ b/solver/src/main/java/android/support/constraint/solver/widgets/Analyzer.java @@ -0,0 +1,550 @@ +/* + * 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; + } +} |