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