ChainRun.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.widgets.ConstraintAnchor;
import androidx.constraintlayout.core.widgets.ConstraintWidget;
import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer;

import java.util.ArrayList;

import static androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT;
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_WRAP;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.VERTICAL;

public class ChainRun extends WidgetRun {
    ArrayList<WidgetRun> widgets = new ArrayList<>();
    private int chainStyle;

    public ChainRun(ConstraintWidget widget, int orientation) {
        super(widget);
        this.orientation = orientation;
        build();
    }

    @Override
    public String toString() {
        StringBuilder log = new StringBuilder("ChainRun ");
        log.append((orientation == HORIZONTAL ? "horizontal : " : "vertical : "));
        for (WidgetRun run : widgets) {
            log.append("<");
            log.append(run);
            log.append("> ");
        }
        return log.toString();
    }

    @Override
    boolean supportsWrapComputation() {
        final int count = widgets.size();
        for (int i = 0; i < count; i++) {
            WidgetRun run = widgets.get(i);
            if (!run.supportsWrapComputation()) {
                return false;
            }
        }
        return true;
    }

    public long getWrapDimension() {
        final int count = widgets.size();
        long wrapDimension = 0;
        for (int i = 0; i < count; i++) {
            WidgetRun run = widgets.get(i);
            wrapDimension += run.start.margin;
            wrapDimension += run.getWrapDimension();
            wrapDimension += run.end.margin;
        }
        return wrapDimension;
    }

    private void build() {
        ConstraintWidget current = widget;
        ConstraintWidget previous = current.getPreviousChainMember(orientation);
        while (previous != null) {
            current = previous;
            previous = current.getPreviousChainMember(orientation);
        }
        widget = current; // first element of the chain
        widgets.add(current.getRun(orientation));
        ConstraintWidget next = current.getNextChainMember(orientation);
        while (next != null) {
            current = next;
            widgets.add(current.getRun(orientation));
            next = current.getNextChainMember(orientation);
        }
        for (WidgetRun run : widgets) {
            if (orientation == HORIZONTAL) {
                run.widget.horizontalChainRun = this;
            } else if (orientation == ConstraintWidget.VERTICAL) {
                run.widget.verticalChainRun = this;
            }
        }
        boolean isInRtl = (orientation == HORIZONTAL) && ((ConstraintWidgetContainer) widget.getParent()).isRtl();
        if (isInRtl && widgets.size() > 1) {
            widget = widgets.get(widgets.size() - 1).widget;
        }
        chainStyle = orientation == HORIZONTAL ? widget.getHorizontalChainStyle() : widget.getVerticalChainStyle();
    }


    @Override
    void clear() {
        runGroup = null;
        for (WidgetRun run : widgets) {
            run.clear();
        }
    }

    @Override
    void reset() {
        start.resolved = false;
        end.resolved = false;
    }

