ConstraintAnchor.java
/*
* Copyright (C) 2015 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;
import androidx.constraintlayout.core.Cache;
import androidx.constraintlayout.core.SolverVariable;
import androidx.constraintlayout.core.widgets.analyzer.Grouping;
import androidx.constraintlayout.core.widgets.analyzer.WidgetGroup;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
/**
* Model a constraint relation. Widgets contains anchors, and a constraint relation between
* two widgets is made by connecting one anchor to another. The anchor will contains a pointer
* to the target anchor if it is connected.
*/
public class ConstraintAnchor {
private static final boolean ALLOW_BINARY = false;
private HashSet<ConstraintAnchor> mDependents = null;
private int mFinalValue;
private boolean mHasFinalValue;
public void findDependents(int orientation, ArrayList<WidgetGroup> list, WidgetGroup group) {
if (mDependents != null) {
for (ConstraintAnchor anchor : mDependents) {
Grouping.findDependents(anchor.mOwner, orientation, list, group);
}
}
}
public HashSet<ConstraintAnchor> getDependents() { return mDependents; }
public boolean hasDependents() {
if (mDependents == null) {
return false;
}
return mDependents.size() > 0;
}
public boolean hasCenteredDependents() {
if (mDependents == null) {
return false;
}
for (ConstraintAnchor anchor : mDependents) {
ConstraintAnchor opposite = anchor.getOpposite();
if (opposite.isConnected()) {
return true;
}
}
return false;
}
public void setFinalValue(int finalValue) {
this.mFinalValue = finalValue;
this.mHasFinalValue = true;
}
public int getFinalValue() {
if (!mHasFinalValue) {
return 0;
}
return mFinalValue;
}
public void resetFinalResolution() {
mHasFinalValue = false;
mFinalValue = 0;
}
public boolean hasFinalValue() { return mHasFinalValue; }
/**
* Define the type of anchor
*/
public enum Type { NONE, LEFT, TOP, RIGHT, BOTTOM, BASELINE, CENTER, CENTER_X, CENTER_Y }
private static final int UNSET_GONE_MARGIN = Integer.MIN_VALUE;
public final ConstraintWidget mOwner;
public final Type mType;
public ConstraintAnchor mTarget;
public int mMargin = 0;
int mGoneMargin = UNSET_GONE_MARGIN;
SolverVariable mSolverVariable;
public void copyFrom(ConstraintAnchor source, HashMap<ConstraintWidget, ConstraintWidget> map) {
if (mTarget != null) {
if (mTarget.mDependents != null) {
mTarget.mDependents.remove(this);
}
}
if (source.mTarget != null) {
Type type = source.mTarget.getType();
ConstraintWidget owner = map.get(source.mTarget.mOwner);
mTarget = owner.getAnchor(type);
} else {
mTarget = null;
}
if (mTarget != null) {
if (mTarget.mDependents == null) {
mTarget.mDependents = new HashSet<>();
}
mTarget.mDependents.add(this);
}
mMargin = source.mMargin;
mGoneMargin = source.mGoneMargin;
}
/**
* Constructor
* @param owner the widget owner of this anchor.
* @param type the anchor type.
*/
public ConstraintAnchor(ConstraintWidget owner, Type type) {
mOwner = owner;
mType = type;
}
/**
* Return the solver variable for this anchor
* @return
*/
public SolverVariable getSolverVariable() { return mSolverVariable; }
/**
* Reset the solver variable
*/
public void resetSolverVariable(Cache cache) {
if (mSolverVariable == null) {
mSolverVariable = new SolverVariable(SolverVariable.Type.UNRESTRICTED, null);
} else {
mSolverVariable.reset();
}
}
/**
* Return the anchor's owner
* @return the Widget owning the anchor
*/
public ConstraintWidget getOwner() { return mOwner; }
/**
* Return the type of the anchor
* @return type of the anchor.
*/
public Type getType() { return mType; }
/**
* Return the connection's margin from this anchor to its target.
* @return the margin value. 0 if not connected.
*/
public int getMargin() {
if (mOwner.getVisibility() == ConstraintWidget.GONE) {
return 0;
}
if (mGoneMargin != UNSET_GONE_MARGIN && mTarget != null
&& mTarget.mOwner.getVisibility() == ConstraintWidget.GONE) {
return mGoneMargin;
}
return mMargin;
}
/**
* Return the connection's target (null if not connected)
* @return the ConstraintAnchor target
*/
public ConstraintAnchor getTarget() { return mTarget; }
/**
* Resets the anchor's connection.
*/
public void reset() {
if (mTarget != null && mTarget.mDependents != null) {
mTarget.mDependents.remove(this);
if (mTarget.mDependents.size() == 0) {
mTarget.mDependents = null;
}
}
mDependents = null;
mTarget = null;
mMargin = 0;
mGoneMargin = UNSET_GONE_MARGIN;
mHasFinalValue = false;
mFinalValue = 0;
}
/**
* Connects this anchor to another one.
*
* @param toAnchor
* @param margin
* @param goneMargin
* @param forceConnection
* @return true if the connection succeeds.
*/
public boolean connect(ConstraintAnchor toAnchor, int margin, int goneMargin,
boolean forceConnection) {
if (toAnchor == null) {
reset();
return true;
}
if (!forceConnection && !isValidConnection(toAnchor)) {
return false;
}
mTarget = toAnchor;
if (mTarget.mDependents == null) {
mTarget.mDependents = new HashSet<>();
}
if (mTarget.mDependents != null) {
mTarget.mDependents.add(this);
}
mMargin = margin;
mGoneMargin = goneMargin;
return true;
}
/**
* Connects this anchor to another one.
* @param toAnchor
* @param margin
* @return true if the connection succeeds.
*/
public boolean connect(ConstraintAnchor toAnchor, int margin) {
return connect(toAnchor, margin, UNSET_GONE_MARGIN, false);
}
/**
* Returns the connection status of this anchor
* @return true if the anchor is connected to another one.
*/
public boolean isConnected() {
return mTarget != null;
}
/**
* Checks if the connection to a given anchor is valid.
* @param anchor the anchor we want to connect to
* @return true if it's a compatible anchor
*/
public boolean isValidConnection(ConstraintAnchor anchor) {
if (anchor == null) {
return false;
}
Type target = anchor.getType();
if (target == mType) {
if (mType == Type.BASELINE
&& (!anchor.getOwner().hasBaseline() || !getOwner().hasBaseline())) {
return false;
}
return true;
}
switch (mType) {
case CENTER: {
// allow everything but baseline and center_x/center_y
return target != Type.BASELINE && target != Type.CENTER_X
&& target != Type.CENTER_Y;
}
case LEFT:
case RIGHT: {
boolean isCompatible = target == Type.LEFT || target == Type.RIGHT;
if (anchor.getOwner() instanceof Guideline) {
isCompatible = isCompatible || target == Type.CENTER_X;
}
return isCompatible;
}
case TOP:
case BOTTOM: {
boolean isCompatible = target == Type.TOP || target == Type.BOTTOM;
if (anchor.getOwner() instanceof Guideline) {
isCompatible = isCompatible || target == Type.CENTER_Y;
}
return isCompatible;
}
case BASELINE: {
if (target == Type.LEFT || target == Type.RIGHT) {
return false;
}
return true;
}
case CENTER_X:
case CENTER_Y:
case NONE:
return false;
}
throw new AssertionError(mType.name());
}
/**
* Return true if this anchor is a side anchor
*
* @return true if side anchor
*/
public boolean isSideAnchor() {
switch (mType) {
case LEFT:
case RIGHT:
case TOP:
case BOTTOM:
return true;
case BASELINE:
case CENTER:
case CENTER_X:
case CENTER_Y:
case NONE:
return false;
}
throw new AssertionError(mType.name());
}
/**
* Return true if the connection to the given anchor is in the
* same dimension (horizontal or vertical)
*
* @param anchor the anchor we want to connect to
* @return true if it's an anchor on the same dimension
*/
public boolean isSimilarDimensionConnection(ConstraintAnchor anchor) {
Type target = anchor.getType();
if (target == mType) {
return true;
}
switch (mType) {
case CENTER: {
return target != Type.BASELINE;
}
case LEFT:
case RIGHT:
case CENTER_X: {
return target == Type.LEFT || target == Type.RIGHT || target == Type.CENTER_X;
}
case TOP:
case BOTTOM:
case CENTER_Y:
case BASELINE: {
return target == Type.TOP || target == Type.BOTTOM || target == Type.CENTER_Y || target == Type.BASELINE;
}
case NONE:
return false;
}
throw new AssertionError(mType.name());
}
/**
* Set the margin of the connection (if there's one)
* @param margin the new margin of the connection
*/
public void setMargin(int margin) {
if (isConnected()) {
mMargin = margin;
}
}
/**
* Set the gone margin of the connection (if there's one)
* @param margin the new margin of the connection
*/
public void setGoneMargin(int margin) {
if (isConnected()) {
mGoneMargin = margin;
}
}
/**
* Utility function returning true if this anchor is a vertical one.
*
* @return true if vertical anchor, false otherwise
*/
public boolean isVerticalAnchor() {
switch (mType) {
case LEFT:
case RIGHT:
case CENTER:
case CENTER_X:
return false;
case CENTER_Y:
case TOP:
case BOTTOM:
case BASELINE:
case NONE:
return true;
}
throw new AssertionError(mType.name());
}
/**
* Return a string representation of this anchor
*
* @return string representation of the anchor
*/
@Override
public String toString() {
return mOwner.getDebugName() + ":" + mType.toString();
}
/**
*
* Return true if we can connect this anchor to this target.
* We recursively follow connections in order to detect eventual cycles; if we
* do we disallow the connection.
* We also only allow connections to direct parent, siblings, and descendants.
*
* @param target the ConstraintWidget we are trying to connect to
* @param anchor Allow anchor if it loops back to me directly
* @return if the connection is allowed, false otherwise
*/
public boolean isConnectionAllowed(ConstraintWidget target, ConstraintAnchor anchor) {
if (ALLOW_BINARY) {
if (anchor != null && anchor.getTarget() == this) {
return true;
}
}
return isConnectionAllowed(target);
}
/**
* Return true if we can connect this anchor to this target.
* We recursively follow connections in order to detect eventual cycles; if we
* do we disallow the connection.
* We also only allow connections to direct parent, siblings, and descendants.
*
* @param target the ConstraintWidget we are trying to connect to
* @return true if the connection is allowed, false otherwise
*/
public boolean isConnectionAllowed(ConstraintWidget target) {
HashSet<ConstraintWidget> checked = new HashSet<>();
if (isConnectionToMe(target, checked)) {
return false;
}
ConstraintWidget parent = getOwner().getParent();
if (parent == target) { // allow connections to parent
return true;
}
if (target.getParent() == parent) { // allow if we share the same parent
return true;
}
return false;
}
/**
* Recursive with check for loop
*
* @param target
* @param checked set of things already checked
* @return true if it is connected to me
*/
private boolean isConnectionToMe(ConstraintWidget target, HashSet<ConstraintWidget> checked) {
if (checked.contains(target)) {
return false;
}
checked.add(target);
if (target == getOwner()) {
return true;
}
ArrayList<ConstraintAnchor> targetAnchors = target.getAnchors();
for (int i = 0, targetAnchorsSize = targetAnchors.size(); i < targetAnchorsSize; i++) {
ConstraintAnchor anchor = targetAnchors.get(i);
if (anchor.isSimilarDimensionConnection(this) && anchor.isConnected()) {
if (isConnectionToMe(anchor.getTarget().getOwner(), checked)) {
return true;
}
}
}
return false;
}
/**
* Returns the opposite anchor to this one
* @return opposite anchor
*/
public final ConstraintAnchor getOpposite() {
switch (mType) {
case LEFT: {
return mOwner.mRight;
}
case RIGHT: {
return mOwner.mLeft;
}
case TOP: {
return mOwner.mBottom;
}
case BOTTOM: {
return mOwner.mTop;
}
case BASELINE:
case CENTER:
case CENTER_X:
case CENTER_Y:
case NONE:
return null;
}
throw new AssertionError(mType.name());
}
}