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