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