BasicMeasure.java

/*
 * 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;
    }
}