WidgetRun.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 static androidx.constraintlayout.core.widgets.ConstraintWidget.HORIZONTAL;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_PERCENT;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_RATIO;
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;

import androidx.constraintlayout.core.widgets.ConstraintAnchor;
import androidx.constraintlayout.core.widgets.ConstraintWidget;

public abstract class WidgetRun implements Dependency {
    public int matchConstraintsType;
    ConstraintWidget mWidget;
    RunGroup mRunGroup;
    protected ConstraintWidget.DimensionBehaviour mDimensionBehavior;
    DimensionDependency mDimension = new DimensionDependency(this);

    public int orientation = HORIZONTAL;
    boolean mResolved = false;
    public DependencyNode start = new DependencyNode(this);
    public DependencyNode end = new DependencyNode(this);

    @SuppressWarnings("HiddenTypeParameter")
    protected RunType mRunType = RunType.NONE;

    public WidgetRun(ConstraintWidget widget) {
        this.mWidget = widget;
    }

    @SuppressWarnings("HiddenAbstractMethod")
    abstract void clear();

    @SuppressWarnings("HiddenAbstractMethod")
    abstract void apply();

    @SuppressWarnings("HiddenAbstractMethod")
    abstract void applyToWidget();

    @SuppressWarnings("HiddenAbstractMethod")
    abstract void reset();

    @SuppressWarnings("HiddenAbstractMethod")
    abstract boolean supportsWrapComputation();

    public boolean isDimensionResolved() {
        return mDimension.resolved;
    }

    // @TODO: add description
    public boolean isCenterConnection() {
        int connections = 0;
        int count = start.mTargets.size();
        for (int i = 0; i < count; i++) {
            DependencyNode dependency = start.mTargets.get(i);
            if (dependency.mRun != this) {
                connections++;
            }
        }
        count = end.mTargets.size();
        for (int i = 0; i < count; i++) {
            DependencyNode dependency = end.mTargets.get(i);
            if (dependency.mRun != this) {
                connections++;
            }
        }
        return connections >= 2;
    }

    // @TODO: add description
    public long wrapSize(int direction) {
        if (mDimension.resolved) {
            long size = mDimension.value;
            if (isCenterConnection()) { //start.targets.size() > 0 && end.targets.size() > 0) {
                size += start.mMargin - end.mMargin;
            } else {
                if (direction == RunGroup.START) {
                    size += start.mMargin;
                } else {
                    size -= end.mMargin;
                }
            }
            return size;
        }
        return 0;
    }

    protected final DependencyNode getTarget(ConstraintAnchor anchor) {
        if (anchor.mTarget == null) {
            return null;
        }
        DependencyNode target = null;
        ConstraintWidget targetWidget = anchor.mTarget.mOwner;
        ConstraintAnchor.Type targetType = anchor.mTarget.mType;
        switch (targetType) {
            case LEFT: {
                HorizontalWidgetRun run = targetWidget.mHorizontalRun;
                target = run.start;
            }
            break;
            case RIGHT: {
                HorizontalWidgetRun run = targetWidget.mHorizontalRun;
                target = run.end;
            }
            break;
            case TOP: {
                VerticalWidgetRun run = targetWidget.mVerticalRun;
                target = run.start;
            }
            break;
            case BASELINE: {
                VerticalWidgetRun run = targetWidget.mVerticalRun;
                target = run.baseline;
            }
            break;
            case BOTTOM: {
                VerticalWidgetRun run = targetWidget.mVerticalRun;
                target = run.end;
            }
            break;
            default:
                break;
        }
        return target;
    }

    protected void updateRunCenter(Dependency dependency,
            ConstraintAnchor startAnchor,
            ConstraintAnchor endAnchor,
            int orientation) {
        DependencyNode startTarget = getTarget(startAnchor);
        DependencyNode endTarget = getTarget(endAnchor);

        if (!(startTarget.resolved && endTarget.resolved)) {
            return;
        }

        int startPos = startTarget.value + startAnchor.getMargin();
        int endPos = endTarget.value - endAnchor.getMargin();
        int distance = endPos - startPos;

        if (!mDimension.resolved
                && mDimensionBehavior == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT) {
            resolveDimension(orientation, distance);
        }

        if (!mDimension.resolved) {
            return;
        }

        if (mDimension.value == distance) {
            start.resolve(startPos);
            end.resolve(endPos);
            return;
        }

        // Otherwise, we have to center
        float bias = orientation == HORIZONTAL ? mWidget.getHorizontalBiasPercent()
                : mWidget.getVerticalBiasPercent();

        if (startTarget == endTarget) {
            startPos = startTarget.value;
            endPos = endTarget.value;
            // TODO: taking advantage of bias here would be a nice feature to support,
            // but for now let's stay compatible with 1.1
            bias = 0.5f;
        }

        int availableDistance = (endPos - startPos - mDimension.value);
        start.resolve((int) (0.5f + startPos + availableDistance * bias));
        end.resolve(start.value + mDimension.value);
    }

