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