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