    private void resolveDimension(int orientation, int distance) {
        switch (matchConstraintsType) {
            case MATCH_CONSTRAINT_SPREAD: {
                mDimension.resolve(getLimitedDimension(distance, orientation));
            }
            break;
            case MATCH_CONSTRAINT_PERCENT: {
                ConstraintWidget parent = mWidget.getParent();
                if (parent != null) {
                    WidgetRun run = orientation == HORIZONTAL
                            ? parent.mHorizontalRun
                            : parent.mVerticalRun;
                    if (run.mDimension.resolved) {
                        float percent = orientation == HORIZONTAL
                                ? mWidget.mMatchConstraintPercentWidth
                                : mWidget.mMatchConstraintPercentHeight;
                        int targetDimensionValue = run.mDimension.value;
                        int size = (int) (0.5f + targetDimensionValue * percent);
                        mDimension.resolve(getLimitedDimension(size, orientation));
                    }
                }
            }
            break;
            case MATCH_CONSTRAINT_WRAP: {
                int wrapValue = getLimitedDimension(mDimension.wrapValue, orientation);
                mDimension.resolve(Math.min(wrapValue, distance));
            }
            break;
            case MATCH_CONSTRAINT_RATIO: {
                if (mWidget.mHorizontalRun.mDimensionBehavior
                        == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
                        && mWidget.mHorizontalRun.matchConstraintsType == MATCH_CONSTRAINT_RATIO
                        && mWidget.mVerticalRun.mDimensionBehavior
                        == ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
                        && mWidget.mVerticalRun.matchConstraintsType == MATCH_CONSTRAINT_RATIO) {
                    // pof
                } else {
                    WidgetRun run = (orientation == HORIZONTAL)
                            ? mWidget.mVerticalRun : mWidget.mHorizontalRun;
                    if (run.mDimension.resolved) {
                        float ratio = mWidget.getDimensionRatio();
                        int value;
                        if (orientation == VERTICAL) {
                            value = (int) (0.5f + run.mDimension.value / ratio);
                        } else {
                            value = (int) (0.5f + ratio * run.mDimension.value);
                        }
                        mDimension.resolve(value);
                    }
                }
            }
            break;
            default:
                break;
        }
    }

    protected void updateRunStart(Dependency dependency) {

    }

    protected void updateRunEnd(Dependency dependency) {

    }

    // @TODO: add description
    @Override
    public void update(Dependency dependency) {
    }

    protected final int getLimitedDimension(int dimension, int orientation) {
        if (orientation == HORIZONTAL) {
            int max = mWidget.mMatchConstraintMaxWidth;
            int min = mWidget.mMatchConstraintMinWidth;
            int value = Math.max(min, dimension);
            if (max > 0) {
                value = Math.min(max, dimension);
            }
            if (value != dimension) {
                dimension = value;
            }
        } else {
            int max = mWidget.mMatchConstraintMaxHeight;
            int min = mWidget.mMatchConstraintMinHeight;
            int value = Math.max(min, dimension);
            if (max > 0) {
                value = Math.min(max, dimension);
            }
            if (value != dimension) {
                dimension = value;
            }
        }
        return dimension;
    }

    protected final DependencyNode getTarget(ConstraintAnchor anchor, int orientation) {
        if (anchor.mTarget == null) {
            return null;
        }
        DependencyNode target = null;
        ConstraintWidget targetWidget = anchor.mTarget.mOwner;
        WidgetRun run = (orientation == ConstraintWidget.HORIZONTAL)
                ? targetWidget.mHorizontalRun : targetWidget.mVerticalRun;
        ConstraintAnchor.Type targetType = anchor.mTarget.mType;
        switch (targetType) {
            case TOP:
            case LEFT: {
                target = run.start;
            }
            break;
            case BOTTOM:
            case RIGHT: {
                target = run.end;
            }
            break;
            default:
                break;
        }
        return target;
    }

    protected final void addTarget(DependencyNode node,
            DependencyNode target,
            int margin) {
        node.mTargets.add(target);
        node.mMargin = margin;
        target.mDependencies.add(node);
    }

    protected final void addTarget(DependencyNode node,
            DependencyNode target,
            int marginFactor,
            @SuppressWarnings("HiddenTypeParameter") DimensionDependency
                    dimensionDependency) {
        node.mTargets.add(target);
        node.mTargets.add(mDimension);
        node.mMarginFactor = marginFactor;
        node.mMarginDependency = dimensionDependency;
        target.mDependencies.add(node);
        dimensionDependency.mDependencies.add(node);
    }

    // @TODO: add description
    public long getWrapDimension() {
        if (mDimension.resolved) {
            return mDimension.value;
        }
        return 0;
    }

    public boolean isResolved() {
        return mResolved;
    }

    enum RunType {NONE, START, END, CENTER}
}