1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php 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 com.android.ide.common.layout.relative; 17 18 import static com.android.ide.common.api.MarginType.NO_MARGIN; 19 import static com.android.ide.common.api.MarginType.WITHOUT_MARGIN; 20 import static com.android.ide.common.api.MarginType.WITH_MARGIN; 21 import static com.android.ide.common.api.SegmentType.BASELINE; 22 import static com.android.ide.common.api.SegmentType.BOTTOM; 23 import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL; 24 import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL; 25 import static com.android.ide.common.api.SegmentType.LEFT; 26 import static com.android.ide.common.api.SegmentType.RIGHT; 27 import static com.android.ide.common.api.SegmentType.TOP; 28 import static com.android.ide.common.layout.BaseLayoutRule.getMaxMatchDistance; 29 import static com.android.SdkConstants.ATTR_ID; 30 import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE; 31 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE; 32 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM; 33 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT; 34 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM; 35 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; 36 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; 37 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP; 38 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT; 39 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP; 40 import static com.android.SdkConstants.ATTR_LAYOUT_BELOW; 41 import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL; 42 import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT; 43 import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL; 44 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM; 45 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT; 46 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT; 47 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP; 48 import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF; 49 import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF; 50 import static com.android.SdkConstants.VALUE_N_DP; 51 import static com.android.SdkConstants.VALUE_TRUE; 52 import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE; 53 54 import static java.lang.Math.abs; 55 56 import com.android.SdkConstants; 57 import static com.android.SdkConstants.ANDROID_URI; 58 import com.android.ide.common.api.DropFeedback; 59 import com.android.ide.common.api.IClientRulesEngine; 60 import com.android.ide.common.api.INode; 61 import com.android.ide.common.api.Margins; 62 import com.android.ide.common.api.Rect; 63 import com.android.ide.common.api.Segment; 64 import com.android.ide.common.api.SegmentType; 65 import com.android.ide.common.layout.BaseLayoutRule; 66 import com.android.ide.common.layout.relative.DependencyGraph.Constraint; 67 import com.android.ide.common.layout.relative.DependencyGraph.ViewData; 68 69 import java.util.ArrayList; 70 import java.util.Collection; 71 import java.util.Collections; 72 import java.util.Comparator; 73 import java.util.List; 74 import java.util.Set; 75 76 /** 77 * The {@link GuidelineHandler} class keeps track of state related to a guideline operation 78 * like move and resize, and performs various constraint computations. 79 */ 80 public class GuidelineHandler { 81 /** 82 * A dependency graph for the relative layout recording constraint relationships 83 */ 84 protected DependencyGraph mDependencyGraph; 85 86 /** The RelativeLayout we are moving/resizing within */ 87 public INode layout; 88 89 /** The set of nodes being dragged (may be null) */ 90 protected Collection<INode> mDraggedNodes; 91 92 /** The bounds of the primary child node being dragged */ 93 protected Rect mBounds; 94 95 /** Whether the left edge is being moved/resized */ 96 protected boolean mMoveLeft; 97 98 /** Whether the right edge is being moved/resized */ 99 protected boolean mMoveRight; 100 101 /** Whether the top edge is being moved/resized */ 102 protected boolean mMoveTop; 103 104 /** Whether the bottom edge is being moved/resized */ 105 protected boolean mMoveBottom; 106 107 /** 108 * Whether the drop/move/resize position should be snapped (which can be turned off 109 * with a modifier key during the operation) 110 */ 111 protected boolean mSnap = true; 112 113 /** 114 * The set of nodes which depend on the currently selected nodes, including 115 * transitively, through horizontal constraints (a "horizontal constraint" 116 * is a constraint between two horizontal edges) 117 */ 118 protected Set<INode> mHorizontalDeps; 119 120 /** 121 * The set of nodes which depend on the currently selected nodes, including 122 * transitively, through vertical constraints (a "vertical constraint" 123 * is a constraint between two vertical edges) 124 */ 125 protected Set<INode> mVerticalDeps; 126 127 /** The current list of constraints which result in a horizontal cycle (if applicable) */ 128 protected List<Constraint> mHorizontalCycle; 129 130 /** The current list of constraints which result in a vertical cycle (if applicable) */ 131 protected List<Constraint> mVerticalCycle; 132 133 /** 134 * All horizontal segments in the relative layout - top and bottom edges, baseline 135 * edges, and top and bottom edges offset by the applicable margins in each direction 136 */ 137 protected List<Segment> mHorizontalEdges; 138 139 /** 140 * All vertical segments in the relative layout - left and right edges, and left and 141 * right edges offset by the applicable margins in each direction 142 */ 143 protected List<Segment> mVerticalEdges; 144 145 /** 146 * All center vertical segments in the relative layout. These are kept separate since 147 * they only match other center edges. 148 */ 149 protected List<Segment> mCenterVertEdges; 150 151 /** 152 * All center horizontal segments in the relative layout. These are kept separate 153 * since they only match other center edges. 154 */ 155 protected List<Segment> mCenterHorizEdges; 156 157 /** 158 * Suggestions for horizontal matches. There could be more than one, but all matches 159 * will be equidistant from the current position (as well as in the same direction, 160 * which means that you can't have one match 5 pixels to the left and one match 5 161 * pixels to the right since it would be impossible to snap to fit with both; you can 162 * however have multiple matches all 5 pixels to the left.) 163 * <p 164 * The best vertical match will be found in {@link #mCurrentTopMatch} or 165 * {@link #mCurrentBottomMatch}. 166 */ 167 protected List<Match> mHorizontalSuggestions; 168 169 /** 170 * Suggestions for vertical matches. 171 * <p 172 * The best vertical match will be found in {@link #mCurrentLeftMatch} or 173 * {@link #mCurrentRightMatch}. 174 */ 175 protected List<Match> mVerticalSuggestions; 176 177 /** 178 * The current match on the left edge, or null if no match or if the left edge is not 179 * being moved or resized. 180 */ 181 protected Match mCurrentLeftMatch; 182 183 /** 184 * The current match on the top edge, or null if no match or if the top edge is not 185 * being moved or resized. 186 */ 187 protected Match mCurrentTopMatch; 188 189 /** 190 * The current match on the right edge, or null if no match or if the right edge is 191 * not being moved or resized. 192 */ 193 protected Match mCurrentRightMatch; 194 195 /** 196 * The current match on the bottom edge, or null if no match or if the bottom edge is 197 * not being moved or resized. 198 */ 199 protected Match mCurrentBottomMatch; 200 201 /** 202 * The amount of margin to add to the top edge, or 0 203 */ 204 protected int mTopMargin; 205 206 /** 207 * The amount of margin to add to the bottom edge, or 0 208 */ 209 protected int mBottomMargin; 210 211 /** 212 * The amount of margin to add to the left edge, or 0 213 */ 214 protected int mLeftMargin; 215 216 /** 217 * The amount of margin to add to the right edge, or 0 218 */ 219 protected int mRightMargin; 220 221 /** 222 * The associated rules engine 223 */ 224 protected IClientRulesEngine mRulesEngine; 225 226 /** 227 * Construct a new {@link GuidelineHandler} for the given relative layout. 228 * 229 * @param layout the RelativeLayout to handle 230 */ 231 GuidelineHandler(INode layout, IClientRulesEngine rulesEngine) { 232 this.layout = layout; 233 mRulesEngine = rulesEngine; 234 235 mHorizontalEdges = new ArrayList<Segment>(); 236 mVerticalEdges = new ArrayList<Segment>(); 237 mCenterVertEdges = new ArrayList<Segment>(); 238 mCenterHorizEdges = new ArrayList<Segment>(); 239 mDependencyGraph = new DependencyGraph(layout); 240 } 241 242 /** 243 * Returns true if the handler has any suggestions to offer 244 * 245 * @return true if the handler has any suggestions to offer 246 */ 247 public boolean haveSuggestions() { 248 return mCurrentLeftMatch != null || mCurrentTopMatch != null 249 || mCurrentRightMatch != null || mCurrentBottomMatch != null; 250 } 251 252 /** 253 * Returns the closest match. 254 * 255 * @return the closest match, or null if nothing matched 256 */ 257 protected Match pickBestMatch(List<Match> matches) { 258 int alternatives = matches.size(); 259 if (alternatives == 0) { 260 return null; 261 } else if (alternatives == 1) { 262 Match match = matches.get(0); 263 return match; 264 } else { 265 assert alternatives > 1; 266 Collections.sort(matches, new MatchComparator()); 267 return matches.get(0); 268 } 269 } 270 271 private boolean checkCycle(DropFeedback feedback, Match match, boolean vertical) { 272 if (match != null && match.cycle) { 273 for (INode node : mDraggedNodes) { 274 INode from = match.edge.node; 275 assert match.with.node == null || match.with.node == node; 276 INode to = node; 277 List<Constraint> path = mDependencyGraph.getPathTo(from, to, vertical); 278 if (path != null) { 279 if (vertical) { 280 mVerticalCycle = path; 281 } else { 282 mHorizontalCycle = path; 283 } 284 String desc = Constraint.describePath(path, 285 match.type.name, match.edge.id); 286 287 feedback.errorMessage = "Constraint creates a cycle: " + desc; 288 return true; 289 } 290 } 291 } 292 293 return false; 294 } 295 296 /** 297 * Checks for any cycles in the dependencies 298 * 299 * @param feedback the drop feedback state 300 */ 301 public void checkCycles(DropFeedback feedback) { 302 // Deliberate short circuit evaluation -- only list the first cycle 303 feedback.errorMessage = null; 304 mHorizontalCycle = null; 305 mVerticalCycle = null; 306 307 if (checkCycle(feedback, mCurrentTopMatch, true /* vertical */) 308 || checkCycle(feedback, mCurrentBottomMatch, true)) { 309 } 310 311 if (checkCycle(feedback, mCurrentLeftMatch, false) 312 || checkCycle(feedback, mCurrentRightMatch, false)) { 313 } 314 } 315 316 /** Records the matchable outside edges for the given node to the potential match list */ 317 protected void addBounds(INode node, String id, 318 boolean addHorizontal, boolean addVertical) { 319 Rect b = node.getBounds(); 320 Margins margins = node.getMargins(); 321 if (addHorizontal) { 322 if (margins.top != 0) { 323 mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, WITHOUT_MARGIN)); 324 mHorizontalEdges.add(new Segment(b.y - margins.top, b.x, b.x2(), node, id, 325 TOP, WITH_MARGIN)); 326 } else { 327 mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, NO_MARGIN)); 328 } 329 if (margins.bottom != 0) { 330 mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id, BOTTOM, 331 WITHOUT_MARGIN)); 332 mHorizontalEdges.add(new Segment(b.y2() + margins.bottom, b.x, b.x2(), node, 333 id, BOTTOM, WITH_MARGIN)); 334 } else { 335 mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id, 336 BOTTOM, NO_MARGIN)); 337 } 338 } 339 if (addVertical) { 340 if (margins.left != 0) { 341 mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, WITHOUT_MARGIN)); 342 mVerticalEdges.add(new Segment(b.x - margins.left, b.y, b.y2(), node, id, LEFT, 343 WITH_MARGIN)); 344 } else { 345 mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, NO_MARGIN)); 346 } 347 348 if (margins.right != 0) { 349 mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id, 350 RIGHT, WITHOUT_MARGIN)); 351 mVerticalEdges.add(new Segment(b.x2() + margins.right, b.y, b.y2(), node, id, 352 RIGHT, WITH_MARGIN)); 353 } else { 354 mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id, 355 RIGHT, NO_MARGIN)); 356 } 357 } 358 } 359 360 /** Records the center edges for the given node to the potential match list */ 361 protected void addCenter(INode node, String id, 362 boolean addHorizontal, boolean addVertical) { 363 Rect b = node.getBounds(); 364 365 if (addHorizontal) { 366 mCenterHorizEdges.add(new Segment(b.centerY(), b.x, b.x2(), 367 node, id, CENTER_HORIZONTAL, NO_MARGIN)); 368 } 369 if (addVertical) { 370 mCenterVertEdges.add(new Segment(b.centerX(), b.y, b.y2(), 371 node, id, CENTER_VERTICAL, NO_MARGIN)); 372 } 373 } 374 375 /** Records the baseline edge for the given node to the potential match list */ 376 protected int addBaseLine(INode node, String id) { 377 int baselineY = node.getBaseline(); 378 if (baselineY != -1) { 379 Rect b = node.getBounds(); 380 mHorizontalEdges.add(new Segment(b.y + baselineY, b.x, b.x2(), node, id, BASELINE, 381 NO_MARGIN)); 382 } 383 384 return baselineY; 385 } 386 387 protected void snapVertical(Segment vEdge, int x, Rect newBounds) { 388 newBounds.x = x; 389 } 390 391 protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) { 392 newBounds.y = y; 393 } 394 395 /** 396 * Returns whether two edge types are compatible. For example, we only match the 397 * center of one object with the center of another. 398 * 399 * @param edge the first edge type to compare 400 * @param dragged the second edge type to compare the first one with 401 * @param delta the delta between the two edge locations 402 * @return true if the two edge types can be compatibly matched 403 */ 404 protected boolean isEdgeTypeCompatible(SegmentType edge, SegmentType dragged, int delta) { 405 406 if (Math.abs(delta) > BaseLayoutRule.getMaxMatchDistance()) { 407 if (dragged == LEFT || dragged == TOP) { 408 if (delta > 0) { 409 return false; 410 } 411 } else { 412 if (delta < 0) { 413 return false; 414 } 415 } 416 } 417 418 switch (edge) { 419 case BOTTOM: 420 case TOP: 421 return dragged == TOP || dragged == BOTTOM; 422 case LEFT: 423 case RIGHT: 424 return dragged == LEFT || dragged == RIGHT; 425 426 // Center horizontal, center vertical and Baseline only matches the same 427 // type, and only within the matching distance -- no margins! 428 case BASELINE: 429 case CENTER_HORIZONTAL: 430 case CENTER_VERTICAL: 431 return dragged == edge && Math.abs(delta) < getMaxMatchDistance(); 432 default: assert false : edge; 433 } 434 return false; 435 } 436 437 /** 438 * Finds the closest matching segments among the given list of edges for the given 439 * dragged edge, and returns these as a list of matches 440 */ 441 protected List<Match> findClosest(Segment draggedEdge, List<Segment> edges) { 442 List<Match> closest = new ArrayList<Match>(); 443 addClosest(draggedEdge, edges, closest); 444 return closest; 445 } 446 447 protected void addClosest(Segment draggedEdge, List<Segment> edges, 448 List<Match> closest) { 449 int at = draggedEdge.at; 450 int closestDelta = closest.size() > 0 ? closest.get(0).delta : Integer.MAX_VALUE; 451 int closestDistance = abs(closestDelta); 452 for (Segment edge : edges) { 453 assert draggedEdge.edgeType.isHorizontal() == edge.edgeType.isHorizontal(); 454 455 int delta = edge.at - at; 456 int distance = abs(delta); 457 if (distance > closestDistance) { 458 continue; 459 } 460 461 if (!isEdgeTypeCompatible(edge.edgeType, draggedEdge.edgeType, delta)) { 462 continue; 463 } 464 465 boolean withParent = edge.node == layout; 466 ConstraintType type = ConstraintType.forMatch(withParent, 467 draggedEdge.edgeType, edge.edgeType); 468 if (type == null) { 469 continue; 470 } 471 472 // Ensure that the edge match is compatible; for example, a "below" 473 // constraint can only apply to the margin bounds and a "bottom" 474 // constraint can only apply to the non-margin bounds. 475 if (type.relativeToMargin && edge.marginType == WITHOUT_MARGIN) { 476 continue; 477 } else if (!type.relativeToMargin && edge.marginType == WITH_MARGIN) { 478 continue; 479 } 480 481 Match match = new Match(this, edge, draggedEdge, type, delta); 482 483 if (distance < closestDistance) { 484 closest.clear(); 485 closestDistance = distance; 486 closestDelta = delta; 487 } else if (delta * closestDelta < 0) { 488 // They have different signs, e.g. the matches are equal but 489 // on opposite sides; can't accept them both 490 continue; 491 } 492 closest.add(match); 493 } 494 } 495 496 protected void clearSuggestions() { 497 mHorizontalSuggestions = mVerticalSuggestions = null; 498 mCurrentLeftMatch = mCurrentRightMatch = null; 499 mCurrentTopMatch = mCurrentBottomMatch = null; 500 } 501 502 /** 503 * Given a node, apply the suggestions by expressing them as relative layout param 504 * values 505 * 506 * @param n the node to apply constraints to 507 */ 508 public void applyConstraints(INode n) { 509 // Process each edge separately 510 String centerBoth = n.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT); 511 if (centerBoth != null && centerBoth.equals(VALUE_TRUE)) { 512 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, null); 513 514 // If you had a center-in-both-directions attribute, and you're 515 // only resizing in one dimension, then leave the other dimension 516 // centered, e.g. if you have centerInParent and apply alignLeft, 517 // then you should end up with alignLeft and centerVertically 518 if (mCurrentTopMatch == null && mCurrentBottomMatch == null) { 519 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE); 520 } 521 if (mCurrentLeftMatch == null && mCurrentRightMatch == null) { 522 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE); 523 } 524 } 525 526 if (mMoveTop) { 527 // Remove top attachments 528 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null); 529 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null); 530 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null); 531 532 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null); 533 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null); 534 535 } 536 537 if (mMoveBottom) { 538 // Remove bottom attachments 539 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null); 540 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null); 541 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null); 542 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null); 543 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null); 544 } 545 546 if (mMoveLeft) { 547 // Remove left attachments 548 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null); 549 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null); 550 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null); 551 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null); 552 } 553 554 if (mMoveRight) { 555 // Remove right attachments 556 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null); 557 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null); 558 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null); 559 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null); 560 } 561 562 if (mMoveTop && mCurrentTopMatch != null) { 563 applyConstraint(n, mCurrentTopMatch.getConstraint(true /* generateId */)); 564 if (mCurrentTopMatch.type == ALIGN_BASELINE) { 565 // HACK! WORKAROUND! Baseline doesn't provide a new bottom edge for attachments 566 String c = mCurrentTopMatch.getConstraint(true); 567 c = c.replace(ATTR_LAYOUT_ALIGN_BASELINE, ATTR_LAYOUT_ALIGN_BOTTOM); 568 applyConstraint(n, c); 569 } 570 } 571 572 if (mMoveBottom && mCurrentBottomMatch != null) { 573 applyConstraint(n, mCurrentBottomMatch.getConstraint(true)); 574 } 575 576 if (mMoveLeft && mCurrentLeftMatch != null) { 577 applyConstraint(n, mCurrentLeftMatch.getConstraint(true)); 578 } 579 580 if (mMoveRight && mCurrentRightMatch != null) { 581 applyConstraint(n, mCurrentRightMatch.getConstraint(true)); 582 } 583 584 if (mMoveLeft) { 585 applyMargin(n, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin); 586 } 587 if (mMoveRight) { 588 applyMargin(n, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin); 589 } 590 if (mMoveTop) { 591 applyMargin(n, ATTR_LAYOUT_MARGIN_TOP, mTopMargin); 592 } 593 if (mMoveBottom) { 594 applyMargin(n, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin); 595 } 596 } 597 598 private void applyConstraint(INode n, String constraint) { 599 assert constraint.contains("=") : constraint; 600 String name = constraint.substring(0, constraint.indexOf('=')); 601 String value = constraint.substring(constraint.indexOf('=') + 1); 602 n.setAttribute(ANDROID_URI, name, value); 603 } 604 605 private void applyMargin(INode n, String marginAttribute, int margin) { 606 if (margin > 0) { 607 int dp = mRulesEngine.pxToDp(margin); 608 n.setAttribute(ANDROID_URI, marginAttribute, String.format(VALUE_N_DP, dp)); 609 } else if (n.getStringAttr(ANDROID_URI, marginAttribute) != null) { 610 // Clear out existing margin 611 n.setAttribute(ANDROID_URI, marginAttribute, null); 612 } 613 } 614 615 private void removeRelativeParams(INode node) { 616 for (ConstraintType type : ConstraintType.values()) { 617 node.setAttribute(ANDROID_URI, type.name, null); 618 } 619 node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_LEFT, null); 620 node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_RIGHT, null); 621 node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_TOP, null); 622 node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_BOTTOM, null); 623 } 624 625 /** 626 * Attach the new child to the previous node 627 * @param previous the previous child 628 * @param node the new child to attach it to 629 */ 630 public void attachPrevious(INode previous, INode node) { 631 removeRelativeParams(node); 632 633 String id = previous.getStringAttr(ANDROID_URI, ATTR_ID); 634 if (id == null) { 635 return; 636 } 637 638 if (mCurrentTopMatch != null || mCurrentBottomMatch != null) { 639 // Attaching the top: arrange below, and for bottom arrange above 640 node.setAttribute(ANDROID_URI, 641 mCurrentTopMatch != null ? ATTR_LAYOUT_BELOW : ATTR_LAYOUT_ABOVE, id); 642 // Apply same left/right constraints as the parent 643 if (mCurrentLeftMatch != null) { 644 applyConstraint(node, mCurrentLeftMatch.getConstraint(true)); 645 applyMargin(node, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin); 646 } else if (mCurrentRightMatch != null) { 647 applyConstraint(node, mCurrentRightMatch.getConstraint(true)); 648 applyMargin(node, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin); 649 } 650 } else if (mCurrentLeftMatch != null || mCurrentRightMatch != null) { 651 node.setAttribute(ANDROID_URI, 652 mCurrentLeftMatch != null ? ATTR_LAYOUT_TO_RIGHT_OF : ATTR_LAYOUT_TO_LEFT_OF, 653 id); 654 // Apply same top/bottom constraints as the parent 655 if (mCurrentTopMatch != null) { 656 applyConstraint(node, mCurrentTopMatch.getConstraint(true)); 657 applyMargin(node, ATTR_LAYOUT_MARGIN_TOP, mTopMargin); 658 } else if (mCurrentBottomMatch != null) { 659 applyConstraint(node, mCurrentBottomMatch.getConstraint(true)); 660 applyMargin(node, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin); 661 } 662 } else { 663 return; 664 } 665 } 666 667 /** Breaks any cycles detected by the handler */ 668 public void removeCycles() { 669 if (mHorizontalCycle != null) { 670 removeCycles(mHorizontalDeps); 671 } 672 if (mVerticalCycle != null) { 673 removeCycles(mVerticalDeps); 674 } 675 } 676 677 private void removeCycles(Set<INode> deps) { 678 for (INode node : mDraggedNodes) { 679 ViewData view = mDependencyGraph.getView(node); 680 if (view != null) { 681 for (Constraint constraint : view.dependedOnBy) { 682 // For now, remove ALL constraints pointing to this node in this orientation. 683 // Later refine this to be smarter. (We can't JUST remove the constraints 684 // identified in the cycle since there could be multiple.) 685 constraint.from.node.setAttribute(ANDROID_URI, constraint.type.name, null); 686 } 687 } 688 } 689 } 690 691 /** 692 * Comparator used to sort matches such that the first match is the most desirable 693 * match (where we prefer attaching to parent bounds, we avoid matches that lead to a 694 * cycle, we prefer constraints on closer widgets rather than ones further away, and 695 * so on.) 696 * <p> 697 * There are a number of sorting criteria. One of them is the distance between the 698 * matched edges. We may end up with multiple matches that are the same distance. In 699 * that case we look at the orientation; on the left side, prefer left-oriented 700 * attachments, and on the right-side prefer right-oriented attachments. For example, 701 * consider the following scenario: 702 * 703 * <pre> 704 * +--------------------+-------------------------+ 705 * | Attached on left | | 706 * +--------------------+ | 707 * | | 708 * | +-----+ | 709 * | | A | | 710 * | +-----+ | 711 * | | 712 * | +-------------------------+ 713 * | | Attached on right | 714 * +--------------------+-------------------------+ 715 * </pre> 716 * 717 * Here, dragging the left edge should attach to the top left attached view, whereas 718 * in the following layout dragging the right edge would attach to the bottom view: 719 * 720 * <pre> 721 * +--------------------------+-------------------+ 722 * | Attached on left | | 723 * +--------------------------+ | 724 * | | 725 * | +-----+ | 726 * | | A | | 727 * | +-----+ | 728 * | | 729 * | +-------------------+ 730 * | | Attached on right | 731 * +--------------------------+-------------------+ 732 * 733 * </pre> 734 * 735 * </ul> 736 */ 737 private final class MatchComparator implements Comparator<Match> { 738 @Override 739 public int compare(Match m1, Match m2) { 740 // Always prefer matching parent bounds 741 int parent1 = m1.edge.node == layout ? -1 : 1; 742 int parent2 = m2.edge.node == layout ? -1 : 1; 743 // unless it's a center bound -- those should always get lowest priority since 744 // they overlap with other usually more interesting edges near the center of 745 // the layout. 746 if (m1.edge.edgeType == CENTER_HORIZONTAL 747 || m1.edge.edgeType == CENTER_VERTICAL) { 748 parent1 = 2; 749 } 750 if (m2.edge.edgeType == CENTER_HORIZONTAL 751 || m2.edge.edgeType == CENTER_VERTICAL) { 752 parent2 = 2; 753 } 754 if (parent1 != parent2) { 755 return parent1 - parent2; 756 } 757 758 // Avoid matching edges that would lead to a cycle 759 if (m1.edge.edgeType.isHorizontal()) { 760 int cycle1 = mHorizontalDeps.contains(m1.edge.node) ? 1 : -1; 761 int cycle2 = mHorizontalDeps.contains(m2.edge.node) ? 1 : -1; 762 if (cycle1 != cycle2) { 763 return cycle1 - cycle2; 764 } 765 } else { 766 int cycle1 = mVerticalDeps.contains(m1.edge.node) ? 1 : -1; 767 int cycle2 = mVerticalDeps.contains(m2.edge.node) ? 1 : -1; 768 if (cycle1 != cycle2) { 769 return cycle1 - cycle2; 770 } 771 } 772 773 // TODO: Sort by minimum depth -- do we have the depth anywhere? 774 775 // Prefer nodes that are closer 776 int distance1, distance2; 777 if (m1.edge.to <= m1.with.from) { 778 distance1 = m1.with.from - m1.edge.to; 779 } else if (m1.edge.from >= m1.with.to) { 780 distance1 = m1.edge.from - m1.with.to; 781 } else { 782 // Some kind of overlap - not sure how to prioritize these yet... 783 distance1 = 0; 784 } 785 if (m2.edge.to <= m2.with.from) { 786 distance2 = m2.with.from - m2.edge.to; 787 } else if (m2.edge.from >= m2.with.to) { 788 distance2 = m2.edge.from - m2.with.to; 789 } else { 790 // Some kind of overlap - not sure how to prioritize these yet... 791 distance2 = 0; 792 } 793 794 if (distance1 != distance2) { 795 return distance1 - distance2; 796 } 797 798 // Prefer matching on baseline 799 int baseline1 = (m1.edge.edgeType == BASELINE) ? -1 : 1; 800 int baseline2 = (m2.edge.edgeType == BASELINE) ? -1 : 1; 801 if (baseline1 != baseline2) { 802 return baseline1 - baseline2; 803 } 804 805 // Prefer matching top/left edges before matching bottom/right edges 806 int orientation1 = (m1.with.edgeType == LEFT || 807 m1.with.edgeType == TOP) ? -1 : 1; 808 int orientation2 = (m2.with.edgeType == LEFT || 809 m2.with.edgeType == TOP) ? -1 : 1; 810 if (orientation1 != orientation2) { 811 return orientation1 - orientation2; 812 } 813 814 // Prefer opposite-matching over same-matching. 815 // In other words, if we have the choice of matching 816 // our left edge with another element's left edge, 817 // or matching our left edge with another element's right 818 // edge, prefer the right edge since that 819 // The two matches have identical distance; try to sort by 820 // orientation 821 int edgeType1 = (m1.edge.edgeType != m1.with.edgeType) ? -1 : 1; 822 int edgeType2 = (m2.edge.edgeType != m2.with.edgeType) ? -1 : 1; 823 if (edgeType1 != edgeType2) { 824 return edgeType1 - edgeType2; 825 } 826 827 return 0; 828 } 829 } 830 831 /** 832 * Returns the {@link IClientRulesEngine} IDE callback 833 * 834 * @return the {@link IClientRulesEngine} IDE callback, never null 835 */ 836 public IClientRulesEngine getRulesEngine() { 837 return mRulesEngine; 838 } 839 } 840