    @Override
    public void update(Dependency dependency) {
        if (!(start.resolved && end.resolved)) {
            return;
        }

        ConstraintWidget parent = widget.getParent();
        boolean isInRtl = false;
        if (parent instanceof ConstraintWidgetContainer) {
            isInRtl = ((ConstraintWidgetContainer) parent).isRtl();
        }
        int distance = end.value - start.value;
        int size = 0;
        int numMatchConstraints = 0;
        float weights = 0;
        int numVisibleWidgets = 0;
        final int count = widgets.size();
        // let's find the first visible widget...
        int firstVisibleWidget = -1;
        for (int i = 0; i < count; i++) {
            WidgetRun run = widgets.get(i);
            if (run.widget.getVisibility() == GONE) {
                continue;
            }
            firstVisibleWidget = i;
            break;
        }
        // now the last visible widget...
        int lastVisibleWidget = -1;
        for (int i = count - 1; i >= 0; i--) {
            WidgetRun run = widgets.get(i);
            if (run.widget.getVisibility() == GONE) {
                continue;
            }
            lastVisibleWidget = i;
            break;
        }
        for (int j = 0; j< 2; j++ ){
            for (int i = 0; i < count; i++) {
                WidgetRun run = widgets.get(i);
                if (run.widget.getVisibility() == GONE) {
                    continue;
                }
                numVisibleWidgets++;
                if (i > 0 && i >= firstVisibleWidget) {
                    size += run.start.margin;
                }
                int dimension = run.dimension.value;
                boolean treatAsFixed = run.dimensionBehavior != MATCH_CONSTRAINT;
                if (treatAsFixed) {
                    if (orientation == HORIZONTAL && !run.widget.horizontalRun.dimension.resolved) {
                        return;
                    }
                    if (orientation == VERTICAL && !run.widget.verticalRun.dimension.resolved) {
                        return;
                    }
                } else if (run.matchConstraintsType == MATCH_CONSTRAINT_WRAP && j == 0) {
                    treatAsFixed = true;
                    dimension = run.dimension.wrapValue;
                    numMatchConstraints++;
                } else if (run.dimension.resolved) {
                    treatAsFixed = true;
                }
                if (!treatAsFixed) { // only for the first pass
                    numMatchConstraints++;
                    float weight = run.widget.mWeight[orientation];
                    if (weight >= 0) {
                        weights += weight;
                    }
                } else {
                    size += dimension;
                }
                if (i < count - 1 && i < lastVisibleWidget) {
                    size += -run.end.margin;
                }
            }
            if (size < distance || numMatchConstraints == 0) {
                break; // we are good to go!
            }
            // otherwise, let's do another pass with using match_constraints
            numVisibleWidgets = 0;
            numMatchConstraints = 0;
            size = 0;
            weights = 0;
        }

        int position = start.value;
        if (isInRtl) {
            position = end.value;
        }
        if (size > distance) {
            if (isInRtl) {
                position += (int) (0.5f + (size - distance) / 2f);
            } else {
                position -= (int) (0.5f + (size - distance) / 2f);
            }
        }
        int matchConstraintsDimension = 0;
        if (numMatchConstraints > 0) {
            matchConstraintsDimension = (int) (0.5f + (distance - size) / (float) numMatchConstraints);

            int appliedLimits = 0;
            for (int i = 0; i < count; i++) {
                WidgetRun run = widgets.get(i);
                if (run.widget.getVisibility() == GONE) {
                    continue;
                }
                if (run.dimensionBehavior == MATCH_CONSTRAINT && !run.dimension.resolved) {
                    int dimension = matchConstraintsDimension;
                    if (weights > 0) {
                        float weight = run.widget.mWeight[orientation];
                        dimension = (int) (0.5f + weight * (distance - size) / weights);
                    }
                    int max;
                    int min;
                    int value = dimension;
                    if (orientation == HORIZONTAL) {
                        max = run.widget.mMatchConstraintMaxWidth;
                        min = run.widget.mMatchConstraintMinWidth;
                    } else {
                        max = run.widget.mMatchConstraintMaxHeight;
                        min = run.widget.mMatchConstraintMinHeight;
                    }
                    if (run.matchConstraintsType == MATCH_CONSTRAINT_WRAP) {
                        value = Math.min(value, run.dimension.wrapValue);
                    }
                    value = Math.max(min, value);
                    if (max > 0) {
                        value = Math.min(max, value);
                    }
                    if (value != dimension) {
                        appliedLimits++;
                        dimension = value;
                    }
                    run.dimension.resolve(dimension);
                }
            }
            if (appliedLimits > 0) {
                numMatchConstraints -= appliedLimits;
                // we have to recompute the sizes
                size = 0;
                for (int i = 0; i < count; i++) {
                    WidgetRun run = widgets.get(i);
                    if (run.widget.getVisibility() == GONE) {
                        continue;
                    }
                    if (i > 0 && i >= firstVisibleWidget) {
                        size += run.start.margin;
                    }
                    size += run.dimension.value;
                    if (i < count - 1 && i < lastVisibleWidget) {
                        size += -run.end.margin;
                    }
                }
            }
            if (chainStyle == ConstraintWidget.CHAIN_PACKED && appliedLimits == 0) {
                chainStyle = ConstraintWidget.CHAIN_SPREAD;
            }
        }

        if (size > distance) {
            chainStyle = ConstraintWidget.CHAIN_PACKED;
        }

        if (numVisibleWidgets > 0 && numMatchConstraints == 0 && firstVisibleWidget == lastVisibleWidget) {
            // only one widget of fixed size to display...
            chainStyle = ConstraintWidget.CHAIN_PACKED;
        }

        if (chainStyle == ConstraintWidget.CHAIN_SPREAD_INSIDE) {
            int gap = 0;
            if (numVisibleWidgets > 1) {
                gap = (distance - size) / (numVisibleWidgets - 1);
            } else if (numVisibleWidgets == 1) {
                gap = (distance - size) / 2;
            }
            if (numMatchConstraints > 0) {
                gap = 0;
            }
            for (int i = 0; i < count; i++) {
                int index = i;
                if (isInRtl) {
                    index = count - (i + 1);
                }
                WidgetRun run = widgets.get(index);
                if (run.widget.getVisibility() == GONE) {
                    run.start.resolve(position);
                    run.end.resolve(position);
                    continue;
                }
                if (i > 0) {
                    if (isInRtl) {
                        position -= gap;
                    } else {
                        position += gap;
                    }
                }
                if (i > 0 && i >= firstVisibleWidget) {
                    if (isInRtl) {
                        position -= run.start.margin;
                    } else {
                        position += run.start.margin;
                    }
                }

                if (isInRtl) {
                    run.end.resolve(position);
                } else {
                    run.start.resolve(position);
                }

                int dimension = run.dimension.value;
                if (run.dimensionBehavior == MATCH_CONSTRAINT
                        && run.matchConstraintsType == MATCH_CONSTRAINT_WRAP) {
                    dimension = run.dimension.wrapValue;
                }
                if (isInRtl) {
                    position -= dimension;
                } else {
                    position += dimension;
                }

                if (isInRtl) {
                    run.start.resolve(position);
                } else {
                    run.end.resolve(position);
                }
                run.resolved = true;
                if (i < count - 1 && i < lastVisibleWidget) {
                    if (isInRtl) {
                        position -= -run.end.margin;
                    } else {
                        position += -run.end.margin;
                    }
                }
            }
        } else if (chainStyle == ConstraintWidget.CHAIN_SPREAD) {
            int gap = (distance - size) / (numVisibleWidgets + 1);
            if (numMatchConstraints > 0) {
                gap = 0;
            }
            for (int i = 0; i < count; i++) {
                int index = i;
                if (isInRtl) {
                    index = count - (i + 1);
                }
                WidgetRun run = widgets.get(index);
                if (run.widget.getVisibility() == GONE) {
                    run.start.resolve(position);
                    run.end.resolve(position);
                    continue;
                }
                if (isInRtl) {
                    position -= gap;
                } else {
                    position += gap;
                }
                if (i > 0 && i >= firstVisibleWidget) {
                    if (isInRtl) {
                        position -= run.start.margin;
                    } else {
                        position += run.start.margin;
                    }
                }

                if (isInRtl) {
                    run.end.resolve(position);
                } else {
                    run.start.resolve(position);
                }

                int dimension = run.dimension.value;
                if (run.dimensionBehavior == MATCH_CONSTRAINT
                        && run.matchConstraintsType == MATCH_CONSTRAINT_WRAP) {
                    dimension = Math.min(dimension, run.dimension.wrapValue);
                }

                if (isInRtl) {
                    position -= dimension;
                } else {
                    position += dimension;
                }

                if (isInRtl) {
                    run.start.resolve(position);
                } else {
                    run.end.resolve(position);
                }
                if (i < count - 1 && i < lastVisibleWidget) {
                    if (isInRtl) {
                        position -= -run.end.margin;
                    } else {
                        position += -run.end.margin;
                    }
                }
            }
        } else if (chainStyle == ConstraintWidget.CHAIN_PACKED) {
            float bias = (orientation == HORIZONTAL) ? widget.getHorizontalBiasPercent()
                    : widget.getVerticalBiasPercent();
            if (isInRtl) {
                bias = 1 - bias;
            }
            int gap = (int) (0.5f + (distance - size) * bias);
            if (gap < 0 || numMatchConstraints > 0) {
                gap = 0;
            }
            if (isInRtl) {
                position -= gap;
            } else {
                position += gap;
            }
            for (int i = 0; i < count; i++) {
                int index = i;
                if (isInRtl) {
                    index = count - (i + 1);
                }
                WidgetRun run = widgets.get(index);
                if (run.widget.getVisibility() == GONE) {
                    run.start.resolve(position);
                    run.end.resolve(position);
                    continue;
                }
                if (i > 0 && i >= firstVisibleWidget) {
                    if (isInRtl) {
                        position -= run.start.margin;
                    } else {
                        position += run.start.margin;
                    }
                }
                if (isInRtl) {
                    run.end.resolve(position);
                } else {
                    run.start.resolve(position);
                }

                int dimension = run.dimension.value;
                if (run.dimensionBehavior == MATCH_CONSTRAINT
                        && run.matchConstraintsType == MATCH_CONSTRAINT_WRAP) {
                    dimension = run.dimension.wrapValue;
                }
                if (isInRtl) {
                    position -= dimension;
                } else {
                    position += dimension;
                }

                if (isInRtl) {
                    run.start.resolve(position);
                } else {
                    run.end.resolve(position);
                }
                if (i < count - 1 && i < lastVisibleWidget) {
                    if (isInRtl) {
                        position -= -run.end.margin;
                    } else {
                        position += -run.end.margin;
                    }
                }
            }
        }
    }

