1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.ide.common.layout.grid; 17 18 import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM; 19 import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ; 20 import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT; 21 import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT; 22 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT; 24 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; 25 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN; 26 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_SPAN; 27 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY; 28 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; 29 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW; 30 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW_SPAN; 31 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; 32 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; 33 import static com.android.ide.common.layout.LayoutConstants.ATTR_ROW_COUNT; 34 import static com.android.ide.common.layout.LayoutConstants.FQCN_GRID_LAYOUT; 35 import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE; 36 import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE_V7; 37 import static com.android.ide.common.layout.LayoutConstants.GRID_LAYOUT; 38 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; 39 import static com.android.ide.common.layout.LayoutConstants.SPACE; 40 import static com.android.ide.common.layout.LayoutConstants.VALUE_BOTTOM; 41 import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_VERTICAL; 42 import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP; 43 import static com.android.ide.common.layout.LayoutConstants.VALUE_TOP; 44 import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; 45 import static java.lang.Math.abs; 46 import static java.lang.Math.max; 47 import static java.lang.Math.min; 48 49 import com.android.ide.common.api.IClientRulesEngine; 50 import com.android.ide.common.api.INode; 51 import com.android.ide.common.api.IViewMetadata; 52 import com.android.ide.common.api.Margins; 53 import com.android.ide.common.api.Rect; 54 import com.android.ide.common.layout.GravityHelper; 55 import com.android.ide.common.layout.GridLayoutRule; 56 import com.android.util.Pair; 57 58 import java.io.PrintWriter; 59 import java.io.StringWriter; 60 import java.lang.reflect.Field; 61 import java.util.ArrayList; 62 import java.util.Arrays; 63 import java.util.Collection; 64 import java.util.Collections; 65 import java.util.HashMap; 66 import java.util.HashSet; 67 import java.util.List; 68 import java.util.Map; 69 import java.util.Set; 70 import java.util.regex.Matcher; 71 import java.util.regex.Pattern; 72 73 /** Models a GridLayout */ 74 public class GridModel { 75 /** Marker value used to indicate values (rows, columns, etc) which have not been set */ 76 static final int UNDEFINED = Integer.MIN_VALUE; 77 78 /** The size of spacers in the dimension that they are not defining */ 79 private static final int SPACER_SIZE_DP = 1; 80 /** Attribute value used for {@link #SPACER_SIZE_DP} */ 81 private static final String SPACER_SIZE = String.format(VALUE_N_DP, SPACER_SIZE_DP); 82 /** Width assigned to a newly added column with the Add Column action */ 83 private static final int DEFAULT_CELL_WIDTH = 100; 84 /** Height assigned to a newly added row with the Add Row action */ 85 private static final int DEFAULT_CELL_HEIGHT = 15; 86 private static final Pattern DIP_PATTERN = Pattern.compile("(\\d+)dp"); //$NON-NLS-1$ 87 88 /** The GridLayout node, never null */ 89 public final INode layout; 90 91 /** True if this is a vertical layout, and false if it is horizontal (the default) */ 92 public boolean vertical; 93 /** The declared count of rows (which may be {@link #UNDEFINED} if not specified) */ 94 public int declaredRowCount; 95 /** The declared count of columns (which may be {@link #UNDEFINED} if not specified) */ 96 public int declaredColumnCount; 97 /** The actual count of rows found in the grid */ 98 public int actualRowCount; 99 /** The actual count of columns found in the grid */ 100 public int actualColumnCount; 101 102 /** 103 * Array of positions (indexed by column) of the left edge of table cells; this 104 * corresponds to the column positions in the grid 105 */ 106 private int[] mLeft; 107 108 /** 109 * Array of positions (indexed by row) of the top edge of table cells; this 110 * corresponds to the row positions in the grid 111 */ 112 private int[] mTop; 113 114 /** 115 * Array of positions (indexed by column) of the maximum right hand side bounds of a 116 * node in the given column; this represents the visual edge of a column even when the 117 * actual column is wider 118 */ 119 private int[] mMaxRight; 120 121 /** 122 * Array of positions (indexed by row) of the maximum bottom bounds of a node in the 123 * given row; this represents the visual edge of a row even when the actual row is 124 * taller 125 */ 126 private int[] mMaxBottom; 127 128 /** 129 * Array of baselines computed for the rows. This array is populated lazily and should 130 * not be accessed directly; call {@link #getBaseline(int)} instead. 131 */ 132 private int[] mBaselines; 133 134 /** List of all the view data for the children in this layout */ 135 private List<ViewData> mChildViews; 136 137 /** The {@link IClientRulesEngine} */ 138 private final IClientRulesEngine mRulesEngine; 139 140 /** List of nodes marked for deletion (may be null) */ 141 private Set<INode> mDeleted; 142 143 /** 144 * Flag which tracks whether we've edited the DOM model, in which case the grid data 145 * may be stale and should be refreshed. 146 */ 147 private boolean stale; 148 149 /** 150 * An actual instance of a GridLayout object that this grid model corresponds to. 151 */ 152 private Object mViewObject; 153 154 /** The namespace to use for attributes */ 155 private String mNamespace; 156 157 /** 158 * Constructs a {@link GridModel} for the given layout 159 * 160 * @param rulesEngine the associated rules engine 161 * @param node the GridLayout node 162 * @param viewObject an actual GridLayout instance, or null 163 */ 164 public GridModel(IClientRulesEngine rulesEngine, INode node, Object viewObject) { 165 mRulesEngine = rulesEngine; 166 layout = node; 167 mViewObject = viewObject; 168 loadFromXml(); 169 } 170 171 /** 172 * Returns the {@link ViewData} for the child at the given index 173 * 174 * @param index the position of the child node whose view we want to look up 175 * @return the corresponding {@link ViewData} 176 */ 177 public ViewData getView(int index) { 178 return mChildViews.get(index); 179 } 180 181 /** 182 * Returns the {@link ViewData} for the given child node. 183 * 184 * @param node the node for which we want the view info 185 * @return the view info for the node, or null if not found 186 */ 187 public ViewData getView(INode node) { 188 for (ViewData view : mChildViews) { 189 if (view.node == node) { 190 return view; 191 } 192 } 193 194 return null; 195 } 196 197 /** 198 * Computes the index (among the children nodes) to insert a new node into which 199 * should be positioned at the given row and column. This will skip over any nodes 200 * that have implicit positions earlier than the given node, and will also ensure that 201 * all nodes are placed before the spacer nodes. 202 * 203 * @param row the target row of the new node 204 * @param column the target column of the new node 205 * @return the insert position to use or -1 if no preference is found 206 */ 207 public int getInsertIndex(int row, int column) { 208 if (vertical) { 209 for (ViewData view : mChildViews) { 210 if (view.column > column || view.column == column && view.row >= row) { 211 return view.index; 212 } 213 } 214 } else { 215 for (ViewData view : mChildViews) { 216 if (view.row > row || view.row == row && view.column >= column) { 217 return view.index; 218 } 219 } 220 } 221 222 // Place it before the first spacer 223 for (ViewData view : mChildViews) { 224 if (view.isSpacer()) { 225 return view.index; 226 } 227 } 228 229 return -1; 230 } 231 232 /** 233 * Returns the baseline of the given row, or -1 if none is found. This looks for views 234 * in the row which have baseline vertical alignment and also define their own 235 * baseline, and returns the first such match. 236 * 237 * @param row the row to look up a baseline for 238 * @return the baseline relative to the row position, or -1 if not defined 239 */ 240 public int getBaseline(int row) { 241 if (row < 0 || row >= mBaselines.length) { 242 return -1; 243 } 244 245 int baseline = mBaselines[row]; 246 if (baseline == UNDEFINED) { 247 baseline = -1; 248 249 // TBD: Consider stringing together row information in the view data 250 // so I can quickly identify the views in a given row instead of searching 251 // among all? 252 for (ViewData view : mChildViews) { 253 // We only count baselines for views with rowSpan=1 because 254 // baseline alignment doesn't work for cell spanning views 255 if (view.row == row && view.rowSpan == 1) { 256 baseline = view.node.getBaseline(); 257 if (baseline != -1) { 258 // Even views that do have baselines do not count towards a row 259 // baseline if they have a vertical gravity 260 String gravity = getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY); 261 if (gravity == null 262 || !(gravity.contains(VALUE_TOP) 263 || gravity.contains(VALUE_BOTTOM) 264 || gravity.contains(VALUE_CENTER_VERTICAL))) { 265 // Compute baseline relative to the row, not the view itself 266 baseline += view.node.getBounds().y - getRowY(row); 267 break; 268 } 269 } 270 } 271 } 272 mBaselines[row] = baseline; 273 } 274 275 return baseline; 276 } 277 278 /** Applies the row and column values into the XML */ 279 void applyPositionAttributes() { 280 for (ViewData view : mChildViews) { 281 view.applyPositionAttributes(); 282 } 283 284 // Also fix the columnCount 285 if (getGridAttribute(layout, ATTR_COLUMN_COUNT) != null && 286 declaredColumnCount > actualColumnCount) { 287 setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount); 288 } 289 } 290 291 /** 292 * Sets the given GridLayout attribute (rowCount, layout_row, etc) to the 293 * given value. This automatically handles using the right XML namespace 294 * based on whether the GridLayout is the android.widget.GridLayout, or the 295 * support library GridLayout, and whether it's in a library project or not 296 * etc. 297 * 298 * @param node the node to apply the attribute to 299 * @param name the local name of the attribute 300 * @param value the integer value to set the attribute to 301 */ 302 public void setGridAttribute(INode node, String name, int value) { 303 setGridAttribute(node, name, Integer.toString(value)); 304 } 305 306 /** 307 * Sets the given GridLayout attribute (rowCount, layout_row, etc) to the 308 * given value. This automatically handles using the right XML namespace 309 * based on whether the GridLayout is the android.widget.GridLayout, or the 310 * support library GridLayout, and whether it's in a library project or not 311 * etc. 312 * 313 * @param node the node to apply the attribute to 314 * @param name the local name of the attribute 315 * @param value the string value to set the attribute to, or null to clear 316 * it 317 */ 318 public void setGridAttribute(INode node, String name, String value) { 319 node.setAttribute(getNamespace(), name, value); 320 } 321 322 /** 323 * Returns the namespace URI to use for GridLayout-specific attributes, such 324 * as columnCount, layout_column, layout_column_span, layout_gravity etc. 325 * 326 * @return the namespace, never null 327 */ 328 public String getNamespace() { 329 if (mNamespace == null) { 330 mNamespace = ANDROID_URI; 331 332 if (!layout.getFqcn().equals(FQCN_GRID_LAYOUT)) { 333 mNamespace = mRulesEngine.getAppNameSpace(); 334 } 335 } 336 337 return mNamespace; 338 } 339 340 /** Removes the given flag from a flag attribute value and returns the result */ 341 static String removeFlag(String flag, String value) { 342 if (value.equals(flag)) { 343 return null; 344 } 345 // Handle spaces between pipes and flag are a prefix, suffix and interior occurrences 346 int index = value.indexOf(flag); 347 if (index != -1) { 348 int pipe = value.lastIndexOf('|', index); 349 int endIndex = index + flag.length(); 350 if (pipe != -1) { 351 value = value.substring(0, pipe).trim() + value.substring(endIndex).trim(); 352 } else { 353 pipe = value.indexOf('|', endIndex); 354 if (pipe != -1) { 355 value = value.substring(0, index).trim() + value.substring(pipe + 1).trim(); 356 } else { 357 value = value.substring(0, index).trim() + value.substring(endIndex).trim(); 358 } 359 } 360 } 361 362 return value; 363 } 364 365 /** 366 * Loads a {@link GridModel} from the XML model. 367 */ 368 void loadFromXml() { 369 INode[] children = layout.getChildren(); 370 371 declaredRowCount = getGridAttribute(layout, ATTR_ROW_COUNT, UNDEFINED); 372 declaredColumnCount = getGridAttribute(layout, ATTR_COLUMN_COUNT, UNDEFINED); 373 // Horizontal is the default, so if no value is specified it is horizontal. 374 vertical = VALUE_VERTICAL.equals(getGridAttribute(layout, ATTR_ORIENTATION)); 375 376 mChildViews = new ArrayList<ViewData>(children.length); 377 int index = 0; 378 for (INode child : children) { 379 ViewData view = new ViewData(child, index++); 380 mChildViews.add(view); 381 } 382 383 // Assign row/column positions to all cells that do not explicitly define them 384 assignRowsAndColumns( 385 declaredRowCount == UNDEFINED ? children.length : declaredRowCount, 386 declaredColumnCount == UNDEFINED ? children.length : declaredColumnCount); 387 388 assignCellBounds(); 389 390 for (int i = 0; i <= actualRowCount; i++) { 391 mBaselines[i] = UNDEFINED; 392 } 393 394 stale = false; 395 } 396 397 private Pair<Map<Integer, Integer>, Map<Integer, Integer>> findCellsOutsideDeclaredBounds() { 398 // See if we have any (row,column) pairs that fall outside the declared 399 // bounds; for these we identify the number of unique values and assign these 400 // consecutive values 401 Map<Integer, Integer> extraColumnsMap = null; 402 Map<Integer, Integer> extraRowsMap = null; 403 if (declaredRowCount != UNDEFINED) { 404 Set<Integer> extraRows = null; 405 for (ViewData view : mChildViews) { 406 if (view.row >= declaredRowCount) { 407 if (extraRows == null) { 408 extraRows = new HashSet<Integer>(); 409 } 410 extraRows.add(view.row); 411 } 412 } 413 if (extraRows != null && declaredRowCount != UNDEFINED) { 414 List<Integer> rows = new ArrayList<Integer>(extraRows); 415 Collections.sort(rows); 416 int row = declaredRowCount; 417 extraRowsMap = new HashMap<Integer, Integer>(); 418 for (Integer declared : rows) { 419 extraRowsMap.put(declared, row++); 420 } 421 } 422 } 423 if (declaredColumnCount != UNDEFINED) { 424 Set<Integer> extraColumns = null; 425 for (ViewData view : mChildViews) { 426 if (view.column >= declaredColumnCount) { 427 if (extraColumns == null) { 428 extraColumns = new HashSet<Integer>(); 429 } 430 extraColumns.add(view.column); 431 } 432 } 433 if (extraColumns != null && declaredColumnCount != UNDEFINED) { 434 List<Integer> columns = new ArrayList<Integer>(extraColumns); 435 Collections.sort(columns); 436 int column = declaredColumnCount; 437 extraColumnsMap = new HashMap<Integer, Integer>(); 438 for (Integer declared : columns) { 439 extraColumnsMap.put(declared, column++); 440 } 441 } 442 } 443 444 return Pair.of(extraRowsMap, extraColumnsMap); 445 } 446 447 /** 448 * Figure out actual row and column numbers for views that do not specify explicit row 449 * and/or column numbers 450 * TODO: Consolidate with the algorithm in GridLayout to ensure we get the 451 * exact same results! 452 */ 453 private void assignRowsAndColumns(int rowCount, int columnCount) { 454 Pair<Map<Integer, Integer>, Map<Integer, Integer>> p = findCellsOutsideDeclaredBounds(); 455 Map<Integer, Integer> extraRowsMap = p.getFirst(); 456 Map<Integer, Integer> extraColumnsMap = p.getSecond(); 457 458 if (!vertical) { 459 // Horizontal GridLayout: this is the default. Row and column numbers 460 // are assigned by assuming that the children are assigned successive 461 // column numbers until we get to the column count of the grid, at which 462 // point we jump to the next row. If any cell specifies either an explicit 463 // row number of column number, we jump to the next available position. 464 // Note also that if there are any rowspans on the current row, then the 465 // next row we jump to is below the largest such rowspan - in other words, 466 // the algorithm does not fill holes in the middle! 467 468 // TODO: Ensure that we don't run into trouble if a later element specifies 469 // an earlier number... find out what the layout does in that case! 470 int row = 0; 471 int column = 0; 472 int nextRow = 1; 473 for (ViewData view : mChildViews) { 474 int declaredColumn = view.column; 475 if (declaredColumn != UNDEFINED) { 476 if (declaredColumn >= columnCount) { 477 assert extraColumnsMap != null; 478 declaredColumn = extraColumnsMap.get(declaredColumn); 479 view.column = declaredColumn; 480 } 481 if (declaredColumn < column) { 482 // Must jump to the next row to accommodate the new row 483 assert nextRow > row; 484 //row++; 485 row = nextRow; 486 } 487 column = declaredColumn; 488 } else { 489 view.column = column; 490 } 491 if (view.row != UNDEFINED) { 492 // TODO: Should this adjust the column number too? (If so must 493 // also update view.column since we've already processed the local 494 // column number) 495 row = view.row; 496 } else { 497 view.row = row; 498 } 499 500 nextRow = Math.max(nextRow, view.row + view.rowSpan); 501 502 // Advance 503 column += view.columnSpan; 504 if (column >= columnCount) { 505 column = 0; 506 assert nextRow > row; 507 //row++; 508 row = nextRow; 509 } 510 } 511 } else { 512 // Vertical layout: successive children are assigned to the same column in 513 // successive rows. 514 int row = 0; 515 int column = 0; 516 int nextColumn = 1; 517 for (ViewData view : mChildViews) { 518 int declaredRow = view.row; 519 if (declaredRow != UNDEFINED) { 520 if (declaredRow >= rowCount) { 521 declaredRow = extraRowsMap.get(declaredRow); 522 view.row = declaredRow; 523 } 524 if (declaredRow < row) { 525 // Must jump to the next column to accommodate the new column 526 assert nextColumn > column; 527 column = nextColumn; 528 } 529 row = declaredRow; 530 } else { 531 view.row = row; 532 } 533 if (view.column != UNDEFINED) { 534 // TODO: Should this adjust the row number too? (If so must 535 // also update view.row since we've already processed the local 536 // row number) 537 column = view.column; 538 } else { 539 view.column = column; 540 } 541 542 nextColumn = Math.max(nextColumn, view.column + view.columnSpan); 543 544 // Advance 545 row += view.rowSpan; 546 if (row >= rowCount) { 547 row = 0; 548 assert nextColumn > column; 549 //row++; 550 column = nextColumn; 551 } 552 } 553 } 554 } 555 556 /** 557 * Computes the positions of the column and row boundaries 558 */ 559 private void assignCellBounds() { 560 if (!assignCellBoundsFromView()) { 561 assignCellBoundsFromBounds(); 562 } 563 initializeMaxBounds(); 564 mBaselines = new int[actualRowCount + 1]; 565 } 566 567 /** 568 * Computes the positions of the column and row boundaries, using actual 569 * layout data from the associated GridLayout instance (stored in 570 * {@link #mViewObject}) 571 */ 572 private boolean assignCellBoundsFromView() { 573 if (mViewObject != null) { 574 Pair<int[], int[]> cellBounds = GridModel.getAxisBounds(mViewObject); 575 if (cellBounds != null) { 576 int[] xs = cellBounds.getFirst(); 577 int[] ys = cellBounds.getSecond(); 578 579 actualColumnCount = xs.length - 1; 580 actualRowCount = ys.length - 1; 581 582 Rect layoutBounds = layout.getBounds(); 583 int layoutBoundsX = layoutBounds.x; 584 int layoutBoundsY = layoutBounds.y; 585 mLeft = new int[xs.length]; 586 mTop = new int[ys.length]; 587 for (int i = 0; i < xs.length; i++) { 588 mLeft[i] = xs[i] + layoutBoundsX; 589 } 590 for (int i = 0; i < ys.length; i++) { 591 mTop[i] = ys[i] + layoutBoundsY; 592 } 593 594 return true; 595 } 596 } 597 598 return false; 599 } 600 601 /** 602 * Computes the boundaries of the rows and columns by considering the bounds of the 603 * children. 604 */ 605 private void assignCellBoundsFromBounds() { 606 Rect layoutBounds = layout.getBounds(); 607 608 // Compute the actualColumnCount and actualRowCount. This -should- be 609 // as easy as declaredColumnCount + extraColumnsMap.size(), 610 // but the user doesn't *have* to declare a column count (or a row count) 611 // and we need both, so go and find the actual row and column maximums. 612 int maxColumn = 0; 613 int maxRow = 0; 614 for (ViewData view : mChildViews) { 615 maxColumn = max(maxColumn, view.column); 616 maxRow = max(maxRow, view.row); 617 } 618 actualColumnCount = maxColumn + 1; 619 actualRowCount = maxRow + 1; 620 621 mLeft = new int[actualColumnCount + 1]; 622 for (int i = 1; i < actualColumnCount; i++) { 623 mLeft[i] = UNDEFINED; 624 } 625 mLeft[0] = layoutBounds.x; 626 mLeft[actualColumnCount] = layoutBounds.x2(); 627 mTop = new int[actualRowCount + 1]; 628 for (int i = 1; i < actualRowCount; i++) { 629 mTop[i] = UNDEFINED; 630 } 631 mTop[0] = layoutBounds.y; 632 mTop[actualRowCount] = layoutBounds.y2(); 633 634 for (ViewData view : mChildViews) { 635 Rect bounds = view.node.getBounds(); 636 if (!bounds.isValid()) { 637 continue; 638 } 639 int column = view.column; 640 int row = view.row; 641 642 if (mLeft[column] == UNDEFINED) { 643 mLeft[column] = bounds.x; 644 } else { 645 mLeft[column] = Math.min(bounds.x, mLeft[column]); 646 } 647 if (mTop[row] == UNDEFINED) { 648 mTop[row] = bounds.y; 649 } else { 650 mTop[row] = Math.min(bounds.y, mTop[row]); 651 } 652 } 653 654 // Ensure that any empty columns/rows have a valid boundary value; for now, 655 for (int i = actualColumnCount - 1; i >= 0; i--) { 656 if (mLeft[i] == UNDEFINED) { 657 if (i == 0) { 658 mLeft[i] = layoutBounds.x; 659 } else if (i < actualColumnCount - 1) { 660 mLeft[i] = mLeft[i + 1] - 1; 661 if (mLeft[i - 1] != UNDEFINED && mLeft[i] < mLeft[i - 1]) { 662 mLeft[i] = mLeft[i - 1]; 663 } 664 } else { 665 mLeft[i] = layoutBounds.x2(); 666 } 667 } 668 } 669 for (int i = actualRowCount - 1; i >= 0; i--) { 670 if (mTop[i] == UNDEFINED) { 671 if (i == 0) { 672 mTop[i] = layoutBounds.y; 673 } else if (i < actualRowCount - 1) { 674 mTop[i] = mTop[i + 1] - 1; 675 if (mTop[i - 1] != UNDEFINED && mTop[i] < mTop[i - 1]) { 676 mTop[i] = mTop[i - 1]; 677 } 678 } else { 679 mTop[i] = layoutBounds.y2(); 680 } 681 } 682 } 683 684 // The bounds should be in ascending order now 685 if (false && GridLayoutRule.sDebugGridLayout) { 686 for (int i = 1; i < actualRowCount; i++) { 687 assert mTop[i + 1] >= mTop[i]; 688 } 689 for (int i = 0; i < actualColumnCount; i++) { 690 assert mLeft[i + 1] >= mLeft[i]; 691 } 692 } 693 } 694 695 /** 696 * Determine, for each row and column, what the largest x and y edges are 697 * within that row or column. This is used to find a natural split point to 698 * suggest when adding something "to the right of" or "below" another view. 699 */ 700 private void initializeMaxBounds() { 701 mMaxRight = new int[actualColumnCount + 1]; 702 mMaxBottom = new int[actualRowCount + 1]; 703 704 for (ViewData view : mChildViews) { 705 Rect bounds = view.node.getBounds(); 706 if (!bounds.isValid()) { 707 continue; 708 } 709 710 if (!view.isSpacer()) { 711 int x2 = bounds.x2(); 712 int y2 = bounds.y2(); 713 int column = view.column; 714 int row = view.row; 715 int targetColumn = min(actualColumnCount - 1, 716 column + view.columnSpan - 1); 717 int targetRow = min(actualRowCount - 1, row + view.rowSpan - 1); 718 IViewMetadata metadata = mRulesEngine.getMetadata(view.node.getFqcn()); 719 if (metadata != null) { 720 Margins insets = metadata.getInsets(); 721 if (insets != null) { 722 x2 -= insets.right; 723 y2 -= insets.bottom; 724 } 725 } 726 if (mMaxRight[targetColumn] < x2 727 && ((view.gravity & (GRAVITY_CENTER_HORIZ | GRAVITY_RIGHT)) == 0)) { 728 mMaxRight[targetColumn] = x2; 729 } 730 if (mMaxBottom[targetRow] < y2 731 && ((view.gravity & (GRAVITY_CENTER_VERT | GRAVITY_BOTTOM)) == 0)) { 732 mMaxBottom[targetRow] = y2; 733 } 734 } 735 } 736 } 737 738 /** 739 * Looks up the x[] and y[] locations of the columns and rows in the given GridLayout 740 * instance. 741 * 742 * @param view the GridLayout object, which should already have performed layout 743 * @return a pair of x[] and y[] integer arrays, or null if it could not be found 744 */ 745 public static Pair<int[], int[]> getAxisBounds(Object view) { 746 try { 747 Class<?> clz = view.getClass(); 748 Field horizontalAxis = clz.getDeclaredField("horizontalAxis"); //$NON-NLS-1$ 749 Field verticalAxis = clz.getDeclaredField("verticalAxis"); //$NON-NLS-1$ 750 horizontalAxis.setAccessible(true); 751 verticalAxis.setAccessible(true); 752 Object horizontal = horizontalAxis.get(view); 753 Object vertical = verticalAxis.get(view); 754 Field locations = horizontal.getClass().getDeclaredField("locations"); //$NON-NLS-1$ 755 assert locations.getType().isArray() : locations.getType(); 756 locations.setAccessible(true); 757 Object horizontalLocations = locations.get(horizontal); 758 Object verticalLocations = locations.get(vertical); 759 int[] xs = (int[]) horizontalLocations; 760 int[] ys = (int[]) verticalLocations; 761 return Pair.of(xs, ys); 762 } catch (Throwable t) { 763 // Probably trying to show a GridLayout on a platform that does not support it. 764 // Return null to indicate that the grid bounds must be computed from view bounds. 765 return null; 766 } 767 } 768 769 /** 770 * Add a new column. 771 * 772 * @param selectedChildren if null or empty, add the column at the end of the grid, 773 * and otherwise add it before the column of the first selected child 774 * @return the newly added column spacer 775 */ 776 public INode addColumn(List<? extends INode> selectedChildren) { 777 // Determine insert index 778 int newColumn = actualColumnCount; 779 if (selectedChildren != null && selectedChildren.size() > 0) { 780 INode first = selectedChildren.get(0); 781 ViewData view = getView(first); 782 newColumn = view.column; 783 } 784 785 INode newView = addColumn(newColumn, null, UNDEFINED, false, UNDEFINED, UNDEFINED); 786 if (newView != null) { 787 mRulesEngine.select(Collections.singletonList(newView)); 788 } 789 790 return newView; 791 } 792 793 /** 794 * Adds a new column. 795 * 796 * @param newColumn the column index to insert before 797 * @param newView the {@link INode} to insert as the column spacer, which may be null 798 * (in which case a spacer is automatically created) 799 * @param columnWidthDp the width, in device independent pixels, of the column to be 800 * added (which may be {@link #UNDEFINED} 801 * @param split if true, split the existing column into two at the given x position 802 * @param row the row to add the newView to 803 * @param x the x position of the column we're inserting 804 * @return the column spacer 805 */ 806 public INode addColumn(int newColumn, INode newView, int columnWidthDp, 807 boolean split, int row, int x) { 808 assert !stale; 809 stale = true; 810 811 // Insert a new column 812 if (declaredColumnCount != UNDEFINED) { 813 declaredColumnCount++; 814 setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); 815 } 816 817 boolean isLastColumn = true; 818 for (ViewData view : mChildViews) { 819 if (view.column >= newColumn) { 820 isLastColumn = false; 821 break; 822 } 823 } 824 825 for (ViewData view : mChildViews) { 826 boolean columnSpanSet = false; 827 828 int endColumn = view.column + view.columnSpan; 829 if (view.column >= newColumn || endColumn == newColumn) { 830 if (view.column == newColumn || endColumn == newColumn) { 831 //if (view.row == 0) { 832 if (newView == null && !isLastColumn) { 833 // Insert a new spacer 834 int index = getChildIndex(layout.getChildren(), view.node); 835 assert view.index == index; // TODO: Get rid of getter 836 if (endColumn == newColumn) { 837 // This cell -ends- at the desired position: insert it after 838 index++; 839 } 840 841 newView = addSpacer(layout, index, 842 split ? row : UNDEFINED, 843 split ? newColumn - 1 : UNDEFINED, 844 columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH, 845 DEFAULT_CELL_HEIGHT); 846 } 847 848 // Set the actual row number on the first cell on the new row. 849 // This means we don't really need the spacer above to imply 850 // the new row number, but we use the spacer to assign the row 851 // some height. 852 if (view.column == newColumn) { 853 setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column + 1); 854 } // else: endColumn == newColumn: handled below 855 } else if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) { 856 setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column + 1); 857 } 858 } else if (endColumn > newColumn) { 859 setColumnSpanAttribute(view.node, view.columnSpan + 1); 860 columnSpanSet = true; 861 } 862 863 if (split && !columnSpanSet && view.node.getBounds().x2() > x) { 864 if (view.node.getBounds().x < x) { 865 setColumnSpanAttribute(view.node, view.columnSpan + 1); 866 } 867 } 868 } 869 870 // Hardcode the row numbers if the last column is a new column such that 871 // they don't jump back to backfill the previous row's new last cell 872 if (isLastColumn) { 873 for (ViewData view : mChildViews) { 874 if (view.column == 0 && view.row > 0) { 875 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); 876 } 877 } 878 if (split) { 879 assert newView == null; 880 addSpacer(layout, -1, row, newColumn -1, 881 columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH, 882 SPACER_SIZE_DP); 883 } 884 } 885 886 return newView; 887 } 888 889 /** 890 * Removes the columns containing the given selection 891 * 892 * @param selectedChildren a list of nodes whose columns should be deleted 893 */ 894 public void removeColumns(List<? extends INode> selectedChildren) { 895 if (selectedChildren.size() == 0) { 896 return; 897 } 898 899 assert !stale; 900 stale = true; 901 902 // Figure out which columns should be removed 903 Set<Integer> removedSet = new HashSet<Integer>(); 904 for (INode child : selectedChildren) { 905 ViewData view = getView(child); 906 removedSet.add(view.column); 907 } 908 // Sort them in descending order such that we can process each 909 // deletion independently 910 List<Integer> removed = new ArrayList<Integer>(removedSet); 911 Collections.sort(removed, Collections.reverseOrder()); 912 913 for (int removedColumn : removed) { 914 // Remove column. 915 // First, adjust column count. 916 // TODO: Don't do this if the column being deleted is outside 917 // the declared column range! 918 // TODO: Do this under a write lock? / editXml lock? 919 if (declaredColumnCount != UNDEFINED) { 920 declaredColumnCount--; 921 setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); 922 } 923 924 // Remove any elements that begin in the deleted columns... 925 // If they have colspan > 1, then we must insert a spacer instead. 926 // For any other elements that overlap, we need to subtract from the span. 927 928 for (ViewData view : mChildViews) { 929 if (view.column == removedColumn) { 930 int index = getChildIndex(layout.getChildren(), view.node); 931 assert view.index == index; // TODO: Get rid of getter 932 if (view.columnSpan > 1) { 933 // Make a new spacer which is the width of the following 934 // columns 935 int columnWidth = getColumnWidth(removedColumn, view.columnSpan) - 936 getColumnWidth(removedColumn, 1); 937 int columnWidthDip = mRulesEngine.pxToDp(columnWidth); 938 addSpacer(layout, index, UNDEFINED, UNDEFINED, columnWidthDip, 939 SPACER_SIZE_DP); 940 } 941 layout.removeChild(view.node); 942 } else if (view.column < removedColumn 943 && view.column + view.columnSpan > removedColumn) { 944 // Subtract column span to skip this item 945 setColumnSpanAttribute(view.node, view.columnSpan - 1); 946 } else if (view.column > removedColumn) { 947 if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) { 948 setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column - 1); 949 } 950 } 951 } 952 } 953 } 954 955 /** 956 * Add a new row. 957 * 958 * @param selectedChildren if null or empty, add the row at the bottom of the grid, 959 * and otherwise add it before the row of the first selected child 960 * @return the newly added row spacer 961 */ 962 public INode addRow(List<? extends INode> selectedChildren) { 963 // Determine insert index 964 int newRow = actualRowCount; 965 if (selectedChildren.size() > 0) { 966 INode first = selectedChildren.get(0); 967 ViewData view = getView(first); 968 newRow = view.row; 969 } 970 971 INode newView = addRow(newRow, null, UNDEFINED, false, UNDEFINED, UNDEFINED); 972 if (newView != null) { 973 mRulesEngine.select(Collections.singletonList(newView)); 974 } 975 976 return newView; 977 } 978 979 /** 980 * Adds a new column. 981 * 982 * @param newRow the row index to insert before 983 * @param newView the {@link INode} to insert as the row spacer, which may be null (in 984 * which case a spacer is automatically created) 985 * @param rowHeightDp the height, in device independent pixels, of the row to be added 986 * (which may be {@link #UNDEFINED} 987 * @param split if true, split the existing row into two at the given y position 988 * @param column the column to add the newView to 989 * @param y the y position of the row we're inserting 990 * @return the row spacer 991 */ 992 public INode addRow(int newRow, INode newView, int rowHeightDp, boolean split, 993 int column, int y) { 994 // We'll modify the grid data; the cached data is out of date 995 assert !stale; 996 stale = true; 997 998 if (declaredRowCount != UNDEFINED) { 999 declaredRowCount++; 1000 setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount); 1001 } 1002 boolean added = false; 1003 for (ViewData view : mChildViews) { 1004 if (view.row >= newRow) { 1005 // Adjust the column count 1006 if (view.row == newRow && view.column == 0) { 1007 // Insert a new spacer 1008 if (newView == null) { 1009 int index = getChildIndex(layout.getChildren(), view.node); 1010 assert view.index == index; // TODO: Get rid of getter 1011 if (declaredColumnCount != UNDEFINED && !split) { 1012 setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); 1013 } 1014 newView = addSpacer(layout, index, 1015 split ? newRow - 1 : UNDEFINED, 1016 split ? column : UNDEFINED, 1017 SPACER_SIZE_DP, 1018 rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT); 1019 } 1020 1021 // Set the actual row number on the first cell on the new row. 1022 // This means we don't really need the spacer above to imply 1023 // the new row number, but we use the spacer to assign the row 1024 // some height. 1025 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row + 1); 1026 1027 added = true; 1028 } else if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) { 1029 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row + 1); 1030 } 1031 } else { 1032 int endRow = view.row + view.rowSpan; 1033 if (endRow > newRow) { 1034 setRowSpanAttribute(view.node, view.rowSpan + 1); 1035 } else if (split && view.node.getBounds().y2() > y) { 1036 if (view.node.getBounds().y < y) { 1037 setRowSpanAttribute(view.node, view.rowSpan + 1); 1038 } 1039 } 1040 } 1041 } 1042 1043 if (!added) { 1044 // Append a row at the end 1045 if (newView == null) { 1046 newView = addSpacer(layout, -1, UNDEFINED, UNDEFINED, 1047 SPACER_SIZE_DP, 1048 rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT); 1049 } 1050 if (declaredColumnCount != UNDEFINED && !split) { 1051 setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); 1052 } 1053 if (split) { 1054 setGridAttribute(newView, ATTR_LAYOUT_ROW, newRow - 1); 1055 setGridAttribute(newView, ATTR_LAYOUT_COLUMN, column); 1056 1057 } 1058 } 1059 1060 return newView; 1061 } 1062 1063 /** 1064 * Removes the rows containing the given selection 1065 * 1066 * @param selectedChildren a list of nodes whose rows should be deleted 1067 */ 1068 public void removeRows(List<? extends INode> selectedChildren) { 1069 if (selectedChildren.size() == 0) { 1070 return; 1071 } 1072 1073 assert !stale; 1074 stale = true; 1075 1076 // Figure out which rows should be removed 1077 Set<Integer> removedSet = new HashSet<Integer>(); 1078 for (INode child : selectedChildren) { 1079 ViewData view = getView(child); 1080 removedSet.add(view.row); 1081 } 1082 // Sort them in descending order such that we can process each 1083 // deletion independently 1084 List<Integer> removed = new ArrayList<Integer>(removedSet); 1085 Collections.sort(removed, Collections.reverseOrder()); 1086 1087 for (int removedRow : removed) { 1088 // Remove row. 1089 // First, adjust row count. 1090 // TODO: Don't do this if the row being deleted is outside 1091 // the declared row range! 1092 if (declaredRowCount != UNDEFINED) { 1093 declaredRowCount--; 1094 setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount); 1095 } 1096 1097 // Remove any elements that begin in the deleted rows... 1098 // If they have colspan > 1, then we must hardcode a new row number 1099 // instead. 1100 // For any other elements that overlap, we need to subtract from the span. 1101 1102 for (ViewData view : mChildViews) { 1103 if (view.row == removedRow) { 1104 // We don't have to worry about a rowSpan > 1 here, because even 1105 // if it is, those rowspans are not used to assign default row/column 1106 // positions for other cells 1107 layout.removeChild(view.node); 1108 } else if (view.row > removedRow) { 1109 if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) { 1110 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row - 1); 1111 } 1112 } else if (view.row < removedRow 1113 && view.row + view.rowSpan > removedRow) { 1114 // Subtract row span to skip this item 1115 setRowSpanAttribute(view.node, view.rowSpan - 1); 1116 } 1117 } 1118 } 1119 } 1120 1121 /** 1122 * Returns the row containing the given y line 1123 * 1124 * @param y the vertical position 1125 * @return the row containing the given line 1126 */ 1127 public int getRow(int y) { 1128 int row = Arrays.binarySearch(mTop, y); 1129 if (row == -1) { 1130 // Smaller than the first element; just use the first row 1131 return 0; 1132 } else if (row < 0) { 1133 row = -(row + 2); 1134 } 1135 1136 return row; 1137 } 1138 1139 /** 1140 * Returns the column containing the given x line 1141 * 1142 * @param x the horizontal position 1143 * @return the column containing the given line 1144 */ 1145 public int getColumn(int x) { 1146 int column = Arrays.binarySearch(mLeft, x); 1147 if (column == -1) { 1148 // Smaller than the first element; just use the first column 1149 return 0; 1150 } else if (column < 0) { 1151 column = -(column + 2); 1152 } 1153 1154 return column; 1155 } 1156 1157 /** 1158 * Returns the closest row to the given y line. This is 1159 * either the row containing the line, or the row below it. 1160 * 1161 * @param y the vertical position 1162 * @return the closest row 1163 */ 1164 public int getClosestRow(int y) { 1165 int row = Arrays.binarySearch(mTop, y); 1166 if (row == -1) { 1167 // Smaller than the first element; just use the first column 1168 return 0; 1169 } else if (row < 0) { 1170 row = -(row + 2); 1171 } 1172 1173 if (getRowDistance(row, y) < getRowDistance(row + 1, y)) { 1174 return row; 1175 } else { 1176 return row + 1; 1177 } 1178 } 1179 1180 /** 1181 * Returns the closest column to the given x line. This is 1182 * either the column containing the line, or the column following it. 1183 * 1184 * @param x the horizontal position 1185 * @return the closest column 1186 */ 1187 public int getClosestColumn(int x) { 1188 int column = Arrays.binarySearch(mLeft, x); 1189 if (column == -1) { 1190 // Smaller than the first element; just use the first column 1191 return 0; 1192 } else if (column < 0) { 1193 column = -(column + 2); 1194 } 1195 1196 if (getColumnDistance(column, x) < getColumnDistance(column + 1, x)) { 1197 return column; 1198 } else { 1199 return column + 1; 1200 } 1201 } 1202 1203 /** 1204 * Returns the distance between the given x position and the beginning of the given column 1205 * 1206 * @param column the column 1207 * @param x the x position 1208 * @return the distance between the two 1209 */ 1210 public int getColumnDistance(int column, int x) { 1211 return abs(getColumnX(column) - x); 1212 } 1213 1214 /** 1215 * Returns the actual width of the given column. This returns the difference between 1216 * the rightmost edge of the views (not including spacers) and the left edge of the 1217 * column. 1218 * 1219 * @param column the column 1220 * @return the actual width of the non-spacer views in the column 1221 */ 1222 public int getColumnActualWidth(int column) { 1223 return getColumnMaxX(column) - getColumnX(column); 1224 } 1225 1226 /** 1227 * Returns the distance between the given y position and the top of the given row 1228 * 1229 * @param row the row 1230 * @param y the y position 1231 * @return the distance between the two 1232 */ 1233 public int getRowDistance(int row, int y) { 1234 return abs(getRowY(row) - y); 1235 } 1236 1237 /** 1238 * Returns the y position of the top of the given row 1239 * 1240 * @param row the target row 1241 * @return the y position of its top edge 1242 */ 1243 public int getRowY(int row) { 1244 return mTop[min(mTop.length - 1, max(0, row))]; 1245 } 1246 1247 /** 1248 * Returns the bottom-most edge of any of the non-spacer children in the given row 1249 * 1250 * @param row the target row 1251 * @return the bottom-most edge of any of the non-spacer children in the row 1252 */ 1253 public int getRowMaxY(int row) { 1254 return mMaxBottom[min(mMaxBottom.length - 1, max(0, row))]; 1255 } 1256 1257 /** 1258 * Returns the actual height of the given row. This returns the difference between 1259 * the bottom-most edge of the views (not including spacers) and the top edge of the 1260 * row. 1261 * 1262 * @param row the row 1263 * @return the actual height of the non-spacer views in the row 1264 */ 1265 public int getRowActualHeight(int row) { 1266 return getRowMaxY(row) - getRowY(row); 1267 } 1268 1269 /** 1270 * Returns a list of all the nodes that intersects the rows in the range 1271 * {@code y1 <= y <= y2}. 1272 * 1273 * @param y1 the starting y, inclusive 1274 * @param y2 the ending y, inclusive 1275 * @return a list of nodes intersecting the given rows, never null but possibly empty 1276 */ 1277 public Collection<INode> getIntersectsRow(int y1, int y2) { 1278 List<INode> nodes = new ArrayList<INode>(); 1279 1280 for (ViewData view : mChildViews) { 1281 if (!view.isSpacer()) { 1282 Rect bounds = view.node.getBounds(); 1283 if (bounds.y2() >= y1 && bounds.y <= y2) { 1284 nodes.add(view.node); 1285 } 1286 } 1287 } 1288 1289 return nodes; 1290 } 1291 1292 /** 1293 * Returns the height of the given row or rows (if the rowSpan is greater than 1) 1294 * 1295 * @param row the target row 1296 * @param rowSpan the row span 1297 * @return the height in pixels of the given rows 1298 */ 1299 public int getRowHeight(int row, int rowSpan) { 1300 return getRowY(row + rowSpan) - getRowY(row); 1301 } 1302 1303 /** 1304 * Returns the x position of the left edge of the given column 1305 * 1306 * @param column the target column 1307 * @return the x position of its left edge 1308 */ 1309 public int getColumnX(int column) { 1310 return mLeft[min(mLeft.length - 1, max(0, column))]; 1311 } 1312 1313 /** 1314 * Returns the rightmost edge of any of the non-spacer children in the given row 1315 * 1316 * @param column the target column 1317 * @return the rightmost edge of any of the non-spacer children in the column 1318 */ 1319 public int getColumnMaxX(int column) { 1320 return mMaxRight[min(mMaxRight.length - 1, max(0, column))]; 1321 } 1322 1323 /** 1324 * Returns the width of the given column or columns (if the columnSpan is greater than 1) 1325 * 1326 * @param column the target column 1327 * @param columnSpan the column span 1328 * @return the width in pixels of the given columns 1329 */ 1330 public int getColumnWidth(int column, int columnSpan) { 1331 return getColumnX(column + columnSpan) - getColumnX(column); 1332 } 1333 1334 /** 1335 * Returns the bounds of the cell at the given row and column position, with the given 1336 * row and column spans. 1337 * 1338 * @param row the target row 1339 * @param column the target column 1340 * @param rowSpan the row span 1341 * @param columnSpan the column span 1342 * @return the bounds, in pixels, of the given cell 1343 */ 1344 public Rect getCellBounds(int row, int column, int rowSpan, int columnSpan) { 1345 return new Rect(getColumnX(column), getRowY(row), 1346 getColumnWidth(column, columnSpan), 1347 getRowHeight(row, rowSpan)); 1348 } 1349 1350 /** 1351 * Produces a display of view contents along with the pixel positions of each 1352 * row/column, like the following (used for diagnostics only) 1353 * 1354 * <pre> 1355 * |0 |49 |143 |192 |240 1356 * 36| | |button2 | 1357 * 72| |radioButton1 |button2 | 1358 * 74|button1 |radioButton1 |button2 | 1359 * 108|button1 | |button2 | 1360 * 110| | |button2 | 1361 * 149| | | | 1362 * 320 1363 * </pre> 1364 */ 1365 @Override 1366 public String toString() { 1367 if (stale) { 1368 System.out.println("WARNING: Grid has been modified, so model may be out of date!"); 1369 } 1370 1371 // Dump out the view table 1372 int cellWidth = 25; 1373 1374 List<List<List<ViewData>>> rowList = new ArrayList<List<List<ViewData>>>(mTop.length); 1375 for (int row = 0; row < mTop.length; row++) { 1376 List<List<ViewData>> columnList = new ArrayList<List<ViewData>>(mLeft.length); 1377 for (int col = 0; col < mLeft.length; col++) { 1378 columnList.add(new ArrayList<ViewData>(4)); 1379 } 1380 rowList.add(columnList); 1381 } 1382 for (ViewData view : mChildViews) { 1383 if (mDeleted != null && mDeleted.contains(view.node)) { 1384 continue; 1385 } 1386 for (int i = 0; i < view.rowSpan; i++) { 1387 if (view.row + i > mTop.length) { // Guard against bogus span values 1388 break; 1389 } 1390 if (rowList.size() <= view.row + i) { 1391 break; 1392 } 1393 for (int j = 0; j < view.columnSpan; j++) { 1394 List<List<ViewData>> columnList = rowList.get(view.row + i); 1395 if (columnList.size() <= view.column + j) { 1396 break; 1397 } 1398 columnList.get(view.column + j).add(view); 1399 } 1400 } 1401 } 1402 1403 StringWriter stringWriter = new StringWriter(); 1404 PrintWriter out = new PrintWriter(stringWriter); 1405 out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 1406 for (int col = 0; col < actualColumnCount + 1; col++) { 1407 out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$ 1408 } 1409 out.printf("\n"); //$NON-NLS-1$ 1410 for (int row = 0; row < actualRowCount + 1; row++) { 1411 out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$ 1412 if (row == actualRowCount) { 1413 break; 1414 } 1415 for (int col = 0; col < actualColumnCount; col++) { 1416 List<ViewData> views = rowList.get(row).get(col); 1417 1418 StringBuilder sb = new StringBuilder(); 1419 for (ViewData view : views) { 1420 String id = view != null ? view.getId() : ""; //$NON-NLS-1$ 1421 if (id.startsWith(NEW_ID_PREFIX)) { 1422 id = id.substring(NEW_ID_PREFIX.length()); 1423 } 1424 if (id.length() > cellWidth - 2) { 1425 id = id.substring(0, cellWidth - 2); 1426 } 1427 if (sb.length() > 0) { 1428 sb.append(','); 1429 } 1430 sb.append(id); 1431 } 1432 String cellString = sb.toString(); 1433 if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$ 1434 cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$ 1435 } 1436 out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$ 1437 } 1438 out.printf("\n"); //$NON-NLS-1$ 1439 } 1440 1441 out.flush(); 1442 return stringWriter.toString(); 1443 } 1444 1445 /** 1446 * Split a cell into two or three columns. 1447 * 1448 * @param newColumn The column number to insert before 1449 * @param insertMarginColumn If false, then the cell at newColumn -1 is split with the 1450 * left part taking up exactly columnWidthDp dips. If true, then the column 1451 * is split twice; the left part is the implicit width of the column, the 1452 * new middle (margin) column is exactly the columnWidthDp size and the 1453 * right column is the remaining space of the old cell. 1454 * @param columnWidthDp The width of the column inserted before the new column (or if 1455 * insertMarginColumn is false, then the width of the margin column) 1456 * @param x the x coordinate of the new column 1457 */ 1458 public void splitColumn(int newColumn, boolean insertMarginColumn, int columnWidthDp, int x) { 1459 assert !stale; 1460 stale = true; 1461 1462 // Insert a new column 1463 if (declaredColumnCount != UNDEFINED) { 1464 declaredColumnCount++; 1465 if (insertMarginColumn) { 1466 declaredColumnCount++; 1467 } 1468 setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); 1469 } 1470 1471 // Are we inserting a new last column in the grid? That requires some special handling... 1472 boolean isLastColumn = true; 1473 for (ViewData view : mChildViews) { 1474 if (view.column >= newColumn) { 1475 isLastColumn = false; 1476 break; 1477 } 1478 } 1479 1480 // Hardcode the row numbers if the last column is a new column such that 1481 // they don't jump back to backfill the previous row's new last cell: 1482 // TODO: Only do this for horizontal layouts! 1483 if (isLastColumn) { 1484 for (ViewData view : mChildViews) { 1485 if (view.column == 0 && view.row > 0) { 1486 if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) == null) { 1487 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); 1488 } 1489 } 1490 } 1491 } 1492 1493 // Find the spacer which marks this column, and if found, mark it as a split 1494 ViewData prevColumnSpacer = null; 1495 for (ViewData view : mChildViews) { 1496 if (view.column == newColumn - 1 && view.isColumnSpacer()) { 1497 prevColumnSpacer = view; 1498 break; 1499 } 1500 } 1501 1502 // Process all existing grid elements: 1503 // * Increase column numbers for all columns that have a hardcoded column number 1504 // greater than the new column 1505 // * Set an explicit column=0 where needed (TODO: Implement this) 1506 // * Increase the columnSpan for all columns that overlap the newly inserted column edge 1507 // * Split the spacer which defined the size of this column into two 1508 // (and if not found, create a new spacer) 1509 // 1510 for (ViewData view : mChildViews) { 1511 if (view == prevColumnSpacer) { 1512 continue; 1513 } 1514 1515 INode node = view.node; 1516 int column = view.column; 1517 if (column > newColumn || (column == newColumn && view.node.getBounds().x2() > x)) { 1518 // ALWAYS set the column, because 1519 // (1) if it has been set, it needs to be corrected 1520 // (2) if it has not been set, it needs to be set to cause this column 1521 // to skip over the new column (there may be no views for the new 1522 // column on this row). 1523 // TODO: Enhance this such that we only set the column to a skip number 1524 // where necessary, e.g. only on the FIRST view on this row following the 1525 // skipped column! 1526 1527 //if (getGridAttribute(node, ATTR_LAYOUT_COLUMN) != null) { 1528 setGridAttribute(node, ATTR_LAYOUT_COLUMN, column + (insertMarginColumn ? 2 : 1)); 1529 //} 1530 } else if (!view.isSpacer()) { 1531 int endColumn = column + view.columnSpan; 1532 if (endColumn > newColumn 1533 || endColumn == newColumn && view.node.getBounds().x2() > x) { 1534 // This cell spans the new insert position, so increment the column span 1535 setColumnSpanAttribute(node, view.columnSpan + (insertMarginColumn ? 2 : 1)); 1536 } 1537 } 1538 } 1539 1540 // Insert new spacer: 1541 if (prevColumnSpacer != null) { 1542 int px = getColumnWidth(newColumn - 1, 1); 1543 if (insertMarginColumn || columnWidthDp == 0) { 1544 px -= getColumnActualWidth(newColumn - 1); 1545 } 1546 int dp = mRulesEngine.pxToDp(px); 1547 int remaining = dp - columnWidthDp; 1548 if (remaining > 0) { 1549 prevColumnSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, 1550 String.format(VALUE_N_DP, remaining)); 1551 setGridAttribute(prevColumnSpacer.node, ATTR_LAYOUT_COLUMN, 1552 insertMarginColumn ? newColumn + 1 : newColumn); 1553 } 1554 } 1555 1556 if (columnWidthDp > 0) { 1557 int index = prevColumnSpacer != null ? prevColumnSpacer.index : -1; 1558 1559 addSpacer(layout, index, 0, insertMarginColumn ? newColumn : newColumn - 1, 1560 columnWidthDp, SPACER_SIZE_DP); 1561 } 1562 } 1563 1564 /** 1565 * Split a cell into two or three rows. 1566 * 1567 * @param newRow The row number to insert before 1568 * @param insertMarginRow If false, then the cell at newRow -1 is split with the above 1569 * part taking up exactly rowHeightDp dips. If true, then the row is split 1570 * twice; the top part is the implicit height of the row, the new middle 1571 * (margin) row is exactly the rowHeightDp size and the bottom column is 1572 * the remaining space of the old cell. 1573 * @param rowHeightDp The height of the row inserted before the new row (or if 1574 * insertMarginRow is false, then the height of the margin row) 1575 * @param y the y coordinate of the new row 1576 */ 1577 public void splitRow(int newRow, boolean insertMarginRow, int rowHeightDp, int y) { 1578 // Insert a new row 1579 if (declaredRowCount != UNDEFINED) { 1580 declaredRowCount++; 1581 if (insertMarginRow) { 1582 declaredRowCount++; 1583 } 1584 setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount); 1585 } 1586 1587 // Find the spacer which marks this row, and if found, mark it as a split 1588 ViewData prevRowSpacer = null; 1589 for (ViewData view : mChildViews) { 1590 if (view.row == newRow - 1 && view.isRowSpacer()) { 1591 prevRowSpacer = view; 1592 break; 1593 } 1594 } 1595 1596 // Se splitColumn() for details 1597 for (ViewData view : mChildViews) { 1598 if (view == prevRowSpacer) { 1599 continue; 1600 } 1601 1602 INode node = view.node; 1603 int row = view.row; 1604 if (row > newRow || (row == newRow && view.node.getBounds().y2() > y)) { 1605 //if (getGridAttribute(node, ATTR_LAYOUT_ROW) != null) { 1606 setGridAttribute(node, ATTR_LAYOUT_ROW, row + (insertMarginRow ? 2 : 1)); 1607 //} 1608 } else if (!view.isSpacer()) { 1609 int endRow = row + view.rowSpan; 1610 if (endRow > newRow 1611 || endRow == newRow && view.node.getBounds().y2() > y) { 1612 // This cell spans the new insert position, so increment the row span 1613 setRowSpanAttribute(node, view.rowSpan + (insertMarginRow ? 2 : 1)); 1614 } 1615 } 1616 } 1617 1618 // Insert new spacer: 1619 if (prevRowSpacer != null) { 1620 int px = getRowHeight(newRow - 1, 1); 1621 if (insertMarginRow || rowHeightDp == 0) { 1622 px -= getRowActualHeight(newRow - 1); 1623 } 1624 int dp = mRulesEngine.pxToDp(px); 1625 int remaining = dp - rowHeightDp; 1626 if (remaining > 0) { 1627 prevRowSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, 1628 String.format(VALUE_N_DP, remaining)); 1629 setGridAttribute(prevRowSpacer.node, ATTR_LAYOUT_ROW, 1630 insertMarginRow ? newRow + 1 : newRow); 1631 } 1632 } 1633 1634 if (rowHeightDp > 0) { 1635 int index = prevRowSpacer != null ? prevRowSpacer.index : -1; 1636 addSpacer(layout, index, insertMarginRow ? newRow : newRow - 1, 1637 0, SPACER_SIZE_DP, rowHeightDp); 1638 } 1639 } 1640 1641 /** 1642 * Data about a view in a table; this is not the same as a cell because multiple views 1643 * can share a single cell, and a view can span many cells. 1644 */ 1645 class ViewData { 1646 public final INode node; 1647 public final int index; 1648 public int row; 1649 public int column; 1650 public int rowSpan; 1651 public int columnSpan; 1652 public int gravity; 1653 1654 ViewData(INode n, int index) { 1655 node = n; 1656 this.index = index; 1657 1658 column = getGridAttribute(n, ATTR_LAYOUT_COLUMN, UNDEFINED); 1659 columnSpan = getGridAttribute(n, ATTR_LAYOUT_COLUMN_SPAN, 1); 1660 row = getGridAttribute(n, ATTR_LAYOUT_ROW, UNDEFINED); 1661 rowSpan = getGridAttribute(n, ATTR_LAYOUT_ROW_SPAN, 1); 1662 gravity = GravityHelper.getGravity(getGridAttribute(n, ATTR_LAYOUT_GRAVITY), 0); 1663 } 1664 1665 /** Applies the column and row fields into the XML model */ 1666 void applyPositionAttributes() { 1667 if (getGridAttribute(node, ATTR_LAYOUT_COLUMN) == null) { 1668 setGridAttribute(node, ATTR_LAYOUT_COLUMN, column); 1669 } 1670 if (getGridAttribute(node, ATTR_LAYOUT_ROW) == null) { 1671 setGridAttribute(node, ATTR_LAYOUT_ROW, row); 1672 } 1673 } 1674 1675 /** Returns the id of this node, or makes one up for display purposes */ 1676 String getId() { 1677 String id = node.getStringAttr(ANDROID_URI, ATTR_ID); 1678 if (id == null) { 1679 id = "<unknownid>"; //$NON-NLS-1$ 1680 String fqn = node.getFqcn(); 1681 fqn = fqn.substring(fqn.lastIndexOf('.') + 1); 1682 id = fqn + "-" 1683 + Integer.toString(System.identityHashCode(node)).substring(0, 3); 1684 } 1685 1686 return id; 1687 } 1688 1689 /** Returns true if this {@link ViewData} represents a spacer */ 1690 boolean isSpacer() { 1691 String fqcn = node.getFqcn(); 1692 return FQCN_SPACE.equals(fqcn) || FQCN_SPACE_V7.equals(fqcn); 1693 } 1694 1695 /** 1696 * Returns true if this {@link ViewData} represents a column spacer 1697 */ 1698 boolean isColumnSpacer() { 1699 return isSpacer() && 1700 // Any spacer not found in column 0 is a column spacer since we 1701 // place all horizontal spacers in column 0 1702 ((column > 0) 1703 // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and 1704 // for column distinguish by id. Or at least only do this for column 0! 1705 || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH))); 1706 } 1707 1708 /** 1709 * Returns true if this {@link ViewData} represents a row spacer 1710 */ 1711 boolean isRowSpacer() { 1712 return isSpacer() && 1713 // Any spacer not found in row 0 is a row spacer since we 1714 // place all vertical spacers in row 0 1715 ((row > 0) 1716 // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and 1717 // for column distinguish by id. Or at least only do this for column 0! 1718 || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT))); 1719 } 1720 } 1721 1722 /** 1723 * Sets the column span of the given node to the given value (or if the value is 1, 1724 * removes it) 1725 * 1726 * @param node the target node 1727 * @param span the new column span 1728 */ 1729 public void setColumnSpanAttribute(INode node, int span) { 1730 setGridAttribute(node, ATTR_LAYOUT_COLUMN_SPAN, span > 1 ? Integer.toString(span) : null); 1731 } 1732 1733 /** 1734 * Sets the row span of the given node to the given value (or if the value is 1, 1735 * removes it) 1736 * 1737 * @param node the target node 1738 * @param span the new row span 1739 */ 1740 public void setRowSpanAttribute(INode node, int span) { 1741 setGridAttribute(node, ATTR_LAYOUT_ROW_SPAN, span > 1 ? Integer.toString(span) : null); 1742 } 1743 1744 /** Returns the index of the given target node in the given child node array */ 1745 static int getChildIndex(INode[] children, INode target) { 1746 int index = 0; 1747 for (INode child : children) { 1748 if (child == target) { 1749 return index; 1750 } 1751 index++; 1752 } 1753 1754 return -1; 1755 } 1756 1757 /** 1758 * Notify the grid that the given node is about to be deleted. This can be used in 1759 * conjunction with {@link #cleanup()} to remove and merge unnecessary rows and 1760 * columns. 1761 * 1762 * @param child the child that is going to be removed shortly 1763 */ 1764 public void markDeleted(INode child) { 1765 if (mDeleted == null) { 1766 mDeleted = new HashSet<INode>(); 1767 } 1768 1769 mDeleted.add(child); 1770 } 1771 1772 /** 1773 * Clean up rows and columns that are no longer needed after the nodes marked for 1774 * deletion by {@link #markDeleted(INode)} are removed. 1775 */ 1776 public void cleanup() { 1777 if (mDeleted == null) { 1778 return; 1779 } 1780 1781 Set<Integer> usedColumns = new HashSet<Integer>(actualColumnCount); 1782 Set<Integer> usedRows = new HashSet<Integer>(actualColumnCount); 1783 Map<Integer, ViewData> columnSpacers = new HashMap<Integer, ViewData>(actualColumnCount); 1784 Map<Integer, ViewData> rowSpacers = new HashMap<Integer, ViewData>(actualColumnCount); 1785 1786 for (ViewData view : mChildViews) { 1787 if (view.isColumnSpacer()) { 1788 columnSpacers.put(view.column, view); 1789 } else if (view.isRowSpacer()) { 1790 rowSpacers.put(view.row, view); 1791 } else if (!mDeleted.contains(view.node)) { 1792 usedColumns.add(Integer.valueOf(view.column)); 1793 usedRows.add(Integer.valueOf(view.row)); 1794 } 1795 } 1796 1797 if (usedColumns.size() == 0) { 1798 // No more views - just remove all the spacers 1799 for (ViewData spacer : columnSpacers.values()) { 1800 layout.removeChild(spacer.node); 1801 } 1802 for (ViewData spacer : rowSpacers.values()) { 1803 layout.removeChild(spacer.node); 1804 } 1805 setGridAttribute(layout, ATTR_COLUMN_COUNT, 2); 1806 1807 return; 1808 } 1809 1810 // Remove (merge back) unnecessary columns 1811 for (int column = actualColumnCount - 1; column >= 0; column--) { 1812 if (!usedColumns.contains(column)) { 1813 // This column is no longer needed. Remove it! 1814 ViewData spacer = columnSpacers.get(column); 1815 ViewData prevSpacer = columnSpacers.get(column - 1); 1816 if (spacer == null) { 1817 // Can't touch this column; we only merge spacer columns, not 1818 // other types of columns (TODO: Consider what we can do here!) 1819 1820 // Try to merge with next column 1821 ViewData nextSpacer = columnSpacers.get(column + 1); 1822 if (nextSpacer != null) { 1823 int nextSizeDp = getDipSize(nextSpacer, false /* row */); 1824 int columnWidthPx = getColumnWidth(column, 1); 1825 int columnWidthDp = mRulesEngine.pxToDp(columnWidthPx); 1826 int combinedSizeDp = nextSizeDp + columnWidthDp; 1827 nextSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, 1828 String.format(VALUE_N_DP, combinedSizeDp)); 1829 // Also move the spacer into this column 1830 setGridAttribute(nextSpacer.node, ATTR_LAYOUT_COLUMN, column); 1831 columnSpacers.put(column, nextSpacer); 1832 } else { 1833 continue; 1834 } 1835 } else if (prevSpacer == null) { 1836 // Can't combine this column with a previous column; we don't have 1837 // data for it. 1838 continue; 1839 } 1840 1841 if (spacer != null) { 1842 // Combine spacer and prevSpacer. 1843 mergeSpacers(prevSpacer, spacer, false /*row*/); 1844 } 1845 1846 // Decrement column numbers for all elements to the right of the deleted column, 1847 // and subtract columnSpans for any elements that overlap it 1848 for (ViewData view : mChildViews) { 1849 if (view.column >= column) { 1850 if (view.column > 0) { 1851 view.column--; 1852 setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column); 1853 } 1854 } else if (!view.isSpacer()) { 1855 int endColumn = view.column + view.columnSpan; 1856 if (endColumn > column && view.columnSpan > 1) { 1857 view.columnSpan--; 1858 setColumnSpanAttribute(view.node, view.columnSpan); 1859 } 1860 } 1861 } 1862 } 1863 } 1864 1865 for (int row = actualRowCount - 1; row >= 0; row--) { 1866 if (!usedRows.contains(row)) { 1867 // This row is no longer needed. Remove it! 1868 ViewData spacer = rowSpacers.get(row); 1869 ViewData prevSpacer = rowSpacers.get(row - 1); 1870 if (spacer == null) { 1871 ViewData nextSpacer = rowSpacers.get(row + 1); 1872 if (nextSpacer != null) { 1873 int nextSizeDp = getDipSize(nextSpacer, true /* row */); 1874 int rowHeightPx = getRowHeight(row, 1); 1875 int rowHeightDp = mRulesEngine.pxToDp(rowHeightPx); 1876 int combinedSizeDp = nextSizeDp + rowHeightDp; 1877 nextSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, 1878 String.format(VALUE_N_DP, combinedSizeDp)); 1879 setGridAttribute(nextSpacer.node, ATTR_LAYOUT_ROW, row); 1880 rowSpacers.put(row, nextSpacer); 1881 } else { 1882 continue; 1883 } 1884 } else if (prevSpacer == null) { 1885 continue; 1886 } 1887 1888 if (spacer != null) { 1889 // Combine spacer and prevSpacer. 1890 mergeSpacers(prevSpacer, spacer, true /*row*/); 1891 } 1892 1893 1894 // Decrement row numbers for all elements below the deleted row, 1895 // and subtract rowSpans for any elements that overlap it 1896 for (ViewData view : mChildViews) { 1897 if (view.row >= row) { 1898 if (view.row > 0) { 1899 view.row--; 1900 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); 1901 } 1902 } else if (!view.isSpacer()) { 1903 int endRow = view.row + view.rowSpan; 1904 if (endRow > row && view.rowSpan > 1) { 1905 view.rowSpan--; 1906 setRowSpanAttribute(view.node, view.rowSpan); 1907 } 1908 } 1909 } 1910 } 1911 } 1912 1913 // TODO: Reduce row/column counts! 1914 } 1915 1916 /** 1917 * Merges two spacers together - either row spacers or column spacers based on the 1918 * parameter 1919 */ 1920 private void mergeSpacers(ViewData prevSpacer, ViewData spacer, boolean row) { 1921 int combinedSizeDp = -1; 1922 int prevSizeDp = getDipSize(prevSpacer, row); 1923 int sizeDp = getDipSize(spacer, row); 1924 combinedSizeDp = prevSizeDp + sizeDp; 1925 String attribute = row ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; 1926 prevSpacer.node.setAttribute(ANDROID_URI, attribute, 1927 String.format(VALUE_N_DP, combinedSizeDp)); 1928 layout.removeChild(spacer.node); 1929 } 1930 1931 /** 1932 * Computes the size (in device independent pixels) of the given spacer. 1933 * 1934 * @param spacer the spacer to measure 1935 * @param row if true, this is a row spacer, otherwise it is a column spacer 1936 * @return the size in device independent pixels 1937 */ 1938 private int getDipSize(ViewData spacer, boolean row) { 1939 String attribute = row ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; 1940 String size = spacer.node.getStringAttr(ANDROID_URI, attribute); 1941 if (size != null) { 1942 Matcher matcher = DIP_PATTERN.matcher(size); 1943 if (matcher.matches()) { 1944 try { 1945 return Integer.parseInt(matcher.group(1)); 1946 } catch (NumberFormatException nfe) { 1947 // Can't happen; we pre-check with regexp above. 1948 } 1949 } 1950 } 1951 1952 // Fallback for cases where the attribute values are not regular (e.g. user has edited 1953 // to some resource or other dimension format) - in that case just do bounds-based 1954 // computation. 1955 Rect bounds = spacer.node.getBounds(); 1956 return mRulesEngine.pxToDp(row ? bounds.h : bounds.w); 1957 } 1958 1959 /** 1960 * Adds a spacer to the given parent, at the given index. 1961 * 1962 * @param parent the GridLayout 1963 * @param index the index to insert the spacer at, or -1 to append 1964 * @param row the row to add the spacer to (or {@link #UNDEFINED} to not set a row yet 1965 * @param column the column to add the spacer to (or {@link #UNDEFINED} to not set a 1966 * column yet 1967 * @param widthDp the width in device independent pixels to assign to the spacer 1968 * @param heightDp the height in device independent pixels to assign to the spacer 1969 * @return the newly added spacer 1970 */ 1971 INode addSpacer(INode parent, int index, int row, int column, 1972 int widthDp, int heightDp) { 1973 INode spacer; 1974 1975 String tag = FQCN_SPACE; 1976 String gridLayout = parent.getFqcn(); 1977 if (!gridLayout.equals(GRID_LAYOUT) && gridLayout.length() > GRID_LAYOUT.length()) { 1978 String pkg = gridLayout.substring(0, gridLayout.length() - GRID_LAYOUT.length()); 1979 tag = pkg + SPACE; 1980 } 1981 if (index != -1) { 1982 spacer = parent.insertChildAt(tag, index); 1983 } else { 1984 spacer = parent.appendChild(tag); 1985 } 1986 1987 if (row != UNDEFINED) { 1988 setGridAttribute(spacer, ATTR_LAYOUT_ROW, row); 1989 } 1990 if (column != UNDEFINED) { 1991 setGridAttribute(spacer, ATTR_LAYOUT_COLUMN, column); 1992 } 1993 if (widthDp > 0) { 1994 spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, 1995 String.format(VALUE_N_DP, widthDp)); 1996 } 1997 if (heightDp > 0) { 1998 spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, 1999 String.format(VALUE_N_DP, heightDp)); 2000 } 2001 2002 // Temporary hack 2003 if (GridLayoutRule.sDebugGridLayout) { 2004 //String id = NEW_ID_PREFIX + "s"; 2005 //if (row == 0) { 2006 // id += "c"; 2007 //} 2008 //if (column == 0) { 2009 // id += "r"; 2010 //} 2011 //if (row > 0) { 2012 // id += Integer.toString(row); 2013 //} 2014 //if (column > 0) { 2015 // id += Integer.toString(column); 2016 //} 2017 String id = NEW_ID_PREFIX + "spacer_" //$NON-NLS-1$ 2018 + Integer.toString(System.identityHashCode(spacer)).substring(0, 3); 2019 spacer.setAttribute(ANDROID_URI, ATTR_ID, id); 2020 } 2021 2022 2023 return spacer; 2024 } 2025 2026 /** 2027 * Returns the string value of the given attribute, or null if it does not 2028 * exist. This only works for attributes that are GridLayout specific, such 2029 * as columnCount, layout_column, layout_row_span, etc. 2030 * 2031 * @param node the target node 2032 * @param name the attribute name (which must be in the android: namespace) 2033 * @return the attribute value or null 2034 */ 2035 2036 public String getGridAttribute(INode node, String name) { 2037 return node.getStringAttr(getNamespace(), name); 2038 } 2039 2040 /** 2041 * Returns the integer value of the given attribute, or the given defaultValue if the 2042 * attribute was not set. This only works for attributes that are GridLayout specific, 2043 * such as columnCount, layout_column, layout_row_span, etc. 2044 * 2045 * @param node the target node 2046 * @param attribute the attribute name (which must be in the android: namespace) 2047 * @param defaultValue the default value to use if the value is not set 2048 * @return the attribute integer value 2049 */ 2050 private int getGridAttribute(INode node, String attribute, int defaultValue) { 2051 String valueString = node.getStringAttr(getNamespace(), attribute); 2052 if (valueString != null) { 2053 try { 2054 return Integer.decode(valueString); 2055 } catch (NumberFormatException nufe) { 2056 // Ignore - error in user's XML 2057 } 2058 } 2059 2060 return defaultValue; 2061 } 2062 2063 /** 2064 * Returns the number of children views in the GridLayout 2065 * 2066 * @return the number of children views in the GridLayout 2067 */ 2068 public int getViewCount() { 2069 return mChildViews.size(); 2070 } 2071 } 2072