1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.constraintlayout.core.widgets;
17 
18 import androidx.constraintlayout.core.Cache;
19 import androidx.constraintlayout.core.SolverVariable;
20 import androidx.constraintlayout.core.widgets.analyzer.Grouping;
21 import androidx.constraintlayout.core.widgets.analyzer.WidgetGroup;
22 
23 import java.util.ArrayList;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 
27 /**
28  * Model a constraint relation. Widgets contains anchors, and a constraint relation between
29  * two widgets is made by connecting one anchor to another. The anchor will contains a pointer
30  * to the target anchor if it is connected.
31  */
32 public class ConstraintAnchor {
33 
34     private static final boolean ALLOW_BINARY = false;
35 
36     private HashSet<ConstraintAnchor> mDependents = null;
37     private int mFinalValue;
38     private boolean mHasFinalValue;
39 
40     // @TODO: add description
findDependents(int orientation, ArrayList<WidgetGroup> list, WidgetGroup group)41     public void findDependents(int orientation, ArrayList<WidgetGroup> list, WidgetGroup group) {
42         if (mDependents != null) {
43             for (ConstraintAnchor anchor : mDependents) {
44                 Grouping.findDependents(anchor.mOwner, orientation, list, group);
45             }
46         }
47     }
48 
getDependents()49     public HashSet<ConstraintAnchor> getDependents() {
50         return mDependents;
51     }
52 
53     // @TODO: add description
hasDependents()54     public boolean hasDependents() {
55         if (mDependents == null) {
56             return false;
57         }
58         return mDependents.size() > 0;
59     }
60 
61     // @TODO: add description
hasCenteredDependents()62     public boolean hasCenteredDependents() {
63         if (mDependents == null) {
64             return false;
65         }
66         for (ConstraintAnchor anchor : mDependents) {
67             ConstraintAnchor opposite = anchor.getOpposite();
68             if (opposite.isConnected()) {
69                 return true;
70             }
71         }
72         return false;
73     }
74 
75     // @TODO: add description
setFinalValue(int finalValue)76     public void setFinalValue(int finalValue) {
77         this.mFinalValue = finalValue;
78         this.mHasFinalValue = true;
79     }
80 
81     // @TODO: add description
getFinalValue()82     public int getFinalValue() {
83         if (!mHasFinalValue) {
84             return 0;
85         }
86         return mFinalValue;
87     }
88 
89     // @TODO: add description
resetFinalResolution()90     public void resetFinalResolution() {
91         mHasFinalValue = false;
92         mFinalValue = 0;
93     }
94 
95     // @TODO: add description
hasFinalValue()96     public boolean hasFinalValue() {
97         return mHasFinalValue;
98     }
99 
100     /**
101      * Define the type of anchor
102      */
103     public enum Type {NONE, LEFT, TOP, RIGHT, BOTTOM, BASELINE, CENTER, CENTER_X, CENTER_Y}
104 
105     private static final int UNSET_GONE_MARGIN = Integer.MIN_VALUE;
106 
107     public final ConstraintWidget mOwner;
108     public final Type mType;
109     public ConstraintAnchor mTarget;
110     public int mMargin = 0;
111     int mGoneMargin = UNSET_GONE_MARGIN;
112 
113     SolverVariable mSolverVariable;
114 
115     // @TODO: add description
copyFrom(ConstraintAnchor source, HashMap<ConstraintWidget, ConstraintWidget> map)116     public void copyFrom(ConstraintAnchor source, HashMap<ConstraintWidget, ConstraintWidget> map) {
117         if (mTarget != null) {
118             if (mTarget.mDependents != null) {
119                 mTarget.mDependents.remove(this);
120             }
121         }
122         if (source.mTarget != null) {
123             Type type = source.mTarget.getType();
124             ConstraintWidget owner = map.get(source.mTarget.mOwner);
125             mTarget = owner.getAnchor(type);
126         } else {
127             mTarget = null;
128         }
129         if (mTarget != null) {
130             if (mTarget.mDependents == null) {
131                 mTarget.mDependents = new HashSet<>();
132             }
133             mTarget.mDependents.add(this);
134         }
135         mMargin = source.mMargin;
136         mGoneMargin = source.mGoneMargin;
137     }
138 
139     /**
140      * Constructor
141      *
142      * @param owner the widget owner of this anchor.
143      * @param type  the anchor type.
144      */
ConstraintAnchor(ConstraintWidget owner, Type type)145     public ConstraintAnchor(ConstraintWidget owner, Type type) {
146         mOwner = owner;
147         mType = type;
148     }
149 
150     /**
151      * Return the solver variable for this anchor
152      */
getSolverVariable()153     public SolverVariable getSolverVariable() {
154         return mSolverVariable;
155     }
156 
157     /**
158      * Reset the solver variable
159      */
resetSolverVariable(Cache cache)160     public void resetSolverVariable(Cache cache) {
161         if (mSolverVariable == null) {
162             mSolverVariable = new SolverVariable(SolverVariable.Type.UNRESTRICTED, null);
163         } else {
164             mSolverVariable.reset();
165         }
166     }
167 
168     /**
169      * Return the anchor's owner
170      *
171      * @return the Widget owning the anchor
172      */
getOwner()173     public ConstraintWidget getOwner() {
174         return mOwner;
175     }
176 
177     /**
178      * Return the type of the anchor
179      *
180      * @return type of the anchor.
181      */
getType()182     public Type getType() {
183         return mType;
184     }
185 
186     /**
187      * Return the connection's margin from this anchor to its target.
188      *
189      * @return the margin value. 0 if not connected.
190      */
getMargin()191     public int getMargin() {
192         if (mOwner.getVisibility() == ConstraintWidget.GONE) {
193             return 0;
194         }
195         if (mGoneMargin != UNSET_GONE_MARGIN && mTarget != null
196                 && mTarget.mOwner.getVisibility() == ConstraintWidget.GONE) {
197             return mGoneMargin;
198         }
199         return mMargin;
200     }
201 
202     /**
203      * Return the connection's target (null if not connected)
204      *
205      * @return the ConstraintAnchor target
206      */
getTarget()207     public ConstraintAnchor getTarget() {
208         return mTarget;
209     }
210 
211     /**
212      * Resets the anchor's connection.
213      */
reset()214     public void reset() {
215         if (mTarget != null && mTarget.mDependents != null) {
216             mTarget.mDependents.remove(this);
217             if (mTarget.mDependents.size() == 0) {
218                 mTarget.mDependents = null;
219             }
220         }
221         mDependents = null;
222         mTarget = null;
223         mMargin = 0;
224         mGoneMargin = UNSET_GONE_MARGIN;
225         mHasFinalValue = false;
226         mFinalValue = 0;
227     }
228 
229     /**
230      * Connects this anchor to another one.
231      *
232      * @return true if the connection succeeds.
233      */
connect(ConstraintAnchor toAnchor, int margin, int goneMargin, boolean forceConnection)234     public boolean connect(ConstraintAnchor toAnchor, int margin, int goneMargin,
235             boolean forceConnection) {
236         if (toAnchor == null) {
237             reset();
238             return true;
239         }
240         if (!forceConnection && !isValidConnection(toAnchor)) {
241             return false;
242         }
243         mTarget = toAnchor;
244         if (mTarget.mDependents == null) {
245             mTarget.mDependents = new HashSet<>();
246         }
247         if (mTarget.mDependents != null) {
248             mTarget.mDependents.add(this);
249         }
250         mMargin = margin;
251         mGoneMargin = goneMargin;
252         return true;
253     }
254 
255 
256     /**
257      * Connects this anchor to another one.
258      *
259      * @return true if the connection succeeds.
260      */
connect(ConstraintAnchor toAnchor, int margin)261     public boolean connect(ConstraintAnchor toAnchor, int margin) {
262         return connect(toAnchor, margin, UNSET_GONE_MARGIN, false);
263     }
264 
265     /**
266      * Returns the connection status of this anchor
267      *
268      * @return true if the anchor is connected to another one.
269      */
isConnected()270     public boolean isConnected() {
271         return mTarget != null;
272     }
273 
274     /**
275      * Checks if the connection to a given anchor is valid.
276      *
277      * @param anchor the anchor we want to connect to
278      * @return true if it's a compatible anchor
279      */
isValidConnection(ConstraintAnchor anchor)280     public boolean isValidConnection(ConstraintAnchor anchor) {
281         if (anchor == null) {
282             return false;
283         }
284         Type target = anchor.getType();
285         if (target == mType) {
286             if (mType == Type.BASELINE
287                     && (!anchor.getOwner().hasBaseline() || !getOwner().hasBaseline())) {
288                 return false;
289             }
290             return true;
291         }
292         switch (mType) {
293             case CENTER: {
294                 // allow everything but baseline and center_x/center_y
295                 return target != Type.BASELINE && target != Type.CENTER_X
296                         && target != Type.CENTER_Y;
297             }
298             case LEFT:
299             case RIGHT: {
300                 boolean isCompatible = target == Type.LEFT || target == Type.RIGHT;
301                 if (anchor.getOwner() instanceof Guideline) {
302                     isCompatible = isCompatible || target == Type.CENTER_X;
303                 }
304                 return isCompatible;
305             }
306             case TOP:
307             case BOTTOM: {
308                 boolean isCompatible = target == Type.TOP || target == Type.BOTTOM;
309                 if (anchor.getOwner() instanceof Guideline) {
310                     isCompatible = isCompatible || target == Type.CENTER_Y;
311                 }
312                 return isCompatible;
313             }
314             case BASELINE: {
315                 if (target == Type.LEFT || target == Type.RIGHT) {
316                     return false;
317                 }
318                 return true;
319             }
320             case CENTER_X:
321             case CENTER_Y:
322             case NONE:
323                 return false;
324         }
325         throw new AssertionError(mType.name());
326     }
327 
328     /**
329      * Return true if this anchor is a side anchor
330      *
331      * @return true if side anchor
332      */
isSideAnchor()333     public boolean isSideAnchor() {
334         switch (mType) {
335             case LEFT:
336             case RIGHT:
337             case TOP:
338             case BOTTOM:
339                 return true;
340             case BASELINE:
341             case CENTER:
342             case CENTER_X:
343             case CENTER_Y:
344             case NONE:
345                 return false;
346         }
347         throw new AssertionError(mType.name());
348     }
349 
350     /**
351      * Return true if the connection to the given anchor is in the
352      * same dimension (horizontal or vertical)
353      *
354      * @param anchor the anchor we want to connect to
355      * @return true if it's an anchor on the same dimension
356      */
isSimilarDimensionConnection(ConstraintAnchor anchor)357     public boolean isSimilarDimensionConnection(ConstraintAnchor anchor) {
358         Type target = anchor.getType();
359         if (target == mType) {
360             return true;
361         }
362         switch (mType) {
363             case CENTER: {
364                 return target != Type.BASELINE;
365             }
366             case LEFT:
367             case RIGHT:
368             case CENTER_X: {
369                 return target == Type.LEFT || target == Type.RIGHT || target == Type.CENTER_X;
370             }
371             case TOP:
372             case BOTTOM:
373             case CENTER_Y:
374             case BASELINE: {
375                 return target == Type.TOP || target == Type.BOTTOM
376                         || target == Type.CENTER_Y || target == Type.BASELINE;
377             }
378             case NONE:
379                 return false;
380         }
381         throw new AssertionError(mType.name());
382     }
383 
384     /**
385      * Set the margin of the connection (if there's one)
386      *
387      * @param margin the new margin of the connection
388      */
setMargin(int margin)389     public void setMargin(int margin) {
390         if (isConnected()) {
391             mMargin = margin;
392         }
393     }
394 
395     /**
396      * Set the gone margin of the connection (if there's one)
397      *
398      * @param margin the new margin of the connection
399      */
setGoneMargin(int margin)400     public void setGoneMargin(int margin) {
401         if (isConnected()) {
402             mGoneMargin = margin;
403         }
404     }
405 
406     /**
407      * Utility function returning true if this anchor is a vertical one.
408      *
409      * @return true if vertical anchor, false otherwise
410      */
isVerticalAnchor()411     public boolean isVerticalAnchor() {
412         switch (mType) {
413             case LEFT:
414             case RIGHT:
415             case CENTER:
416             case CENTER_X:
417                 return false;
418             case CENTER_Y:
419             case TOP:
420             case BOTTOM:
421             case BASELINE:
422             case NONE:
423                 return true;
424         }
425         throw new AssertionError(mType.name());
426     }
427 
428     /**
429      * Return a string representation of this anchor
430      *
431      * @return string representation of the anchor
432      */
433     @Override
toString()434     public String toString() {
435         return mOwner.getDebugName() + ":" + mType.toString();
436     }
437 
438     /**
439      * Return true if we can connect this anchor to this target.
440      * We recursively follow connections in order to detect eventual cycles; if we
441      * do we disallow the connection.
442      * We also only allow connections to direct parent, siblings, and descendants.
443      *
444      * @param target the ConstraintWidget we are trying to connect to
445      * @param anchor Allow anchor if it loops back to me directly
446      * @return if the connection is allowed, false otherwise
447      */
isConnectionAllowed(ConstraintWidget target, ConstraintAnchor anchor)448     public boolean isConnectionAllowed(ConstraintWidget target, ConstraintAnchor anchor) {
449         if (ALLOW_BINARY) {
450             if (anchor != null && anchor.getTarget() == this) {
451                 return true;
452             }
453         }
454         return isConnectionAllowed(target);
455     }
456 
457     /**
458      * Return true if we can connect this anchor to this target.
459      * We recursively follow connections in order to detect eventual cycles; if we
460      * do we disallow the connection.
461      * We also only allow connections to direct parent, siblings, and descendants.
462      *
463      * @param target the ConstraintWidget we are trying to connect to
464      * @return true if the connection is allowed, false otherwise
465      */
isConnectionAllowed(ConstraintWidget target)466     public boolean isConnectionAllowed(ConstraintWidget target) {
467         HashSet<ConstraintWidget> checked = new HashSet<>();
468         if (isConnectionToMe(target, checked)) {
469             return false;
470         }
471         ConstraintWidget parent = getOwner().getParent();
472         if (parent == target) { // allow connections to parent
473             return true;
474         }
475         if (target.getParent() == parent) { // allow if we share the same parent
476             return true;
477         }
478         return false;
479     }
480 
481     /**
482      * Recursive with check for loop
483      *
484      * @param checked set of things already checked
485      * @return true if it is connected to me
486      */
isConnectionToMe(ConstraintWidget target, HashSet<ConstraintWidget> checked)487     private boolean isConnectionToMe(ConstraintWidget target, HashSet<ConstraintWidget> checked) {
488         if (checked.contains(target)) {
489             return false;
490         }
491         checked.add(target);
492 
493         if (target == getOwner()) {
494             return true;
495         }
496         ArrayList<ConstraintAnchor> targetAnchors = target.getAnchors();
497         for (int i = 0, targetAnchorsSize = targetAnchors.size(); i < targetAnchorsSize; i++) {
498             ConstraintAnchor anchor = targetAnchors.get(i);
499             if (anchor.isSimilarDimensionConnection(this) && anchor.isConnected()) {
500                 if (isConnectionToMe(anchor.getTarget().getOwner(), checked)) {
501                     return true;
502                 }
503             }
504         }
505         return false;
506     }
507 
508     /**
509      * Returns the opposite anchor to this one
510      *
511      * @return opposite anchor
512      */
getOpposite()513     public final ConstraintAnchor getOpposite() {
514         switch (mType) {
515             case LEFT: {
516                 return mOwner.mRight;
517             }
518             case RIGHT: {
519                 return mOwner.mLeft;
520             }
521             case TOP: {
522                 return mOwner.mBottom;
523             }
524             case BOTTOM: {
525                 return mOwner.mTop;
526             }
527             case BASELINE:
528             case CENTER:
529             case CENTER_X:
530             case CENTER_Y:
531             case NONE:
532                 return null;
533         }
534         throw new AssertionError(mType.name());
535     }
536 }
537