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