    public void applyToWidget() {
        for (int i = 0; i < widgets.size(); i++) {
            WidgetRun run = widgets.get(i);
            run.applyToWidget();
        }
    }

    private ConstraintWidget getFirstVisibleWidget() {
        for (int i = 0; i < widgets.size(); i++) {
            WidgetRun run = widgets.get(i);
            if (run.widget.getVisibility() != GONE) {
                return run.widget;
            }
        }
        return null;
    }

    private ConstraintWidget getLastVisibleWidget() {
        for (int i = widgets.size() -1; i >= 0; i--) {
            WidgetRun run = widgets.get(i);
            if (run.widget.getVisibility() != GONE) {
                return run.widget;
            }
        }
        return null;
    }


    @Override
    void apply() {
        for (WidgetRun run : widgets) {
            run.apply();
        }
        int count = widgets.size();
        if (count < 1) {
            return;
        }

        // get the first and last element of the chain
        ConstraintWidget firstWidget = widgets.get(0).widget;
        ConstraintWidget lastWidget = widgets.get(count - 1).widget;

        if (orientation == HORIZONTAL) {
            ConstraintAnchor startAnchor = firstWidget.mLeft;
            ConstraintAnchor endAnchor = lastWidget.mRight;
            DependencyNode startTarget = getTarget(startAnchor, HORIZONTAL);
            int startMargin = startAnchor.getMargin();
            ConstraintWidget firstVisibleWidget = getFirstVisibleWidget();
            if (firstVisibleWidget != null) {
                startMargin = firstVisibleWidget.mLeft.getMargin();
            }
            if (startTarget != null) {
                addTarget(start, startTarget, startMargin);
            }
            DependencyNode endTarget = getTarget(endAnchor, HORIZONTAL);
            int endMargin = endAnchor.getMargin();
            ConstraintWidget lastVisibleWidget = getLastVisibleWidget();
            if (lastVisibleWidget != null) {
                endMargin = lastVisibleWidget.mRight.getMargin();
            }
            if (endTarget != null) {
                addTarget(end, endTarget, -endMargin);
            }
        } else {
            ConstraintAnchor startAnchor = firstWidget.mTop;
            ConstraintAnchor endAnchor = lastWidget.mBottom;
            DependencyNode startTarget = getTarget(startAnchor, VERTICAL);
            int startMargin = startAnchor.getMargin();
            ConstraintWidget firstVisibleWidget = getFirstVisibleWidget();
            if (firstVisibleWidget != null) {
                startMargin = firstVisibleWidget.mTop.getMargin();
            }
            if (startTarget != null) {
                addTarget(start, startTarget, startMargin);
            }
            DependencyNode endTarget = getTarget(endAnchor, VERTICAL);
            int endMargin = endAnchor.getMargin();
            ConstraintWidget lastVisibleWidget = getLastVisibleWidget();
            if (lastVisibleWidget != null) {
                endMargin = lastVisibleWidget.mBottom.getMargin();
            }
            if (endTarget != null) {
                addTarget(end, endTarget, -endMargin);
            }
        }
        start.updateDelegate = this;
        end.updateDelegate = this;
    }

}