/*
* Copyright (C) 2019 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 androidx.constraintlayout.core.widgets.analyzer;
import androidx.constraintlayout.core.LinearSystem;
import androidx.constraintlayout.core.widgets.Barrier;
import androidx.constraintlayout.core.widgets.ConstraintAnchor;
import androidx.constraintlayout.core.widgets.ConstraintWidget;
import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer;
import androidx.constraintlayout.core.widgets.Guideline;
import androidx.constraintlayout.core.widgets.Helper;
import androidx.constraintlayout.core.widgets.Optimizer;
import androidx.constraintlayout.core.widgets.VirtualLayout;
import java.util.ArrayList;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.GONE;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.HORIZONTAL;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_SPREAD;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_WRAP;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.VERTICAL;
/**
* Implements basic measure for linear resolution
*/
public class BasicMeasure {
private static final boolean DEBUG = false;
private static final int MODE_SHIFT = 30;
public static final int UNSPECIFIED = 0;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
public static final int FIXED = -3;
private final ArrayList<ConstraintWidget> mVariableDimensionsWidgets = new ArrayList<>();
private Measure mMeasure = new Measure();
public void updateHierarchy(ConstraintWidgetContainer layout) {
mVariableDimensionsWidgets.clear();
final int childCount = layout.mChildren.size();
for (int i = 0; i < childCount; i++) {
ConstraintWidget widget = layout.mChildren.get(i);
if (widget.getHorizontalDimensionBehaviour() == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
|| widget.getVerticalDimensionBehaviour() == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT) {
mVariableDimensionsWidgets.add(widget);
}
}
layout.invalidateGraph();
}
private ConstraintWidgetContainer constraintWidgetContainer;
public BasicMeasure(ConstraintWidgetContainer constraintWidgetContainer) {
this.constraintWidgetContainer = constraintWidgetContainer;
}
private void measureChildren(ConstraintWidgetContainer layout) {
final int childCount = layout.mChildren.size();
boolean optimize = layout.optimizeFor(Optimizer.OPTIMIZATION_GRAPH);
Measurer measurer = layout.getMeasurer();
for (int i = 0; i < childCount; i++) {
ConstraintWidget child = layout.mChildren.get(i);
if (child instanceof Guideline) {
continue;
}
if (child instanceof Barrier) {
continue;
}
if (child.isInVirtualLayout()) {
continue;
}
if (optimize && child.horizontalRun != null && child.verticalRun != null &&
child.horizontalRun.dimension.resolved &&
child.verticalRun.dimension.resolved) {
continue;
}
ConstraintWidget.DimensionBehaviour widthBehavior = child.getDimensionBehaviour(HORIZONTAL);
ConstraintWidget.DimensionBehaviour heightBehavior = child.getDimensionBehaviour(VERTICAL);
boolean skip = widthBehavior == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
&& child.mMatchConstraintDefaultWidth != MATCH_CONSTRAINT_WRAP
&& heightBehavior == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
&& child.mMatchConstraintDefaultHeight != MATCH_CONSTRAINT_WRAP;
if (!skip && layout.optimizeFor(Optimizer.OPTIMIZATION_DIRECT)
&& !(child instanceof VirtualLayout)) {
if (widthBehavior == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
&& child.mMatchConstraintDefaultWidth == MATCH_CONSTRAINT_SPREAD
&& heightBehavior != ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
&& !child.isInHorizontalChain()) {
skip = true;
}
if (heightBehavior == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
&& child.mMatchConstraintDefaultHeight == MATCH_CONSTRAINT_SPREAD
&& widthBehavior != ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
&& !child.isInHorizontalChain()) {
skip = true;
}
// Don't measure yet -- let the direct solver have a shot at it.
if ((widthBehavior == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
|| heightBehavior == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT)
&& child.mDimensionRatio > 0) {
skip = true;
}
}
if (skip) {
// we don't need to measure here as the dimension of the widget
// will be completely computed by the solver.
continue;
}
measure(measurer, child, Measure.SELF_DIMENSIONS);
if (layout.mMetrics != null) {
layout.mMetrics.measuredWidgets++;
}
}
measurer.didMeasures();
}
private void solveLinearSystem(ConstraintWidgetContainer layout, String reason, int pass, int w, int h) {
long startLayout;
if (LinearSystem.MEASURE) {
startLayout = System.nanoTime();
}
int minWidth = layout.getMinWidth();
int minHeight = layout.getMinHeight();
layout.setMinWidth(0);
layout.setMinHeight(0);
layout.setWidth(w);
layout.setHeight(h);
layout.setMinWidth(minWidth);
layout.setMinHeight(minHeight);
if (DEBUG) {
System.out.println("### Solve <" + reason + "> ###");
}
constraintWidgetContainer.setPass(pass);
constraintWidgetContainer.layout();
if (LinearSystem.MEASURE && layout.mMetrics != null) {
long endLayout = System.nanoTime();
layout.mMetrics.measuresLayoutDuration += (endLayout - startLayout);
}
}
/**
* Called by ConstraintLayout onMeasure()
*
* @param layout
* @param optimizationLevel
* @param widthMode
* @param widthSize
* @param heightMode
* @param heightSize
* @param lastMeasureWidth
* @param lastMeasureHeight
*/
public long solverMeasure(ConstraintWidgetContainer layout,
int optimizationLevel,
int paddingX, int paddingY,
int widthMode, int widthSize,
int heightMode, int heightSize,
int lastMeasureWidth,
int lastMeasureHeight) {
Measurer measurer = layout.getMeasurer();
long layoutTime = 0;
final int childCount = layout.mChildren.size();
int startingWidth = layout.getWidth();
int startingHeight = layout.getHeight();
boolean optimizeWrap = Optimizer.enabled(optimizationLevel, Optimizer.OPTIMIZATION_GRAPH_WRAP);
boolean optimize = optimizeWrap || Optimizer.enabled(optimizationLevel, Optimizer.OPTIMIZATION_GRAPH);
if (optimize) {
for (int i = 0; i < childCount; i++) {
ConstraintWidget child = layout.mChildren.get(i);
boolean matchWidth = child.getHorizontalDimensionBehaviour() == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT;
boolean matchHeight = child.getVerticalDimensionBehaviour() == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT;
boolean ratio = matchWidth && matchHeight && child.getDimensionRatio() > 0;
if (child.isInHorizontalChain() && (ratio)) {
optimize = false;
break;
}
if (child.isInVerticalChain() && (ratio)) {
optimize = false;
break;
}
if (child instanceof VirtualLayout) {
optimize = false;
break;
}
if (child.isInHorizontalChain()
|| child.isInVerticalChain()) {
optimize = false;
break;
}
}
}
if (optimize && LinearSystem.sMetrics != null) {
LinearSystem.sMetrics.measures++;
}
boolean allSolved = false;
optimize &= (widthMode == EXACTLY && heightMode == EXACTLY) || optimizeWrap;
int computations = 0;
if (optimize) {
// For non-optimizer this doesn't seem to be a problem.
// For both cases, having the width address max size early seems to work (which makes sense).
// Putting it specific to optimizer to reduce unnecessary risk.
widthSize = Math.min(layout.getMaxWidth(), widthSize);
heightSize = Math.min(layout.getMaxHeight(), heightSize);
if (widthMode == EXACTLY && layout.getWidth() != widthSize) {
layout.setWidth(widthSize);
layout.invalidateGraph();
}
if (heightMode == EXACTLY && layout.getHeight() != heightSize) {
layout.setHeight(heightSize);
layout.invalidateGraph();
}
if (widthMode == EXACTLY && heightMode == EXACTLY) {
allSolved = layout.directMeasure(optimizeWrap);
computations = 2;
} else {
allSolved = layout.directMeasureSetup(optimizeWrap);
if (widthMode == EXACTLY) {
allSolved &= layout.directMeasureWithOrientation(optimizeWrap, HORIZONTAL);
computations++;
}
if (heightMode == EXACTLY) {
allSolved &= layout.directMeasureWithOrientation(optimizeWrap, VERTICAL);
computations++;
}
}
if (allSolved) {
layout.updateFromRuns(widthMode == EXACTLY, heightMode == EXACTLY);
}
} else {
if (false) {
layout.horizontalRun.clear();
layout.verticalRun.clear();
for (ConstraintWidget child : layout.getChildren()) {
child.horizontalRun.clear();
child.verticalRun.clear();
}
}
}
if (!allSolved || computations != 2) {
int optimizations = layout.getOptimizationLevel();
if (childCount > 0) {
measureChildren(layout);
}
if (LinearSystem.MEASURE) {
layoutTime = System.nanoTime();
}
updateHierarchy(layout);
// let's update the size dependent widgets if any...
final int sizeDependentWidgetsCount = mVariableDimensionsWidgets.size();
// let's solve the linear system.
if (childCount > 0) {
solveLinearSystem(layout, "First pass", 0, startingWidth, startingHeight);
}
if (DEBUG) {
System.out.println("size dependent widgets: " + sizeDependentWidgetsCount);
}
if (sizeDependentWidgetsCount > 0) {
boolean needSolverPass = false;
boolean containerWrapWidth = layout.getHorizontalDimensionBehaviour()
== ConstraintWidget.DimensionBehaviour.WRAP_CONTENT;
boolean containerWrapHeight = layout.getVerticalDimensionBehaviour()
== ConstraintWidget.DimensionBehaviour.WRAP_CONTENT;
int minWidth = Math.max(layout.getWidth(), constraintWidgetContainer.getMinWidth());
int minHeight = Math.max(layout.getHeight(), constraintWidgetContainer.getMinHeight());
////////////////////////////////////////////////////////////////////////////////////
// Let's first apply sizes for VirtualLayouts if any
////////////////////////////////////////////////////////////////////////////////////
for (int i = 0; i < sizeDependentWidgetsCount; i++) {
ConstraintWidget widget = mVariableDimensionsWidgets.get(i);
if (!(widget instanceof VirtualLayout)) {
continue;
}
int preWidth = widget.getWidth();
int preHeight = widget.getHeight();
needSolverPass |= measure(measurer, widget, Measure.TRY_GIVEN_DIMENSIONS);
if (layout.mMetrics != null) {
layout.mMetrics.measuredMatchWidgets++;
}
int measuredWidth = widget.getWidth();
int measuredHeight = widget.getHeight();
if (measuredWidth != preWidth) {
widget.setWidth(measuredWidth);
if (containerWrapWidth && widget.getRight() > minWidth) {
int w = widget.getRight()
+ widget.getAnchor(ConstraintAnchor.Type.RIGHT).getMargin();
minWidth = Math.max(minWidth, w);
}
needSolverPass = true;
}
if (measuredHeight != preHeight) {
widget.setHeight(measuredHeight);
if (containerWrapHeight && widget.getBottom() > minHeight) {
int h = widget.getBottom()
+ widget.getAnchor(ConstraintAnchor.Type.BOTTOM).getMargin();
minHeight = Math.max(minHeight, h);
}
needSolverPass = true;
}
VirtualLayout virtualLayout = (VirtualLayout) widget;
needSolverPass |= virtualLayout.needSolverPass();
}
////////////////////////////////////////////////////////////////////////////////////
int maxIterations = 2;
for (int j = 0; j < maxIterations; j++) {
for (int i = 0; i < sizeDependentWidgetsCount; i++) {
ConstraintWidget widget = mVariableDimensionsWidgets.get(i);
if ((widget instanceof Helper && !(widget instanceof VirtualLayout)) || widget instanceof Guideline) {
continue;
}
if (widget.getVisibility() == GONE) {
continue;
}
if (optimize &&
widget.horizontalRun.dimension.resolved && widget.verticalRun.dimension.resolved) {
continue;
}
if (widget instanceof VirtualLayout) {
continue;
}
int preWidth = widget.getWidth();
int preHeight = widget.getHeight();
int preBaselineDistance = widget.getBaselineDistance();
int measureStrategy = Measure.TRY_GIVEN_DIMENSIONS;
if (j == maxIterations -1) {
measureStrategy = Measure.USE_GIVEN_DIMENSIONS;
}
boolean hasMeasure = measure(measurer, widget, measureStrategy);
if (false && !widget.hasDependencies()) {
hasMeasure = false;
}
needSolverPass |= hasMeasure;
if (DEBUG && hasMeasure) {
System.out.println("{#} Needs Solver pass as measure true for " + widget.getDebugName());
}
if (layout.mMetrics != null) {
layout.mMetrics.measuredMatchWidgets++;
}
int measuredWidth = widget.getWidth();
int measuredHeight = widget.getHeight();
if (measuredWidth != preWidth) {
widget.setWidth(measuredWidth);
if (containerWrapWidth && widget.getRight() > minWidth) {
int w = widget.getRight()
+ widget.getAnchor(ConstraintAnchor.Type.RIGHT).getMargin();
minWidth = Math.max(minWidth, w);
}
if (DEBUG) {
System.out.println("{#} Needs Solver pass as Width for " + widget.getDebugName() + " changed: " + measuredWidth + " != " + preWidth);
}
needSolverPass = true;
}
if (measuredHeight != preHeight) {
widget.setHeight(measuredHeight);
if (containerWrapHeight && widget.getBottom() > minHeight) {
int h = widget.getBottom()
+ widget.getAnchor(ConstraintAnchor.Type.BOTTOM).getMargin();
minHeight = Math.max(minHeight, h);
}
if (DEBUG) {
System.out.println("{#} Needs Solver pass as Height for " + widget.getDebugName() + " changed: " + measuredHeight + " != " + preHeight);
}
needSolverPass = true;
}
if (widget.hasBaseline() && preBaselineDistance != widget.getBaselineDistance()) {
if (DEBUG) {
System.out.println("{#} Needs Solver pass as Baseline for " + widget.getDebugName() + " changed: " + widget.getBaselineDistance() + " != " + preBaselineDistance);
}
needSolverPass = true;
}
}
if (needSolverPass) {
solveLinearSystem(layout, "intermediate pass", 1 + j, startingWidth, startingHeight);
needSolverPass = false;
} else {
break;
}
}
}
layout.setOptimizationLevel(optimizations);
}
if (LinearSystem.MEASURE) {
layoutTime = (System.nanoTime() - layoutTime);
}
return layoutTime;
}
/**
* Convenience function to fill in the measure spec
*
* @param measurer the measurer callback
* @param widget the widget to measure
* @param measureStrategy how to use the current ConstraintWidget dimensions during the measure
* @return true if needs another solver pass
*/
private boolean measure(Measurer measurer, ConstraintWidget widget, int measureStrategy) {
mMeasure.horizontalBehavior = widget.getHorizontalDimensionBehaviour();
mMeasure.verticalBehavior = widget.getVerticalDimensionBehaviour();
mMeasure.horizontalDimension = widget.getWidth();
mMeasure.verticalDimension = widget.getHeight();
mMeasure.measuredNeedsSolverPass = false;
mMeasure.measureStrategy = measureStrategy;
boolean horizontalMatchConstraints = (mMeasure.horizontalBehavior == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT);
boolean verticalMatchConstraints = (mMeasure.verticalBehavior == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT);
boolean horizontalUseRatio = horizontalMatchConstraints && widget.mDimensionRatio > 0;
boolean verticalUseRatio = verticalMatchConstraints && widget.mDimensionRatio > 0;
if (horizontalUseRatio) {
if (widget.mResolvedMatchConstraintDefault[HORIZONTAL] == ConstraintWidget.MATCH_CONSTRAINT_RATIO_RESOLVED) {
mMeasure.horizontalBehavior = ConstraintWidget.DimensionBehaviour.FIXED;
}
}
if (verticalUseRatio) {
if (widget.mResolvedMatchConstraintDefault[VERTICAL] == ConstraintWidget.MATCH_CONSTRAINT_RATIO_RESOLVED) {
mMeasure.verticalBehavior = ConstraintWidget.DimensionBehaviour.FIXED;
}
}
measurer.measure(widget, mMeasure);
widget.setWidth(mMeasure.measuredWidth);
widget.setHeight(mMeasure.measuredHeight);
widget.setHasBaseline(mMeasure.measuredHasBaseline);
widget.setBaselineDistance(mMeasure.measuredBaseline);
mMeasure.measureStrategy = Measure.SELF_DIMENSIONS;
return mMeasure.measuredNeedsSolverPass;
}
public interface Measurer {
void measure(ConstraintWidget widget, Measure measure);
void didMeasures();
}
public static class Measure {
public static int SELF_DIMENSIONS = 0;
public static int TRY_GIVEN_DIMENSIONS = 1;
public static int USE_GIVEN_DIMENSIONS = 2;
public ConstraintWidget.DimensionBehaviour horizontalBehavior;
public ConstraintWidget.DimensionBehaviour verticalBehavior;
public int horizontalDimension;
public int verticalDimension;
public int measuredWidth;
public int measuredHeight;
public int measuredBaseline;
public boolean measuredHasBaseline;
public boolean measuredNeedsSolverPass;
public int measureStrategy;
}
}