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