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