1 /* 2 * Copyright (C) 2010 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 17 package com.android.ide.common.layout; 18 19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 20 import static com.android.ide.common.layout.LayoutConstants.ATTR_BASELINE_ALIGNED; 21 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY; 22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WEIGHT; 24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; 25 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; 26 import static com.android.ide.common.layout.LayoutConstants.ATTR_WEIGHT_SUM; 27 import static com.android.ide.common.layout.LayoutConstants.VALUE_1; 28 import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL; 29 import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; 30 import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; 31 import static com.android.ide.common.layout.LayoutConstants.VALUE_ZERO_DP; 32 33 import com.android.annotations.VisibleForTesting; 34 import com.android.ide.common.api.DrawingStyle; 35 import com.android.ide.common.api.DropFeedback; 36 import com.android.ide.common.api.IClientRulesEngine; 37 import com.android.ide.common.api.IDragElement; 38 import com.android.ide.common.api.IFeedbackPainter; 39 import com.android.ide.common.api.IGraphics; 40 import com.android.ide.common.api.IMenuCallback; 41 import com.android.ide.common.api.INode; 42 import com.android.ide.common.api.INodeHandler; 43 import com.android.ide.common.api.IViewMetadata; 44 import com.android.ide.common.api.IViewMetadata.FillPreference; 45 import com.android.ide.common.api.IViewRule; 46 import com.android.ide.common.api.InsertType; 47 import com.android.ide.common.api.Point; 48 import com.android.ide.common.api.Rect; 49 import com.android.ide.common.api.RuleAction; 50 import com.android.ide.common.api.RuleAction.Choices; 51 import com.android.ide.common.api.SegmentType; 52 import com.android.ide.eclipse.adt.AdtPlugin; 53 import com.android.sdklib.SdkConstants; 54 55 import java.net.URL; 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.Collections; 59 import java.util.List; 60 import java.util.Locale; 61 import java.util.Map; 62 63 /** 64 * An {@link IViewRule} for android.widget.LinearLayout and all its derived 65 * classes. 66 */ 67 public class LinearLayoutRule extends BaseLayoutRule { 68 private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$ 69 private static final String ACTION_WEIGHT = "_weight"; //$NON-NLS-1$ 70 private static final String ACTION_DISTRIBUTE = "_distribute"; //$NON-NLS-1$ 71 private static final String ACTION_BASELINE = "_baseline"; //$NON-NLS-1$ 72 private static final String ACTION_CLEAR = "_clear"; //$NON-NLS-1$ 73 private static final String ACTION_DOMINATE = "_dominate"; //$NON-NLS-1$ 74 75 private static final URL ICON_HORIZONTAL = 76 LinearLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$ 77 private static final URL ICON_VERTICAL = 78 LinearLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$ 79 private static final URL ICON_WEIGHTS = 80 LinearLayoutRule.class.getResource("weights.png"); //$NON-NLS-1$ 81 private static final URL ICON_DISTRIBUTE = 82 LinearLayoutRule.class.getResource("distribute.png"); //$NON-NLS-1$ 83 private static final URL ICON_BASELINE = 84 LinearLayoutRule.class.getResource("baseline.png"); //$NON-NLS-1$ 85 private static final URL ICON_CLEAR_WEIGHTS = 86 LinearLayoutRule.class.getResource("clearweights.png"); //$NON-NLS-1$ 87 private static final URL ICON_DOMINATE = 88 LinearLayoutRule.class.getResource("allweight.png"); //$NON-NLS-1$ 89 90 /** 91 * Returns the current orientation, regardless of whether it has been defined in XML 92 * 93 * @param node The LinearLayout to look up the orientation for 94 * @return "horizontal" or "vertical" depending on the current orientation of the 95 * linear layout 96 */ 97 private String getCurrentOrientation(final INode node) { 98 String orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION); 99 if (orientation == null || orientation.length() == 0) { 100 orientation = VALUE_HORIZONTAL; 101 } 102 return orientation; 103 } 104 105 /** 106 * Returns true if the given node represents a vertical linear layout. 107 * @param node the node to check layout orientation for 108 * @return true if the layout is in vertical mode, otherwise false 109 */ 110 protected boolean isVertical(INode node) { 111 // Horizontal is the default, so if no value is specified it is horizontal. 112 return VALUE_VERTICAL.equals(node.getStringAttr(ANDROID_URI, 113 ATTR_ORIENTATION)); 114 } 115 116 /** 117 * Returns true if this LinearLayout supports switching orientation. 118 * 119 * @return true if this layout supports orientations 120 */ 121 protected boolean supportsOrientation() { 122 return true; 123 } 124 125 @Override 126 public void addLayoutActions(List<RuleAction> actions, final INode parentNode, 127 final List<? extends INode> children) { 128 super.addLayoutActions(actions, parentNode, children); 129 if (supportsOrientation()) { 130 Choices action = RuleAction.createChoices( 131 ACTION_ORIENTATION, "Orientation", //$NON-NLS-1$ 132 new PropertyCallback(Collections.singletonList(parentNode), 133 "Change LinearLayout Orientation", 134 ANDROID_URI, ATTR_ORIENTATION), 135 Arrays.<String>asList("Set Horizontal Orientation","Set Vertical Orientation"), 136 Arrays.<URL>asList(ICON_HORIZONTAL, ICON_VERTICAL), 137 Arrays.<String>asList("horizontal", "vertical"), 138 getCurrentOrientation(parentNode), 139 null /* icon */, 140 -10, 141 false /* supportsMultipleNodes */ 142 ); 143 action.setRadio(true); 144 actions.add(action); 145 } 146 if (!isVertical(parentNode)) { 147 String current = parentNode.getStringAttr(ANDROID_URI, ATTR_BASELINE_ALIGNED); 148 boolean isAligned = current == null || Boolean.valueOf(current); 149 actions.add(RuleAction.createToggle(null, "Toggle Baseline Alignment", 150 isAligned, 151 new PropertyCallback(Collections.singletonList(parentNode), 152 "Change Baseline Alignment", 153 ANDROID_URI, ATTR_BASELINE_ALIGNED), // TODO: Also set index? 154 ICON_BASELINE, 38, false)); 155 } 156 157 // Gravity 158 if (children != null && children.size() > 0) { 159 actions.add(RuleAction.createSeparator(35)); 160 161 // Margins 162 actions.add(createMarginAction(parentNode, children)); 163 164 // Gravity 165 actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY)); 166 167 // Weights 168 IMenuCallback actionCallback = new IMenuCallback() { 169 @Override 170 public void action(final RuleAction action, List<? extends INode> selectedNodes, 171 final String valueId, final Boolean newValue) { 172 parentNode.editXml("Change Weight", new INodeHandler() { 173 @Override 174 public void handle(INode n) { 175 String id = action.getId(); 176 if (id.equals(ACTION_WEIGHT)) { 177 String weight = 178 children.get(0).getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT); 179 if (weight == null || weight.length() == 0) { 180 weight = "0.0"; //$NON-NLS-1$ 181 } 182 weight = mRulesEngine.displayInput("Enter Weight Value:", weight, 183 null); 184 if (weight != null) { 185 for (INode child : children) { 186 child.setAttribute(ANDROID_URI, 187 ATTR_LAYOUT_WEIGHT, weight); 188 } 189 } 190 } else if (id.equals(ACTION_DISTRIBUTE)) { 191 distributeWeights(parentNode, parentNode.getChildren()); 192 } else if (id.equals(ACTION_CLEAR)) { 193 clearWeights(parentNode); 194 } else if (id.equals(ACTION_CLEAR) || id.equals(ACTION_DOMINATE)) { 195 clearWeights(parentNode); 196 distributeWeights(parentNode, 197 children.toArray(new INode[children.size()])); 198 } else { 199 assert id.equals(ACTION_BASELINE); 200 } 201 } 202 }); 203 } 204 }; 205 actions.add(RuleAction.createSeparator(50)); 206 actions.add(RuleAction.createAction(ACTION_DISTRIBUTE, "Distribute Weights Evenly", 207 actionCallback, ICON_DISTRIBUTE, 60, false /*supportsMultipleNodes*/)); 208 actions.add(RuleAction.createAction(ACTION_DOMINATE, "Assign All Weight", 209 actionCallback, ICON_DOMINATE, 70, false)); 210 actions.add(RuleAction.createAction(ACTION_WEIGHT, "Change Layout Weight", 211 actionCallback, ICON_WEIGHTS, 80, false)); 212 actions.add(RuleAction.createAction(ACTION_CLEAR, "Clear All Weights", 213 actionCallback, ICON_CLEAR_WEIGHTS, 90, false)); 214 } 215 } 216 217 private void distributeWeights(INode parentNode, INode[] targets) { 218 // Any XML to get weight sum? 219 String weightSum = parentNode.getStringAttr(ANDROID_URI, 220 ATTR_WEIGHT_SUM); 221 double sum = -1.0; 222 if (weightSum != null) { 223 // Distribute 224 try { 225 sum = Double.parseDouble(weightSum); 226 } catch (NumberFormatException nfe) { 227 // Just keep using the default 228 } 229 } 230 int numTargets = targets.length; 231 double share; 232 if (sum <= 0.0) { 233 // The sum will be computed from the children, so just 234 // use arbitrary amount 235 share = 1.0; 236 } else { 237 share = sum / numTargets; 238 } 239 String value = formatFloatAttribute((float) share); 240 String sizeAttribute = isVertical(parentNode) ? 241 ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; 242 for (INode target : targets) { 243 target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value); 244 // Also set the width/height to 0dp to ensure actual equal 245 // size (without this, only the remaining space is 246 // distributed) 247 if (VALUE_WRAP_CONTENT.equals(target.getStringAttr(ANDROID_URI, sizeAttribute))) { 248 target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP); 249 } 250 } 251 } 252 253 private void clearWeights(INode parentNode) { 254 // Clear attributes 255 String sizeAttribute = isVertical(parentNode) 256 ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; 257 for (INode target : parentNode.getChildren()) { 258 target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); 259 String size = target.getStringAttr(ANDROID_URI, sizeAttribute); 260 if (size != null && size.startsWith("0")) { //$NON-NLS-1$ 261 target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_WRAP_CONTENT); 262 } 263 } 264 } 265 266 // ==== Drag'n'drop support ==== 267 268 @Override 269 public DropFeedback onDropEnter(final INode targetNode, Object targetView, 270 final IDragElement[] elements) { 271 272 if (elements.length == 0) { 273 return null; 274 } 275 276 Rect bn = targetNode.getBounds(); 277 if (!bn.isValid()) { 278 return null; 279 } 280 281 boolean isVertical = isVertical(targetNode); 282 283 // Prepare a list of insertion points: X coords for horizontal, Y for 284 // vertical. 285 List<MatchPos> indexes = new ArrayList<MatchPos>(); 286 287 int last = isVertical ? bn.y : bn.x; 288 int pos = 0; 289 boolean lastDragged = false; 290 int selfPos = -1; 291 for (INode it : targetNode.getChildren()) { 292 Rect bc = it.getBounds(); 293 if (bc.isValid()) { 294 // First see if this node looks like it's the same as one of the 295 // *dragged* bounds 296 boolean isDragged = false; 297 for (IDragElement element : elements) { 298 // This tries to determine if an INode corresponds to an 299 // IDragElement, by comparing their bounds. 300 if (bc.equals(element.getBounds())) { 301 isDragged = true; 302 } 303 } 304 305 // We don't want to insert drag positions before or after the 306 // element that is itself being dragged. However, we -do- want 307 // to insert a match position here, at the center, such that 308 // when you drag near its current position we show a match right 309 // where it's already positioned. 310 if (isDragged) { 311 int v = isVertical ? bc.y + (bc.h / 2) : bc.x + (bc.w / 2); 312 selfPos = pos; 313 indexes.add(new MatchPos(v, pos++)); 314 } else if (lastDragged) { 315 // Even though we don't want to insert a match below, we 316 // need to increment the index counter such that subsequent 317 // lines know their correct index in the child list. 318 pos++; 319 } else { 320 // Add an insertion point between the last point and the 321 // start of this child 322 int v = isVertical ? bc.y : bc.x; 323 v = (last + v) / 2; 324 indexes.add(new MatchPos(v, pos++)); 325 } 326 327 last = isVertical ? (bc.y + bc.h) : (bc.x + bc.w); 328 lastDragged = isDragged; 329 } else { 330 // We still have to count this position even if it has no bounds, or 331 // subsequent children will be inserted at the wrong place 332 pos++; 333 } 334 } 335 336 // Finally add an insert position after all the children - unless of 337 // course we happened to be dragging the last element 338 if (!lastDragged) { 339 int v = last + 1; 340 indexes.add(new MatchPos(v, pos)); 341 } 342 343 int posCount = targetNode.getChildren().length + 1; 344 return new DropFeedback(new LinearDropData(indexes, posCount, isVertical, selfPos), 345 new IFeedbackPainter() { 346 347 @Override 348 public void paint(IGraphics gc, INode node, DropFeedback feedback) { 349 // Paint callback for the LinearLayout. This is called 350 // by the canvas when a draw is needed. 351 drawFeedback(gc, node, elements, feedback); 352 } 353 }); 354 } 355 356 void drawFeedback(IGraphics gc, INode node, IDragElement[] elements, DropFeedback feedback) { 357 Rect b = node.getBounds(); 358 if (!b.isValid()) { 359 return; 360 } 361 362 // Highlight the receiver 363 gc.useStyle(DrawingStyle.DROP_RECIPIENT); 364 gc.drawRect(b); 365 366 gc.useStyle(DrawingStyle.DROP_ZONE); 367 368 LinearDropData data = (LinearDropData) feedback.userData; 369 boolean isVertical = data.isVertical(); 370 int selfPos = data.getSelfPos(); 371 372 for (MatchPos it : data.getIndexes()) { 373 int i = it.getDistance(); 374 int pos = it.getPosition(); 375 // Don't show insert drop zones for "self"-index since that one goes 376 // right through the center of the widget rather than in a sibling 377 // position 378 if (pos != selfPos) { 379 if (isVertical) { 380 // draw horizontal lines 381 gc.drawLine(b.x, i, b.x + b.w, i); 382 } else { 383 // draw vertical lines 384 gc.drawLine(i, b.y, i, b.y + b.h); 385 } 386 } 387 } 388 389 Integer currX = data.getCurrX(); 390 Integer currY = data.getCurrY(); 391 392 if (currX != null && currY != null) { 393 gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE); 394 395 int x = currX; 396 int y = currY; 397 398 Rect be = elements[0].getBounds(); 399 400 // Draw a clear line at the closest drop zone (unless we're over the 401 // dragged element itself) 402 if (data.getInsertPos() != selfPos || selfPos == -1) { 403 gc.useStyle(DrawingStyle.DROP_PREVIEW); 404 if (data.getWidth() != null) { 405 int width = data.getWidth(); 406 int fromX = x - width / 2; 407 int toX = x + width / 2; 408 gc.drawLine(fromX, y, toX, y); 409 } else if (data.getHeight() != null) { 410 int height = data.getHeight(); 411 int fromY = y - height / 2; 412 int toY = y + height / 2; 413 gc.drawLine(x, fromY, x, toY); 414 } 415 } 416 417 if (be.isValid()) { 418 boolean isLast = data.isLastPosition(); 419 420 // At least the first element has a bound. Draw rectangles for 421 // all dropped elements with valid bounds, offset at the drop 422 // point. 423 int offsetX; 424 int offsetY; 425 if (isVertical) { 426 offsetX = b.x - be.x; 427 offsetY = currY - be.y - (isLast ? 0 : (be.h / 2)); 428 429 } else { 430 offsetX = currX - be.x - (isLast ? 0 : (be.w / 2)); 431 offsetY = b.y - be.y; 432 } 433 434 gc.useStyle(DrawingStyle.DROP_PREVIEW); 435 for (IDragElement element : elements) { 436 Rect bounds = element.getBounds(); 437 if (bounds.isValid() && (bounds.w > b.w || bounds.h > b.h) && 438 node.getChildren().length == 0) { 439 // The bounds of the child does not fully fit inside the target. 440 // Limit the bounds to the layout bounds (but only when there 441 // are no children, since otherwise positioning around the existing 442 // children gets difficult) 443 final int px, py, pw, ph; 444 if (bounds.w > b.w) { 445 px = b.x; 446 pw = b.w; 447 } else { 448 px = bounds.x + offsetX; 449 pw = bounds.w; 450 } 451 if (bounds.h > b.h) { 452 py = b.y; 453 ph = b.h; 454 } else { 455 py = bounds.y + offsetY; 456 ph = bounds.h; 457 } 458 Rect within = new Rect(px, py, pw, ph); 459 gc.drawRect(within); 460 } else { 461 drawElement(gc, element, offsetX, offsetY); 462 } 463 } 464 } 465 } 466 } 467 468 @Override 469 public DropFeedback onDropMove(INode targetNode, IDragElement[] elements, 470 DropFeedback feedback, Point p) { 471 Rect b = targetNode.getBounds(); 472 if (!b.isValid()) { 473 return feedback; 474 } 475 476 LinearDropData data = (LinearDropData) feedback.userData; 477 boolean isVertical = data.isVertical(); 478 479 int bestDist = Integer.MAX_VALUE; 480 int bestIndex = Integer.MIN_VALUE; 481 Integer bestPos = null; 482 483 for (MatchPos index : data.getIndexes()) { 484 int i = index.getDistance(); 485 int pos = index.getPosition(); 486 int dist = (isVertical ? p.y : p.x) - i; 487 if (dist < 0) 488 dist = -dist; 489 if (dist < bestDist) { 490 bestDist = dist; 491 bestIndex = i; 492 bestPos = pos; 493 if (bestDist <= 0) 494 break; 495 } 496 } 497 498 if (bestIndex != Integer.MIN_VALUE) { 499 Integer oldX = data.getCurrX(); 500 Integer oldY = data.getCurrY(); 501 502 if (isVertical) { 503 data.setCurrX(b.x + b.w / 2); 504 data.setCurrY(bestIndex); 505 data.setWidth(b.w); 506 data.setHeight(null); 507 } else { 508 data.setCurrX(bestIndex); 509 data.setCurrY(b.y + b.h / 2); 510 data.setWidth(null); 511 data.setHeight(b.h); 512 } 513 514 data.setInsertPos(bestPos); 515 516 feedback.requestPaint = !equals(oldX, data.getCurrX()) 517 || !equals(oldY, data.getCurrY()); 518 } 519 520 return feedback; 521 } 522 523 private static boolean equals(Integer i1, Integer i2) { 524 if (i1 == i2) { 525 return true; 526 } else if (i1 != null) { 527 return i1.equals(i2); 528 } else { 529 // We know i2 != null 530 return i2.equals(i1); 531 } 532 } 533 534 @Override 535 public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) { 536 // ignore 537 } 538 539 @Override 540 public void onDropped(final INode targetNode, final IDragElement[] elements, 541 final DropFeedback feedback, final Point p) { 542 543 LinearDropData data = (LinearDropData) feedback.userData; 544 final int initialInsertPos = data.getInsertPos(); 545 insertAt(targetNode, elements, feedback.isCopy || !feedback.sameCanvas, initialInsertPos); 546 } 547 548 @Override 549 public void onChildInserted(INode node, INode parent, InsertType insertType) { 550 if (insertType == InsertType.MOVE_WITHIN) { 551 // Don't adjust widths/heights/weights when just moving within a single 552 // LinearLayout 553 return; 554 } 555 556 // Attempt to set fill-properties on newly added views such that for example, 557 // in a vertical layout, a text field defaults to filling horizontally, but not 558 // vertically. 559 String fqcn = node.getFqcn(); 560 IViewMetadata metadata = mRulesEngine.getMetadata(fqcn); 561 if (metadata != null) { 562 boolean vertical = isVertical(parent); 563 FillPreference fill = metadata.getFillPreference(); 564 String fillParent = getFillParentValueName(); 565 if (fill.fillHorizontally(vertical)) { 566 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent); 567 } else if (!vertical && fill == FillPreference.WIDTH_IN_VERTICAL) { 568 // In a horizontal layout, make views that would fill horizontally in a 569 // vertical layout have a non-zero weight instead. This will make the item 570 // fill but only enough to allow other views to be shown as well. 571 // (However, for drags within the same layout we do not touch 572 // the weight, since it might already have been tweaked to a particular 573 // value) 574 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, VALUE_1); 575 } 576 if (fill.fillVertically(vertical)) { 577 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent); 578 } 579 } 580 581 // If you insert into a layout that already is using layout weights, 582 // and all the layout weights are the same (nonzero) value, then use 583 // the same weight for this new layout as well. Also duplicate the 0dip/0px/0dp 584 // sizes, if used. 585 boolean duplicateWeight = true; 586 boolean duplicate0dip = true; 587 String sameWeight = null; 588 String sizeAttribute = isVertical(parent) ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; 589 for (INode target : parent.getChildren()) { 590 if (target == node) { 591 continue; 592 } 593 String weight = target.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT); 594 if (weight == null || weight.length() == 0) { 595 duplicateWeight = false; 596 break; 597 } else if (sameWeight != null && !sameWeight.equals(weight)) { 598 duplicateWeight = false; 599 } else { 600 sameWeight = weight; 601 } 602 String size = target.getStringAttr(ANDROID_URI, sizeAttribute); 603 if (size != null && !size.startsWith("0")) { //$NON-NLS-1$ 604 duplicate0dip = false; 605 break; 606 } 607 } 608 if (duplicateWeight && sameWeight != null) { 609 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, sameWeight); 610 if (duplicate0dip) { 611 node.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP); 612 } 613 } 614 } 615 616 /** A possible match position */ 617 private static class MatchPos { 618 /** The pixel distance */ 619 private int mDistance; 620 /** The position among siblings */ 621 private int mPosition; 622 623 public MatchPos(int distance, int position) { 624 this.mDistance = distance; 625 this.mPosition = position; 626 } 627 628 @Override 629 public String toString() { 630 return "MatchPos [distance=" + mDistance //$NON-NLS-1$ 631 + ", position=" + mPosition //$NON-NLS-1$ 632 + "]"; //$NON-NLS-1$ 633 } 634 635 private int getDistance() { 636 return mDistance; 637 } 638 639 private int getPosition() { 640 return mPosition; 641 } 642 } 643 644 private static class LinearDropData { 645 /** Vertical layout? */ 646 private final boolean mVertical; 647 648 /** Insert points (pixels + index) */ 649 private final List<MatchPos> mIndexes; 650 651 /** Number of insert positions in the target node */ 652 private final int mNumPositions; 653 654 /** Current marker X position */ 655 private Integer mCurrX; 656 657 /** Current marker Y position */ 658 private Integer mCurrY; 659 660 /** Position of the dragged element in this layout (or 661 -1 if the dragged element is from elsewhere) */ 662 private final int mSelfPos; 663 664 /** Current drop insert index (-1 for "at the end") */ 665 private int mInsertPos = -1; 666 667 /** width of match line if it's a horizontal one */ 668 private Integer mWidth; 669 670 /** height of match line if it's a vertical one */ 671 private Integer mHeight; 672 673 public LinearDropData(List<MatchPos> indexes, int numPositions, 674 boolean isVertical, int selfPos) { 675 this.mIndexes = indexes; 676 this.mNumPositions = numPositions; 677 this.mVertical = isVertical; 678 this.mSelfPos = selfPos; 679 } 680 681 @Override 682 public String toString() { 683 return "LinearDropData [currX=" + mCurrX //$NON-NLS-1$ 684 + ", currY=" + mCurrY //$NON-NLS-1$ 685 + ", height=" + mHeight //$NON-NLS-1$ 686 + ", indexes=" + mIndexes //$NON-NLS-1$ 687 + ", insertPos=" + mInsertPos //$NON-NLS-1$ 688 + ", isVertical=" + mVertical //$NON-NLS-1$ 689 + ", selfPos=" + mSelfPos //$NON-NLS-1$ 690 + ", width=" + mWidth //$NON-NLS-1$ 691 + "]"; //$NON-NLS-1$ 692 } 693 694 private boolean isVertical() { 695 return mVertical; 696 } 697 698 private void setCurrX(Integer currX) { 699 this.mCurrX = currX; 700 } 701 702 private Integer getCurrX() { 703 return mCurrX; 704 } 705 706 private void setCurrY(Integer currY) { 707 this.mCurrY = currY; 708 } 709 710 private Integer getCurrY() { 711 return mCurrY; 712 } 713 714 private int getSelfPos() { 715 return mSelfPos; 716 } 717 718 private void setInsertPos(int insertPos) { 719 this.mInsertPos = insertPos; 720 } 721 722 private int getInsertPos() { 723 return mInsertPos; 724 } 725 726 private List<MatchPos> getIndexes() { 727 return mIndexes; 728 } 729 730 private void setWidth(Integer width) { 731 this.mWidth = width; 732 } 733 734 private Integer getWidth() { 735 return mWidth; 736 } 737 738 private void setHeight(Integer height) { 739 this.mHeight = height; 740 } 741 742 private Integer getHeight() { 743 return mHeight; 744 } 745 746 /** 747 * Returns true if we are inserting into the last position 748 * 749 * @return true if we are inserting into the last position 750 */ 751 public boolean isLastPosition() { 752 return mInsertPos == mNumPositions - 1; 753 } 754 } 755 756 /** Custom resize state used during linear layout resizing */ 757 private class LinearResizeState extends ResizeState { 758 /** Whether the node should be assigned a new weight */ 759 public boolean useWeight; 760 /** Weight sum to be applied to the parent */ 761 private float mNewWeightSum; 762 /** The weight to be set on the node (provided {@link #useWeight} is true) */ 763 private float mWeight; 764 /** Map from nodes to preferred bounds of nodes where the weights have been cleared */ 765 public final Map<INode, Rect> unweightedSizes; 766 /** Total required size required by the siblings <b>without</b> weights */ 767 public int totalLength; 768 /** List of nodes which should have their weights cleared */ 769 public List<INode> mClearWeights; 770 771 private LinearResizeState(BaseLayoutRule rule, INode layout, Object layoutView, 772 INode node) { 773 super(rule, layout, layoutView, node); 774 775 unweightedSizes = mRulesEngine.measureChildren(layout, 776 new IClientRulesEngine.AttributeFilter() { 777 @Override 778 public String getAttribute(INode n, String namespace, String localName) { 779 // Clear out layout weights; we need to measure the unweighted sizes 780 // of the children 781 if (ATTR_LAYOUT_WEIGHT.equals(localName) 782 && SdkConstants.NS_RESOURCES.equals(namespace)) { 783 return ""; //$NON-NLS-1$ 784 } 785 786 return null; 787 } 788 }); 789 790 // Compute total required size required by the siblings *without* weights 791 totalLength = 0; 792 final boolean isVertical = isVertical(layout); 793 for (Map.Entry<INode, Rect> entry : unweightedSizes.entrySet()) { 794 Rect preferredSize = entry.getValue(); 795 if (isVertical) { 796 totalLength += preferredSize.h; 797 } else { 798 totalLength += preferredSize.w; 799 } 800 } 801 } 802 803 /** Resets the computed state */ 804 void reset() { 805 mNewWeightSum = -1; 806 useWeight = false; 807 mClearWeights = null; 808 } 809 810 /** Sets a weight to be applied to the node */ 811 void setWeight(float weight) { 812 useWeight = true; 813 mWeight = weight; 814 } 815 816 /** Sets a weight sum to be applied to the parent layout */ 817 void setWeightSum(float weightSum) { 818 mNewWeightSum = weightSum; 819 } 820 821 /** Marks that the given node should be cleared when applying the new size */ 822 void clearWeight(INode n) { 823 if (mClearWeights == null) { 824 mClearWeights = new ArrayList<INode>(); 825 } 826 mClearWeights.add(n); 827 } 828 829 /** Applies the state to the nodes */ 830 public void apply() { 831 assert useWeight; 832 833 String value = mWeight > 0 ? formatFloatAttribute(mWeight) : null; 834 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value); 835 836 if (mClearWeights != null) { 837 for (INode n : mClearWeights) { 838 if (getWeight(n) > 0.0f) { 839 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); 840 } 841 } 842 } 843 844 if (mNewWeightSum > 0.0) { 845 layout.setAttribute(ANDROID_URI, ATTR_WEIGHT_SUM, 846 formatFloatAttribute(mNewWeightSum)); 847 } 848 } 849 } 850 851 @Override 852 protected ResizeState createResizeState(INode layout, Object layoutView, INode node) { 853 return new LinearResizeState(this, layout, layoutView, node); 854 } 855 856 protected void updateResizeState(LinearResizeState resizeState, final INode node, INode layout, 857 Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, 858 SegmentType verticalEdge) { 859 // Update the resize state. 860 // This method attempts to compute a new layout weight to be used in the direction 861 // of the linear layout. If the superclass has already determined that we can snap to 862 // a wrap_content or match_parent boundary, we prefer that. Otherwise, we attempt to 863 // compute a layout weight - which can fail if the size is too big (not enough room), 864 // or if the size is too small (smaller than the natural width of the node), and so on. 865 // In that case this method just aborts, which will leave the resize state object 866 // in such a state that it will call the superclass to resize instead, which will fall 867 // back to device independent pixel sizing. 868 resizeState.reset(); 869 870 if (oldBounds.equals(newBounds)) { 871 return; 872 } 873 874 // If we're setting the width/height to wrap_content/match_parent in the dimension of the 875 // linear layout, then just apply wrap_content and clear weights. 876 boolean isVertical = isVertical(layout); 877 if (!isVertical && verticalEdge != null) { 878 if (resizeState.wrapWidth || resizeState.fillWidth) { 879 resizeState.clearWeight(node); 880 return; 881 } 882 if (newBounds.w == oldBounds.w) { 883 return; 884 } 885 } 886 887 if (isVertical && horizontalEdge != null) { 888 if (resizeState.wrapHeight || resizeState.fillHeight) { 889 resizeState.clearWeight(node); 890 return; 891 } 892 if (newBounds.h == oldBounds.h) { 893 return; 894 } 895 } 896 897 // Compute weight sum 898 float sum = getWeightSum(layout); 899 if (sum <= 0.0f) { 900 sum = 1.0f; 901 resizeState.setWeightSum(sum); 902 } 903 904 // If the new size of the node is smaller than its preferred/wrap_content size, 905 // then we cannot use weights to size it; switch to pixel-based sizing instead 906 Map<INode, Rect> sizes = resizeState.unweightedSizes; 907 Rect nodePreferredSize = sizes.get(node); 908 if (nodePreferredSize != null) { 909 if (horizontalEdge != null && newBounds.h < nodePreferredSize.h || 910 verticalEdge != null && newBounds.w < nodePreferredSize.w) { 911 return; 912 } 913 } 914 915 Rect layoutBounds = layout.getBounds(); 916 int remaining = (isVertical ? layoutBounds.h : layoutBounds.w) - resizeState.totalLength; 917 Rect nodeBounds = sizes.get(node); 918 if (nodeBounds == null) { 919 return; 920 } 921 922 if (remaining > 0) { 923 int missing = 0; 924 if (isVertical) { 925 if (newBounds.h > nodeBounds.h) { 926 missing = newBounds.h - nodeBounds.h; 927 } else if (newBounds.h > resizeState.wrapBounds.h) { 928 // The weights concern how much space to ADD to the view. 929 // What if we have resized it to a size *smaller* than its current 930 // size without the weight delta? This can happen if you for example 931 // have set a hardcoded size, such as 500dp, and then size it to some 932 // smaller size. 933 missing = newBounds.h - resizeState.wrapBounds.h; 934 remaining += nodeBounds.h - resizeState.wrapBounds.h; 935 resizeState.wrapHeight = true; 936 } 937 } else { 938 if (newBounds.w > nodeBounds.w) { 939 missing = newBounds.w - nodeBounds.w; 940 } else if (newBounds.w > resizeState.wrapBounds.w) { 941 missing = newBounds.w - resizeState.wrapBounds.w; 942 remaining += nodeBounds.w - resizeState.wrapBounds.w; 943 resizeState.wrapWidth = true; 944 } 945 } 946 if (missing > 0) { 947 // (weight / weightSum) * remaining = missing, so 948 // weight = missing * weightSum / remaining 949 float weight = missing * sum / remaining; 950 resizeState.setWeight(weight); 951 } 952 } 953 } 954 955 /** 956 * {@inheritDoc} 957 * <p> 958 * Overridden in this layout in order to make resizing affect the layout_weight 959 * attribute instead of the layout_width (for horizontal LinearLayouts) or 960 * layout_height (for vertical LinearLayouts). 961 */ 962 @Override 963 protected void setNewSizeBounds(ResizeState state, final INode node, INode layout, 964 Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, 965 SegmentType verticalEdge) { 966 LinearResizeState resizeState = (LinearResizeState) state; 967 updateResizeState(resizeState, node, layout, oldBounds, newBounds, 968 horizontalEdge, verticalEdge); 969 970 if (resizeState.useWeight) { 971 resizeState.apply(); 972 973 // Handle resizing in the opposite dimension of the layout 974 final boolean isVertical = isVertical(layout); 975 if (!isVertical && horizontalEdge != null) { 976 if (newBounds.h != oldBounds.h || resizeState.wrapHeight 977 || resizeState.fillHeight) { 978 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, 979 resizeState.getHeightAttribute()); 980 } 981 } 982 if (isVertical && verticalEdge != null) { 983 if (newBounds.w != oldBounds.w || resizeState.wrapWidth || resizeState.fillWidth) { 984 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, 985 resizeState.getWidthAttribute()); 986 } 987 } 988 } else { 989 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); 990 super.setNewSizeBounds(resizeState, node, layout, oldBounds, newBounds, 991 horizontalEdge, verticalEdge); 992 } 993 } 994 995 @Override 996 protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent, 997 Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { 998 LinearResizeState resizeState = (LinearResizeState) state; 999 updateResizeState(resizeState, child, parent, child.getBounds(), newBounds, 1000 horizontalEdge, verticalEdge); 1001 1002 if (resizeState.useWeight) { 1003 String weight = formatFloatAttribute(resizeState.mWeight); 1004 String dimension = String.format("weight %1$s", weight); 1005 1006 String width; 1007 String height; 1008 if (isVertical(parent)) { 1009 width = resizeState.getWidthAttribute(); 1010 height = dimension; 1011 } else { 1012 width = dimension; 1013 height = resizeState.getHeightAttribute(); 1014 } 1015 1016 if (horizontalEdge == null) { 1017 return width; 1018 } else if (verticalEdge == null) { 1019 return height; 1020 } else { 1021 // U+00D7: Unicode for multiplication sign 1022 return String.format("%s \u00D7 %s", width, height); 1023 } 1024 } else { 1025 return super.getResizeUpdateMessage(state, child, parent, newBounds, 1026 horizontalEdge, verticalEdge); 1027 } 1028 } 1029 1030 /** 1031 * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it 1032 * does not define a weight 1033 */ 1034 private static float getWeight(INode linearLayoutChild) { 1035 String weight = linearLayoutChild.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT); 1036 if (weight != null && weight.length() > 0) { 1037 try { 1038 return Float.parseFloat(weight); 1039 } catch (NumberFormatException nfe) { 1040 AdtPlugin.log(nfe, "Invalid weight %1$s", weight); 1041 } 1042 } 1043 1044 return 0.0f; 1045 } 1046 1047 /** 1048 * Returns the sum of all the layout weights of the children in the given LinearLayout 1049 * 1050 * @param linearLayout the layout to compute the total sum for 1051 * @return the total sum of all the layout weights in the given layout 1052 */ 1053 private static float getWeightSum(INode linearLayout) { 1054 String weightSum = linearLayout.getStringAttr(ANDROID_URI, 1055 ATTR_WEIGHT_SUM); 1056 float sum = -1.0f; 1057 if (weightSum != null) { 1058 // Distribute 1059 try { 1060 sum = Float.parseFloat(weightSum); 1061 return sum; 1062 } catch (NumberFormatException nfe) { 1063 // Just keep using the default 1064 } 1065 } 1066 1067 return getSumOfWeights(linearLayout); 1068 } 1069 1070 private static float getSumOfWeights(INode linearLayout) { 1071 float sum = 0.0f; 1072 for (INode child : linearLayout.getChildren()) { 1073 sum += getWeight(child); 1074 } 1075 1076 return sum; 1077 } 1078 1079 @VisibleForTesting 1080 static String formatFloatAttribute(float value) { 1081 if (value != (int) value) { 1082 // Run String.format without a locale, because we don't want locale-specific 1083 // conversions here like separating the decimal part with a comma instead of a dot! 1084 return String.format((Locale) null, "%.2f", value); //$NON-NLS-1$ 1085 } else { 1086 return Integer.toString((int) value); 1087 } 1088 } 1089 } 1090