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 */ GridModel(IClientRulesEngine rulesEngine, INode node, Object viewObject)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 get( @onNull IClientRulesEngine rulesEngine, @NonNull INode node, @Nullable Object viewObject)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 */ getView(int index)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 */ getView(INode node)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 */ getInsertIndex(int row, int column)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 */ getBaseline(int row)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 */ applyPositionAttributes()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 */ setGridAttribute(INode node, String name, int value)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 */ setGridAttribute(INode node, String name, String value)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 */ getNamespace()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 */ removeFlag(String flag, String value)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 */ loadFromXml()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 findCellsOutsideDeclaredBounds()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 */ assignRowsAndColumnsFromXml(int rowCount, int columnCount)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 assignRowsAndColumnsFromViews(List<ViewData> views)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 */ assignCellBounds()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 */ assignCellBoundsFromView()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 */ assignCellBoundsFromBounds()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 */ initializeMaxBounds()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 */ getAxisBounds(Object view)867 public static Pair<int[], int[]> getAxisBounds(Object view) { 868 try { 869 Class<?> clz = view.getClass(); 870 String verticalAxisName = "verticalAxis"; 871 Field horizontalAxis; 872 try { 873 horizontalAxis = clz.getDeclaredField("horizontalAxis"); //$NON-NLS-1$ 874 } catch (NoSuchFieldException e) { 875 // Field names changed in KitKat 876 horizontalAxis = clz.getDeclaredField("mHorizontalAxis"); //$NON-NLS-1$ 877 verticalAxisName = "mVerticalAxis"; 878 } 879 Field verticalAxis = clz.getDeclaredField(verticalAxisName); 880 horizontalAxis.setAccessible(true); 881 verticalAxis.setAccessible(true); 882 Object horizontal = horizontalAxis.get(view); 883 Object vertical = verticalAxis.get(view); 884 Field locations = horizontal.getClass().getDeclaredField("locations"); //$NON-NLS-1$ 885 assert locations.getType().isArray() : locations.getType(); 886 locations.setAccessible(true); 887 Object horizontalLocations = locations.get(horizontal); 888 Object verticalLocations = locations.get(vertical); 889 int[] xs = (int[]) horizontalLocations; 890 int[] ys = (int[]) verticalLocations; 891 return Pair.of(xs, ys); 892 } catch (Throwable t) { 893 // Probably trying to show a GridLayout on a platform that does not support it. 894 // Return null to indicate that the grid bounds must be computed from view bounds. 895 return null; 896 } 897 } 898 899 /** 900 * Add a new column. 901 * 902 * @param selectedChildren if null or empty, add the column at the end of the grid, 903 * and otherwise add it before the column of the first selected child 904 * @return the newly added column spacer 905 */ addColumn(List<? extends INode> selectedChildren)906 public INode addColumn(List<? extends INode> selectedChildren) { 907 // Determine insert index 908 int newColumn = actualColumnCount; 909 if (selectedChildren != null && selectedChildren.size() > 0) { 910 INode first = selectedChildren.get(0); 911 ViewData view = getView(first); 912 newColumn = view.column; 913 } 914 915 INode newView = addColumn(newColumn, null, UNDEFINED, false, UNDEFINED, UNDEFINED); 916 if (newView != null) { 917 mRulesEngine.select(Collections.singletonList(newView)); 918 } 919 920 return newView; 921 } 922 923 /** 924 * Adds a new column. 925 * 926 * @param newColumn the column index to insert before 927 * @param newView the {@link INode} to insert as the column spacer, which may be null 928 * (in which case a spacer is automatically created) 929 * @param columnWidthDp the width, in device independent pixels, of the column to be 930 * added (which may be {@link #UNDEFINED} 931 * @param split if true, split the existing column into two at the given x position 932 * @param row the row to add the newView to 933 * @param x the x position of the column we're inserting 934 * @return the column spacer 935 */ addColumn(int newColumn, INode newView, int columnWidthDp, boolean split, int row, int x)936 public INode addColumn(int newColumn, INode newView, int columnWidthDp, 937 boolean split, int row, int x) { 938 // Insert a new column 939 actualColumnCount++; 940 if (declaredColumnCount != UNDEFINED) { 941 declaredColumnCount++; 942 setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); 943 } 944 945 boolean isLastColumn = true; 946 for (ViewData view : mChildViews) { 947 if (view.column >= newColumn) { 948 isLastColumn = false; 949 break; 950 } 951 } 952 953 for (ViewData view : mChildViews) { 954 boolean columnSpanSet = false; 955 956 int endColumn = view.column + view.columnSpan; 957 if (view.column >= newColumn || endColumn == newColumn) { 958 if (view.column == newColumn || endColumn == newColumn) { 959 //if (view.row == 0) { 960 if (newView == null && !isLastColumn) { 961 // Insert a new spacer 962 int index = getChildIndex(layout.getChildren(), view.node); 963 assert view.index == index; // TODO: Get rid of getter 964 if (endColumn == newColumn) { 965 // This cell -ends- at the desired position: insert it after 966 index++; 967 } 968 969 ViewData newViewData = addSpacer(layout, index, 970 split ? row : UNDEFINED, 971 split ? newColumn - 1 : UNDEFINED, 972 columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH, 973 DEFAULT_CELL_HEIGHT); 974 newViewData.column = newColumn - 1; 975 newViewData.row = row; 976 newView = newViewData.node; 977 } 978 979 // Set the actual row number on the first cell on the new row. 980 // This means we don't really need the spacer above to imply 981 // the new row number, but we use the spacer to assign the row 982 // some height. 983 if (view.column == newColumn) { 984 view.column++; 985 setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column); 986 } // else: endColumn == newColumn: handled below 987 } else if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) { 988 view.column++; 989 setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column); 990 } 991 } else if (endColumn > newColumn) { 992 view.columnSpan++; 993 setColumnSpanAttribute(view.node, view.columnSpan); 994 columnSpanSet = true; 995 } 996 997 if (split && !columnSpanSet && view.node.getBounds().x2() > x) { 998 if (view.node.getBounds().x < x) { 999 view.columnSpan++; 1000 setColumnSpanAttribute(view.node, view.columnSpan); 1001 } 1002 } 1003 } 1004 1005 // Hardcode the row numbers if the last column is a new column such that 1006 // they don't jump back to backfill the previous row's new last cell 1007 if (isLastColumn) { 1008 for (ViewData view : mChildViews) { 1009 if (view.column == 0 && view.row > 0) { 1010 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); 1011 } 1012 } 1013 if (split) { 1014 assert newView == null; 1015 addSpacer(layout, -1, row, newColumn -1, 1016 columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH, 1017 SPACER_SIZE_DP); 1018 } 1019 } 1020 1021 return newView; 1022 } 1023 1024 /** 1025 * Removes the columns containing the given selection 1026 * 1027 * @param selectedChildren a list of nodes whose columns should be deleted 1028 */ removeColumns(List<? extends INode> selectedChildren)1029 public void removeColumns(List<? extends INode> selectedChildren) { 1030 if (selectedChildren.size() == 0) { 1031 return; 1032 } 1033 1034 // Figure out which columns should be removed 1035 Set<Integer> removeColumns = new HashSet<Integer>(); 1036 Set<ViewData> removedViews = new HashSet<ViewData>(); 1037 for (INode child : selectedChildren) { 1038 ViewData view = getView(child); 1039 removedViews.add(view); 1040 removeColumns.add(view.column); 1041 } 1042 // Sort them in descending order such that we can process each 1043 // deletion independently 1044 List<Integer> removed = new ArrayList<Integer>(removeColumns); 1045 Collections.sort(removed, Collections.reverseOrder()); 1046 1047 for (int removedColumn : removed) { 1048 // Remove column. 1049 // First, adjust column count. 1050 // TODO: Don't do this if the column being deleted is outside 1051 // the declared column range! 1052 // TODO: Do this under a write lock? / editXml lock? 1053 actualColumnCount--; 1054 if (declaredColumnCount != UNDEFINED) { 1055 declaredColumnCount--; 1056 } 1057 1058 // Remove any elements that begin in the deleted columns... 1059 // If they have colspan > 1, then we must insert a spacer instead. 1060 // For any other elements that overlap, we need to subtract from the span. 1061 1062 for (ViewData view : mChildViews) { 1063 if (view.column == removedColumn) { 1064 int index = getChildIndex(layout.getChildren(), view.node); 1065 assert view.index == index; // TODO: Get rid of getter 1066 if (view.columnSpan > 1) { 1067 // Make a new spacer which is the width of the following 1068 // columns 1069 int columnWidth = getColumnWidth(removedColumn, view.columnSpan) - 1070 getColumnWidth(removedColumn, 1); 1071 int columnWidthDip = mRulesEngine.pxToDp(columnWidth); 1072 ViewData spacer = addSpacer(layout, index, UNDEFINED, UNDEFINED, 1073 columnWidthDip, SPACER_SIZE_DP); 1074 spacer.row = 0; 1075 spacer.column = removedColumn; 1076 } 1077 layout.removeChild(view.node); 1078 } else if (view.column < removedColumn 1079 && view.column + view.columnSpan > removedColumn) { 1080 // Subtract column span to skip this item 1081 view.columnSpan--; 1082 setColumnSpanAttribute(view.node, view.columnSpan); 1083 } else if (view.column > removedColumn) { 1084 view.column--; 1085 if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) { 1086 setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column); 1087 } 1088 } 1089 } 1090 } 1091 1092 // Remove children from child list! 1093 if (removedViews.size() <= 2) { 1094 mChildViews.removeAll(removedViews); 1095 } else { 1096 List<ViewData> remaining = 1097 new ArrayList<ViewData>(mChildViews.size() - removedViews.size()); 1098 for (ViewData view : mChildViews) { 1099 if (!removedViews.contains(view)) { 1100 remaining.add(view); 1101 } 1102 } 1103 mChildViews = remaining; 1104 } 1105 1106 //if (declaredColumnCount != UNDEFINED) { 1107 setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount); 1108 //} 1109 1110 } 1111 1112 /** 1113 * Add a new row. 1114 * 1115 * @param selectedChildren if null or empty, add the row at the bottom of the grid, 1116 * and otherwise add it before the row of the first selected child 1117 * @return the newly added row spacer 1118 */ addRow(List<? extends INode> selectedChildren)1119 public INode addRow(List<? extends INode> selectedChildren) { 1120 // Determine insert index 1121 int newRow = actualRowCount; 1122 if (selectedChildren.size() > 0) { 1123 INode first = selectedChildren.get(0); 1124 ViewData view = getView(first); 1125 newRow = view.row; 1126 } 1127 1128 INode newView = addRow(newRow, null, UNDEFINED, false, UNDEFINED, UNDEFINED); 1129 if (newView != null) { 1130 mRulesEngine.select(Collections.singletonList(newView)); 1131 } 1132 1133 return newView; 1134 } 1135 1136 /** 1137 * Adds a new column. 1138 * 1139 * @param newRow the row index to insert before 1140 * @param newView the {@link INode} to insert as the row spacer, which may be null (in 1141 * which case a spacer is automatically created) 1142 * @param rowHeightDp the height, in device independent pixels, of the row to be added 1143 * (which may be {@link #UNDEFINED} 1144 * @param split if true, split the existing row into two at the given y position 1145 * @param column the column to add the newView to 1146 * @param y the y position of the row we're inserting 1147 * @return the row spacer 1148 */ addRow(int newRow, INode newView, int rowHeightDp, boolean split, int column, int y)1149 public INode addRow(int newRow, INode newView, int rowHeightDp, boolean split, 1150 int column, int y) { 1151 actualRowCount++; 1152 if (declaredRowCount != UNDEFINED) { 1153 declaredRowCount++; 1154 setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount); 1155 } 1156 1157 boolean added = false; 1158 for (ViewData view : mChildViews) { 1159 if (view.row >= newRow) { 1160 // Adjust the column count 1161 if (view.row == newRow && view.column == 0) { 1162 // Insert a new spacer 1163 if (newView == null) { 1164 int index = getChildIndex(layout.getChildren(), view.node); 1165 assert view.index == index; // TODO: Get rid of getter 1166 if (declaredColumnCount != UNDEFINED && !split) { 1167 setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); 1168 } 1169 ViewData newViewData = addSpacer(layout, index, 1170 split ? newRow - 1 : UNDEFINED, 1171 split ? column : UNDEFINED, 1172 SPACER_SIZE_DP, 1173 rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT); 1174 newViewData.column = column; 1175 newViewData.row = newRow - 1; 1176 newView = newViewData.node; 1177 } 1178 1179 // Set the actual row number on the first cell on the new row. 1180 // This means we don't really need the spacer above to imply 1181 // the new row number, but we use the spacer to assign the row 1182 // some height. 1183 view.row++; 1184 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); 1185 1186 added = true; 1187 } else if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) { 1188 view.row++; 1189 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); 1190 } 1191 } else { 1192 int endRow = view.row + view.rowSpan; 1193 if (endRow > newRow) { 1194 view.rowSpan++; 1195 setRowSpanAttribute(view.node, view.rowSpan); 1196 } else if (split && view.node.getBounds().y2() > y) { 1197 if (view.node.getBounds().y < y) { 1198 view.rowSpan++; 1199 setRowSpanAttribute(view.node, view.rowSpan); 1200 } 1201 } 1202 } 1203 } 1204 1205 if (!added) { 1206 // Append a row at the end 1207 if (newView == null) { 1208 ViewData newViewData = addSpacer(layout, -1, UNDEFINED, UNDEFINED, 1209 SPACER_SIZE_DP, 1210 rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT); 1211 newViewData.column = column; 1212 // TODO: MAke sure this row number is right! 1213 newViewData.row = split ? newRow - 1 : newRow; 1214 newView = newViewData.node; 1215 } 1216 if (declaredColumnCount != UNDEFINED && !split) { 1217 setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); 1218 } 1219 if (split) { 1220 setGridAttribute(newView, ATTR_LAYOUT_ROW, newRow - 1); 1221 setGridAttribute(newView, ATTR_LAYOUT_COLUMN, column); 1222 } 1223 } 1224 1225 return newView; 1226 } 1227 1228 /** 1229 * Removes the rows containing the given selection 1230 * 1231 * @param selectedChildren a list of nodes whose rows should be deleted 1232 */ removeRows(List<? extends INode> selectedChildren)1233 public void removeRows(List<? extends INode> selectedChildren) { 1234 if (selectedChildren.size() == 0) { 1235 return; 1236 } 1237 1238 // Figure out which rows should be removed 1239 Set<ViewData> removedViews = new HashSet<ViewData>(); 1240 Set<Integer> removedRows = new HashSet<Integer>(); 1241 for (INode child : selectedChildren) { 1242 ViewData view = getView(child); 1243 removedViews.add(view); 1244 removedRows.add(view.row); 1245 } 1246 // Sort them in descending order such that we can process each 1247 // deletion independently 1248 List<Integer> removed = new ArrayList<Integer>(removedRows); 1249 Collections.sort(removed, Collections.reverseOrder()); 1250 1251 for (int removedRow : removed) { 1252 // Remove row. 1253 // First, adjust row count. 1254 // TODO: Don't do this if the row being deleted is outside 1255 // the declared row range! 1256 actualRowCount--; 1257 if (declaredRowCount != UNDEFINED) { 1258 declaredRowCount--; 1259 setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount); 1260 } 1261 1262 // Remove any elements that begin in the deleted rows... 1263 // If they have colspan > 1, then we must hardcode a new row number 1264 // instead. 1265 // For any other elements that overlap, we need to subtract from the span. 1266 1267 for (ViewData view : mChildViews) { 1268 if (view.row == removedRow) { 1269 // We don't have to worry about a rowSpan > 1 here, because even 1270 // if it is, those rowspans are not used to assign default row/column 1271 // positions for other cells 1272 // TODO: Check this; it differs from the removeColumns logic! 1273 layout.removeChild(view.node); 1274 } else if (view.row > removedRow) { 1275 view.row--; 1276 if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) { 1277 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); 1278 } 1279 } else if (view.row < removedRow 1280 && view.row + view.rowSpan > removedRow) { 1281 // Subtract row span to skip this item 1282 view.rowSpan--; 1283 setRowSpanAttribute(view.node, view.rowSpan); 1284 } 1285 } 1286 } 1287 1288 // Remove children from child list! 1289 if (removedViews.size() <= 2) { 1290 mChildViews.removeAll(removedViews); 1291 } else { 1292 List<ViewData> remaining = 1293 new ArrayList<ViewData>(mChildViews.size() - removedViews.size()); 1294 for (ViewData view : mChildViews) { 1295 if (!removedViews.contains(view)) { 1296 remaining.add(view); 1297 } 1298 } 1299 mChildViews = remaining; 1300 } 1301 } 1302 1303 /** 1304 * Returns the row containing the given y line 1305 * 1306 * @param y the vertical position 1307 * @return the row containing the given line 1308 */ getRow(int y)1309 public int getRow(int y) { 1310 int row = Arrays.binarySearch(mTop, y); 1311 if (row == -1) { 1312 // Smaller than the first element; just use the first row 1313 return 0; 1314 } else if (row < 0) { 1315 row = -(row + 2); 1316 } 1317 1318 return row; 1319 } 1320 1321 /** 1322 * Returns the column containing the given x line 1323 * 1324 * @param x the horizontal position 1325 * @return the column containing the given line 1326 */ getColumn(int x)1327 public int getColumn(int x) { 1328 int column = Arrays.binarySearch(mLeft, x); 1329 if (column == -1) { 1330 // Smaller than the first element; just use the first column 1331 return 0; 1332 } else if (column < 0) { 1333 column = -(column + 2); 1334 } 1335 1336 return column; 1337 } 1338 1339 /** 1340 * Returns the closest row to the given y line. This is 1341 * either the row containing the line, or the row below it. 1342 * 1343 * @param y the vertical position 1344 * @return the closest row 1345 */ getClosestRow(int y)1346 public int getClosestRow(int y) { 1347 int row = Arrays.binarySearch(mTop, y); 1348 if (row == -1) { 1349 // Smaller than the first element; just use the first column 1350 return 0; 1351 } else if (row < 0) { 1352 row = -(row + 2); 1353 } 1354 1355 if (getRowDistance(row, y) < getRowDistance(row + 1, y)) { 1356 return row; 1357 } else { 1358 return row + 1; 1359 } 1360 } 1361 1362 /** 1363 * Returns the closest column to the given x line. This is 1364 * either the column containing the line, or the column following it. 1365 * 1366 * @param x the horizontal position 1367 * @return the closest column 1368 */ getClosestColumn(int x)1369 public int getClosestColumn(int x) { 1370 int column = Arrays.binarySearch(mLeft, x); 1371 if (column == -1) { 1372 // Smaller than the first element; just use the first column 1373 return 0; 1374 } else if (column < 0) { 1375 column = -(column + 2); 1376 } 1377 1378 if (getColumnDistance(column, x) < getColumnDistance(column + 1, x)) { 1379 return column; 1380 } else { 1381 return column + 1; 1382 } 1383 } 1384 1385 /** 1386 * Returns the distance between the given x position and the beginning of the given column 1387 * 1388 * @param column the column 1389 * @param x the x position 1390 * @return the distance between the two 1391 */ getColumnDistance(int column, int x)1392 public int getColumnDistance(int column, int x) { 1393 return abs(getColumnX(column) - x); 1394 } 1395 1396 /** 1397 * Returns the actual width of the given column. This returns the difference between 1398 * the rightmost edge of the views (not including spacers) and the left edge of the 1399 * column. 1400 * 1401 * @param column the column 1402 * @return the actual width of the non-spacer views in the column 1403 */ getColumnActualWidth(int column)1404 public int getColumnActualWidth(int column) { 1405 return getColumnMaxX(column) - getColumnX(column); 1406 } 1407 1408 /** 1409 * Returns the distance between the given y position and the top of the given row 1410 * 1411 * @param row the row 1412 * @param y the y position 1413 * @return the distance between the two 1414 */ getRowDistance(int row, int y)1415 public int getRowDistance(int row, int y) { 1416 return abs(getRowY(row) - y); 1417 } 1418 1419 /** 1420 * Returns the y position of the top of the given row 1421 * 1422 * @param row the target row 1423 * @return the y position of its top edge 1424 */ getRowY(int row)1425 public int getRowY(int row) { 1426 return mTop[min(mTop.length - 1, max(0, row))]; 1427 } 1428 1429 /** 1430 * Returns the bottom-most edge of any of the non-spacer children in the given row 1431 * 1432 * @param row the target row 1433 * @return the bottom-most edge of any of the non-spacer children in the row 1434 */ getRowMaxY(int row)1435 public int getRowMaxY(int row) { 1436 return mMaxBottom[min(mMaxBottom.length - 1, max(0, row))]; 1437 } 1438 1439 /** 1440 * Returns the actual height of the given row. This returns the difference between 1441 * the bottom-most edge of the views (not including spacers) and the top edge of the 1442 * row. 1443 * 1444 * @param row the row 1445 * @return the actual height of the non-spacer views in the row 1446 */ getRowActualHeight(int row)1447 public int getRowActualHeight(int row) { 1448 return getRowMaxY(row) - getRowY(row); 1449 } 1450 1451 /** 1452 * Returns a list of all the nodes that intersects the rows in the range 1453 * {@code y1 <= y <= y2}. 1454 * 1455 * @param y1 the starting y, inclusive 1456 * @param y2 the ending y, inclusive 1457 * @return a list of nodes intersecting the given rows, never null but possibly empty 1458 */ getIntersectsRow(int y1, int y2)1459 public Collection<INode> getIntersectsRow(int y1, int y2) { 1460 List<INode> nodes = new ArrayList<INode>(); 1461 1462 for (ViewData view : mChildViews) { 1463 if (!view.isSpacer()) { 1464 Rect bounds = view.node.getBounds(); 1465 if (bounds.y2() >= y1 && bounds.y <= y2) { 1466 nodes.add(view.node); 1467 } 1468 } 1469 } 1470 1471 return nodes; 1472 } 1473 1474 /** 1475 * Returns the height of the given row or rows (if the rowSpan is greater than 1) 1476 * 1477 * @param row the target row 1478 * @param rowSpan the row span 1479 * @return the height in pixels of the given rows 1480 */ getRowHeight(int row, int rowSpan)1481 public int getRowHeight(int row, int rowSpan) { 1482 return getRowY(row + rowSpan) - getRowY(row); 1483 } 1484 1485 /** 1486 * Returns the x position of the left edge of the given column 1487 * 1488 * @param column the target column 1489 * @return the x position of its left edge 1490 */ getColumnX(int column)1491 public int getColumnX(int column) { 1492 return mLeft[min(mLeft.length - 1, max(0, column))]; 1493 } 1494 1495 /** 1496 * Returns the rightmost edge of any of the non-spacer children in the given row 1497 * 1498 * @param column the target column 1499 * @return the rightmost edge of any of the non-spacer children in the column 1500 */ getColumnMaxX(int column)1501 public int getColumnMaxX(int column) { 1502 return mMaxRight[min(mMaxRight.length - 1, max(0, column))]; 1503 } 1504 1505 /** 1506 * Returns the width of the given column or columns (if the columnSpan is greater than 1) 1507 * 1508 * @param column the target column 1509 * @param columnSpan the column span 1510 * @return the width in pixels of the given columns 1511 */ getColumnWidth(int column, int columnSpan)1512 public int getColumnWidth(int column, int columnSpan) { 1513 return getColumnX(column + columnSpan) - getColumnX(column); 1514 } 1515 1516 /** 1517 * Returns the bounds of the cell at the given row and column position, with the given 1518 * row and column spans. 1519 * 1520 * @param row the target row 1521 * @param column the target column 1522 * @param rowSpan the row span 1523 * @param columnSpan the column span 1524 * @return the bounds, in pixels, of the given cell 1525 */ getCellBounds(int row, int column, int rowSpan, int columnSpan)1526 public Rect getCellBounds(int row, int column, int rowSpan, int columnSpan) { 1527 return new Rect(getColumnX(column), getRowY(row), 1528 getColumnWidth(column, columnSpan), 1529 getRowHeight(row, rowSpan)); 1530 } 1531 1532 /** 1533 * Produces a display of view contents along with the pixel positions of each 1534 * row/column, like the following (used for diagnostics only) 1535 * 1536 * <pre> 1537 * |0 |49 |143 |192 |240 1538 * 36| | |button2 | 1539 * 72| |radioButton1 |button2 | 1540 * 74|button1 |radioButton1 |button2 | 1541 * 108|button1 | |button2 | 1542 * 110| | |button2 | 1543 * 149| | | | 1544 * 320 1545 * </pre> 1546 */ 1547 @Override toString()1548 public String toString() { 1549 // Dump out the view table 1550 int cellWidth = 25; 1551 1552 List<List<List<ViewData>>> rowList = new ArrayList<List<List<ViewData>>>(mTop.length); 1553 for (int row = 0; row < mTop.length; row++) { 1554 List<List<ViewData>> columnList = new ArrayList<List<ViewData>>(mLeft.length); 1555 for (int col = 0; col < mLeft.length; col++) { 1556 columnList.add(new ArrayList<ViewData>(4)); 1557 } 1558 rowList.add(columnList); 1559 } 1560 for (ViewData view : mChildViews) { 1561 for (int i = 0; i < view.rowSpan; i++) { 1562 if (view.row + i > mTop.length) { // Guard against bogus span values 1563 break; 1564 } 1565 if (rowList.size() <= view.row + i) { 1566 break; 1567 } 1568 for (int j = 0; j < view.columnSpan; j++) { 1569 List<List<ViewData>> columnList = rowList.get(view.row + i); 1570 if (columnList.size() <= view.column + j) { 1571 break; 1572 } 1573 columnList.get(view.column + j).add(view); 1574 } 1575 } 1576 } 1577 1578 StringWriter stringWriter = new StringWriter(); 1579 PrintWriter out = new PrintWriter(stringWriter); 1580 out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 1581 for (int col = 0; col < actualColumnCount + 1; col++) { 1582 out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$ 1583 } 1584 out.printf("\n"); //$NON-NLS-1$ 1585 for (int row = 0; row < actualRowCount + 1; row++) { 1586 out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$ 1587 if (row == actualRowCount) { 1588 break; 1589 } 1590 for (int col = 0; col < actualColumnCount; col++) { 1591 List<ViewData> views = rowList.get(row).get(col); 1592 1593 StringBuilder sb = new StringBuilder(); 1594 for (ViewData view : views) { 1595 String id = view != null ? view.getId() : ""; //$NON-NLS-1$ 1596 if (id.startsWith(NEW_ID_PREFIX)) { 1597 id = id.substring(NEW_ID_PREFIX.length()); 1598 } 1599 if (id.length() > cellWidth - 2) { 1600 id = id.substring(0, cellWidth - 2); 1601 } 1602 if (sb.length() > 0) { 1603 sb.append(','); 1604 } 1605 sb.append(id); 1606 } 1607 String cellString = sb.toString(); 1608 if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$ 1609 cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$ 1610 } 1611 out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$ 1612 } 1613 out.printf("\n"); //$NON-NLS-1$ 1614 } 1615 1616 out.flush(); 1617 return stringWriter.toString(); 1618 } 1619 1620 /** 1621 * Split a cell into two or three columns. 1622 * 1623 * @param newColumn The column number to insert before 1624 * @param insertMarginColumn If false, then the cell at newColumn -1 is split with the 1625 * left part taking up exactly columnWidthDp dips. If true, then the column 1626 * is split twice; the left part is the implicit width of the column, the 1627 * new middle (margin) column is exactly the columnWidthDp size and the 1628 * right column is the remaining space of the old cell. 1629 * @param columnWidthDp The width of the column inserted before the new column (or if 1630 * insertMarginColumn is false, then the width of the margin column) 1631 * @param x the x coordinate of the new column 1632 */ splitColumn(int newColumn, boolean insertMarginColumn, int columnWidthDp, int x)1633 public void splitColumn(int newColumn, boolean insertMarginColumn, int columnWidthDp, int x) { 1634 actualColumnCount++; 1635 1636 // Insert a new column 1637 if (declaredColumnCount != UNDEFINED) { 1638 declaredColumnCount++; 1639 if (insertMarginColumn) { 1640 declaredColumnCount++; 1641 } 1642 setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); 1643 } 1644 1645 // Are we inserting a new last column in the grid? That requires some special handling... 1646 boolean isLastColumn = true; 1647 for (ViewData view : mChildViews) { 1648 if (view.column >= newColumn) { 1649 isLastColumn = false; 1650 break; 1651 } 1652 } 1653 1654 // Hardcode the row numbers if the last column is a new column such that 1655 // they don't jump back to backfill the previous row's new last cell: 1656 // TODO: Only do this for horizontal layouts! 1657 if (isLastColumn) { 1658 for (ViewData view : mChildViews) { 1659 if (view.column == 0 && view.row > 0) { 1660 if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) == null) { 1661 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); 1662 } 1663 } 1664 } 1665 } 1666 1667 // Find the spacer which marks this column, and if found, mark it as a split 1668 ViewData prevColumnSpacer = null; 1669 for (ViewData view : mChildViews) { 1670 if (view.column == newColumn - 1 && view.isColumnSpacer()) { 1671 prevColumnSpacer = view; 1672 break; 1673 } 1674 } 1675 1676 // Process all existing grid elements: 1677 // * Increase column numbers for all columns that have a hardcoded column number 1678 // greater than the new column 1679 // * Set an explicit column=0 where needed (TODO: Implement this) 1680 // * Increase the columnSpan for all columns that overlap the newly inserted column edge 1681 // * Split the spacer which defined the size of this column into two 1682 // (and if not found, create a new spacer) 1683 // 1684 for (ViewData view : mChildViews) { 1685 if (view == prevColumnSpacer) { 1686 continue; 1687 } 1688 1689 INode node = view.node; 1690 int column = view.column; 1691 if (column > newColumn || (column == newColumn && view.node.getBounds().x2() > x)) { 1692 // ALWAYS set the column, because 1693 // (1) if it has been set, it needs to be corrected 1694 // (2) if it has not been set, it needs to be set to cause this column 1695 // to skip over the new column (there may be no views for the new 1696 // column on this row). 1697 // TODO: Enhance this such that we only set the column to a skip number 1698 // where necessary, e.g. only on the FIRST view on this row following the 1699 // skipped column! 1700 1701 //if (getGridAttribute(node, ATTR_LAYOUT_COLUMN) != null) { 1702 view.column += insertMarginColumn ? 2 : 1; 1703 setGridAttribute(node, ATTR_LAYOUT_COLUMN, view.column); 1704 //} 1705 } else if (!view.isSpacer()) { 1706 // Adjust the column span? We must increase it if 1707 // (1) the new column is inside the range [column, column + columnSpan] 1708 // (2) the new column is within the last cell in the column span, 1709 // and the exact X location of the split is within the horizontal 1710 // *bounds* of this node (provided it has gravity=left) 1711 // (3) the new column is within the last cell and the cell has gravity 1712 // right or gravity center 1713 int endColumn = column + view.columnSpan; 1714 if (endColumn > newColumn 1715 || endColumn == newColumn && (view.node.getBounds().x2() > x 1716 || GravityHelper.isConstrainedHorizontally(view.gravity) 1717 && !GravityHelper.isLeftAligned(view.gravity))) { 1718 // This cell spans the new insert position, so increment the column span 1719 view.columnSpan += insertMarginColumn ? 2 : 1; 1720 setColumnSpanAttribute(node, view.columnSpan); 1721 } 1722 } 1723 } 1724 1725 // Insert new spacer: 1726 if (prevColumnSpacer != null) { 1727 int px = getColumnWidth(newColumn - 1, 1); 1728 if (insertMarginColumn || columnWidthDp == 0) { 1729 px -= getColumnActualWidth(newColumn - 1); 1730 } 1731 int dp = mRulesEngine.pxToDp(px); 1732 int remaining = dp - columnWidthDp; 1733 if (remaining > 0) { 1734 prevColumnSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, 1735 String.format(VALUE_N_DP, remaining)); 1736 prevColumnSpacer.column = insertMarginColumn ? newColumn + 1 : newColumn; 1737 setGridAttribute(prevColumnSpacer.node, ATTR_LAYOUT_COLUMN, 1738 prevColumnSpacer.column); 1739 } 1740 } 1741 1742 if (columnWidthDp > 0) { 1743 int index = prevColumnSpacer != null ? prevColumnSpacer.index : -1; 1744 1745 addSpacer(layout, index, 0, insertMarginColumn ? newColumn : newColumn - 1, 1746 columnWidthDp, SPACER_SIZE_DP); 1747 } 1748 } 1749 1750 /** 1751 * Split a cell into two or three rows. 1752 * 1753 * @param newRow The row number to insert before 1754 * @param insertMarginRow If false, then the cell at newRow -1 is split with the above 1755 * part taking up exactly rowHeightDp dips. If true, then the row is split 1756 * twice; the top part is the implicit height of the row, the new middle 1757 * (margin) row is exactly the rowHeightDp size and the bottom column is 1758 * the remaining space of the old cell. 1759 * @param rowHeightDp The height of the row inserted before the new row (or if 1760 * insertMarginRow is false, then the height of the margin row) 1761 * @param y the y coordinate of the new row 1762 */ splitRow(int newRow, boolean insertMarginRow, int rowHeightDp, int y)1763 public void splitRow(int newRow, boolean insertMarginRow, int rowHeightDp, int y) { 1764 actualRowCount++; 1765 1766 // Insert a new row 1767 if (declaredRowCount != UNDEFINED) { 1768 declaredRowCount++; 1769 if (insertMarginRow) { 1770 declaredRowCount++; 1771 } 1772 setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount); 1773 } 1774 1775 // Find the spacer which marks this row, and if found, mark it as a split 1776 ViewData prevRowSpacer = null; 1777 for (ViewData view : mChildViews) { 1778 if (view.row == newRow - 1 && view.isRowSpacer()) { 1779 prevRowSpacer = view; 1780 break; 1781 } 1782 } 1783 1784 // Se splitColumn() for details 1785 for (ViewData view : mChildViews) { 1786 if (view == prevRowSpacer) { 1787 continue; 1788 } 1789 1790 INode node = view.node; 1791 int row = view.row; 1792 if (row > newRow || (row == newRow && view.node.getBounds().y2() > y)) { 1793 //if (getGridAttribute(node, ATTR_LAYOUT_ROW) != null) { 1794 view.row += insertMarginRow ? 2 : 1; 1795 setGridAttribute(node, ATTR_LAYOUT_ROW, view.row); 1796 //} 1797 } else if (!view.isSpacer()) { 1798 int endRow = row + view.rowSpan; 1799 if (endRow > newRow 1800 || endRow == newRow && (view.node.getBounds().y2() > y 1801 || GravityHelper.isConstrainedVertically(view.gravity) 1802 && !GravityHelper.isTopAligned(view.gravity))) { 1803 // This cell spans the new insert position, so increment the row span 1804 view.rowSpan += insertMarginRow ? 2 : 1; 1805 setRowSpanAttribute(node, view.rowSpan); 1806 } 1807 } 1808 } 1809 1810 // Insert new spacer: 1811 if (prevRowSpacer != null) { 1812 int px = getRowHeight(newRow - 1, 1); 1813 if (insertMarginRow || rowHeightDp == 0) { 1814 px -= getRowActualHeight(newRow - 1); 1815 } 1816 int dp = mRulesEngine.pxToDp(px); 1817 int remaining = dp - rowHeightDp; 1818 if (remaining > 0) { 1819 prevRowSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, 1820 String.format(VALUE_N_DP, remaining)); 1821 prevRowSpacer.row = insertMarginRow ? newRow + 1 : newRow; 1822 setGridAttribute(prevRowSpacer.node, ATTR_LAYOUT_ROW, prevRowSpacer.row); 1823 } 1824 } 1825 1826 if (rowHeightDp > 0) { 1827 int index = prevRowSpacer != null ? prevRowSpacer.index : -1; 1828 addSpacer(layout, index, insertMarginRow ? newRow : newRow - 1, 1829 0, SPACER_SIZE_DP, rowHeightDp); 1830 } 1831 } 1832 1833 /** 1834 * Data about a view in a table; this is not the same as a cell because multiple views 1835 * can share a single cell, and a view can span many cells. 1836 */ 1837 public class ViewData { 1838 public final INode node; 1839 public final int index; 1840 public int row; 1841 public int column; 1842 public int rowSpan; 1843 public int columnSpan; 1844 public int gravity; 1845 ViewData(INode n, int index)1846 ViewData(INode n, int index) { 1847 node = n; 1848 this.index = index; 1849 1850 column = getGridAttribute(n, ATTR_LAYOUT_COLUMN, UNDEFINED); 1851 columnSpan = getGridAttribute(n, ATTR_LAYOUT_COLUMN_SPAN, 1); 1852 row = getGridAttribute(n, ATTR_LAYOUT_ROW, UNDEFINED); 1853 rowSpan = getGridAttribute(n, ATTR_LAYOUT_ROW_SPAN, 1); 1854 gravity = GravityHelper.getGravity(getGridAttribute(n, ATTR_LAYOUT_GRAVITY), 0); 1855 } 1856 1857 /** Applies the column and row fields into the XML model */ applyPositionAttributes()1858 void applyPositionAttributes() { 1859 setGridAttribute(node, ATTR_LAYOUT_COLUMN, column); 1860 setGridAttribute(node, ATTR_LAYOUT_ROW, row); 1861 } 1862 1863 /** Returns the id of this node, or makes one up for display purposes */ getId()1864 String getId() { 1865 String id = node.getStringAttr(ANDROID_URI, ATTR_ID); 1866 if (id == null) { 1867 id = "<unknownid>"; //$NON-NLS-1$ 1868 String fqn = node.getFqcn(); 1869 fqn = fqn.substring(fqn.lastIndexOf('.') + 1); 1870 id = fqn + "-" 1871 + Integer.toString(System.identityHashCode(node)).substring(0, 3); 1872 } 1873 1874 return id; 1875 } 1876 1877 /** Returns true if this {@link ViewData} represents a spacer */ isSpacer()1878 boolean isSpacer() { 1879 return isSpace(node.getFqcn()); 1880 } 1881 1882 /** 1883 * Returns true if this {@link ViewData} represents a column spacer 1884 */ isColumnSpacer()1885 boolean isColumnSpacer() { 1886 return isSpacer() && 1887 // Any spacer not found in column 0 is a column spacer since we 1888 // place all horizontal spacers in column 0 1889 ((column > 0) 1890 // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and 1891 // for column distinguish by id. Or at least only do this for column 0! 1892 || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH))); 1893 } 1894 1895 /** 1896 * Returns true if this {@link ViewData} represents a row spacer 1897 */ isRowSpacer()1898 boolean isRowSpacer() { 1899 return isSpacer() && 1900 // Any spacer not found in row 0 is a row spacer since we 1901 // place all vertical spacers in row 0 1902 ((row > 0) 1903 // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and 1904 // for column distinguish by id. Or at least only do this for column 0! 1905 || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT))); 1906 } 1907 } 1908 1909 /** 1910 * Sets the column span of the given node to the given value (or if the value is 1, 1911 * removes it) 1912 * 1913 * @param node the target node 1914 * @param span the new column span 1915 */ setColumnSpanAttribute(INode node, int span)1916 public void setColumnSpanAttribute(INode node, int span) { 1917 setGridAttribute(node, ATTR_LAYOUT_COLUMN_SPAN, span > 1 ? Integer.toString(span) : null); 1918 } 1919 1920 /** 1921 * Sets the row span of the given node to the given value (or if the value is 1, 1922 * removes it) 1923 * 1924 * @param node the target node 1925 * @param span the new row span 1926 */ setRowSpanAttribute(INode node, int span)1927 public void setRowSpanAttribute(INode node, int span) { 1928 setGridAttribute(node, ATTR_LAYOUT_ROW_SPAN, span > 1 ? Integer.toString(span) : null); 1929 } 1930 1931 /** Returns the index of the given target node in the given child node array */ getChildIndex(INode[] children, INode target)1932 static int getChildIndex(INode[] children, INode target) { 1933 int index = 0; 1934 for (INode child : children) { 1935 if (child == target) { 1936 return index; 1937 } 1938 index++; 1939 } 1940 1941 return -1; 1942 } 1943 1944 /** 1945 * Update the model to account for the given nodes getting deleted. The nodes 1946 * are not actually deleted by this method; that is assumed to be performed by the 1947 * caller. Instead this method performs whatever model updates are necessary to 1948 * preserve the grid structure. 1949 * 1950 * @param nodes the nodes to be deleted 1951 */ onDeleted(@onNull List<INode> nodes)1952 public void onDeleted(@NonNull List<INode> nodes) { 1953 if (nodes.size() == 0) { 1954 return; 1955 } 1956 1957 // Attempt to clean up spacer objects for any newly-empty rows or columns 1958 // as the result of this deletion 1959 1960 Set<INode> deleted = new HashSet<INode>(); 1961 1962 for (INode child : nodes) { 1963 // We don't care about deletion of spacers 1964 String fqcn = child.getFqcn(); 1965 if (fqcn.equals(FQCN_SPACE) || fqcn.equals(FQCN_SPACE_V7)) { 1966 continue; 1967 } 1968 deleted.add(child); 1969 } 1970 1971 Set<Integer> usedColumns = new HashSet<Integer>(actualColumnCount); 1972 Set<Integer> usedRows = new HashSet<Integer>(actualRowCount); 1973 Multimap<Integer, ViewData> columnSpacers = ArrayListMultimap.create(actualColumnCount, 2); 1974 Multimap<Integer, ViewData> rowSpacers = ArrayListMultimap.create(actualRowCount, 2); 1975 Set<ViewData> removedViews = new HashSet<ViewData>(); 1976 1977 for (ViewData view : mChildViews) { 1978 if (deleted.contains(view.node)) { 1979 removedViews.add(view); 1980 } else if (view.isColumnSpacer()) { 1981 columnSpacers.put(view.column, view); 1982 } else if (view.isRowSpacer()) { 1983 rowSpacers.put(view.row, view); 1984 } else { 1985 usedColumns.add(Integer.valueOf(view.column)); 1986 usedRows.add(Integer.valueOf(view.row)); 1987 } 1988 } 1989 1990 if (usedColumns.size() == 0 || usedRows.size() == 0) { 1991 // No more views - just remove all the spacers 1992 for (ViewData spacer : columnSpacers.values()) { 1993 layout.removeChild(spacer.node); 1994 } 1995 for (ViewData spacer : rowSpacers.values()) { 1996 layout.removeChild(spacer.node); 1997 } 1998 mChildViews.clear(); 1999 actualColumnCount = 0; 2000 declaredColumnCount = 2; 2001 actualRowCount = 0; 2002 declaredRowCount = UNDEFINED; 2003 setGridAttribute(layout, ATTR_COLUMN_COUNT, 2); 2004 2005 return; 2006 } 2007 2008 // Determine columns to introduce spacers into: 2009 // This is tricky; I should NOT combine spacers if there are cells tied to 2010 // individual ones 2011 2012 // TODO: Invalidate column sizes too! Otherwise repeated updates might get confused! 2013 // Similarly, inserts need to do the same! 2014 2015 // Produce map of old column numbers to new column numbers 2016 // Collapse regions of consecutive space and non-space ranges together 2017 int[] columnMap = new int[actualColumnCount + 1]; // +1: Easily handle columnSpans as well 2018 int newColumn = 0; 2019 boolean prevUsed = usedColumns.contains(0); 2020 for (int column = 1; column < actualColumnCount; column++) { 2021 boolean used = usedColumns.contains(column); 2022 if (used || prevUsed != used) { 2023 newColumn++; 2024 prevUsed = used; 2025 } 2026 columnMap[column] = newColumn; 2027 } 2028 newColumn++; 2029 columnMap[actualColumnCount] = newColumn; 2030 assert columnMap[0] == 0; 2031 2032 int[] rowMap = new int[actualRowCount + 1]; // +1: Easily handle rowSpans as well 2033 int newRow = 0; 2034 prevUsed = usedRows.contains(0); 2035 for (int row = 1; row < actualRowCount; row++) { 2036 boolean used = usedRows.contains(row); 2037 if (used || prevUsed != used) { 2038 newRow++; 2039 prevUsed = used; 2040 } 2041 rowMap[row] = newRow; 2042 } 2043 newRow++; 2044 rowMap[actualRowCount] = newRow; 2045 assert rowMap[0] == 0; 2046 2047 2048 // Adjust column and row numbers to account for deletions: for a given cell, if it 2049 // is to the right of a deleted column, reduce its column number, and if it only 2050 // spans across the deleted column, reduce its column span. 2051 for (ViewData view : mChildViews) { 2052 if (removedViews.contains(view)) { 2053 continue; 2054 } 2055 int newColumnStart = columnMap[Math.min(columnMap.length - 1, view.column)]; 2056 // Gracefully handle rogue/invalid columnSpans in the XML 2057 int newColumnEnd = columnMap[Math.min(columnMap.length - 1, 2058 view.column + view.columnSpan)]; 2059 if (newColumnStart != view.column) { 2060 view.column = newColumnStart; 2061 setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column); 2062 } 2063 2064 int columnSpan = newColumnEnd - newColumnStart; 2065 if (columnSpan != view.columnSpan) { 2066 if (columnSpan >= 1) { 2067 view.columnSpan = columnSpan; 2068 setColumnSpanAttribute(view.node, view.columnSpan); 2069 } // else: merging spacing columns together 2070 } 2071 2072 2073 int newRowStart = rowMap[Math.min(rowMap.length - 1, view.row)]; 2074 int newRowEnd = rowMap[Math.min(rowMap.length - 1, view.row + view.rowSpan)]; 2075 if (newRowStart != view.row) { 2076 view.row = newRowStart; 2077 setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); 2078 } 2079 2080 int rowSpan = newRowEnd - newRowStart; 2081 if (rowSpan != view.rowSpan) { 2082 if (rowSpan >= 1) { 2083 view.rowSpan = rowSpan; 2084 setRowSpanAttribute(view.node, view.rowSpan); 2085 } // else: merging spacing rows together 2086 } 2087 } 2088 2089 // Merge spacers (and add spacers for newly empty columns) 2090 int start = 0; 2091 while (start < actualColumnCount) { 2092 // Find next unused span 2093 while (start < actualColumnCount && usedColumns.contains(start)) { 2094 start++; 2095 } 2096 if (start == actualColumnCount) { 2097 break; 2098 } 2099 assert !usedColumns.contains(start); 2100 // Find the next span of unused columns and produce a SINGLE 2101 // spacer for that range (unless it's a zero-sized columns) 2102 int end = start + 1; 2103 for (; end < actualColumnCount; end++) { 2104 if (usedColumns.contains(end)) { 2105 break; 2106 } 2107 } 2108 2109 // Add up column sizes 2110 int width = getColumnWidth(start, end - start); 2111 2112 // Find all spacers: the first one found should be moved to the start column 2113 // and assigned to the full height of the columns, and 2114 // the column count reduced by the corresponding amount 2115 2116 // TODO: if width = 0, fully remove 2117 2118 boolean isFirstSpacer = true; 2119 for (int column = start; column < end; column++) { 2120 Collection<ViewData> spacers = columnSpacers.get(column); 2121 if (spacers != null && !spacers.isEmpty()) { 2122 // Avoid ConcurrentModificationException since we're inserting into the 2123 // map within this loop (always at a different index, but the map doesn't 2124 // know that) 2125 spacers = new ArrayList<ViewData>(spacers); 2126 for (ViewData spacer : spacers) { 2127 if (isFirstSpacer) { 2128 isFirstSpacer = false; 2129 spacer.column = columnMap[start]; 2130 setGridAttribute(spacer.node, ATTR_LAYOUT_COLUMN, spacer.column); 2131 if (end - start > 1) { 2132 // Compute a merged width for all the spacers (not needed if 2133 // there's just one spacer; it should already have the correct width) 2134 int columnWidthDp = mRulesEngine.pxToDp(width); 2135 spacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, 2136 String.format(VALUE_N_DP, columnWidthDp)); 2137 } 2138 columnSpacers.put(start, spacer); 2139 } else { 2140 removedViews.add(spacer); // Mark for model removal 2141 layout.removeChild(spacer.node); 2142 } 2143 } 2144 } 2145 } 2146 2147 if (isFirstSpacer) { 2148 // No spacer: create one 2149 int columnWidthDp = mRulesEngine.pxToDp(width); 2150 addSpacer(layout, -1, UNDEFINED, columnMap[start], columnWidthDp, DEFAULT_CELL_HEIGHT); 2151 } 2152 2153 start = end; 2154 } 2155 actualColumnCount = newColumn; 2156 //if (usedColumns.contains(newColumn)) { 2157 // // TODO: This may be totally wrong for right aligned content! 2158 // actualColumnCount++; 2159 //} 2160 2161 // Merge spacers for rows 2162 start = 0; 2163 while (start < actualRowCount) { 2164 // Find next unused span 2165 while (start < actualRowCount && usedRows.contains(start)) { 2166 start++; 2167 } 2168 if (start == actualRowCount) { 2169 break; 2170 } 2171 assert !usedRows.contains(start); 2172 // Find the next span of unused rows and produce a SINGLE 2173 // spacer for that range (unless it's a zero-sized rows) 2174 int end = start + 1; 2175 for (; end < actualRowCount; end++) { 2176 if (usedRows.contains(end)) { 2177 break; 2178 } 2179 } 2180 2181 // Add up row sizes 2182 int height = getRowHeight(start, end - start); 2183 2184 // Find all spacers: the first one found should be moved to the start row 2185 // and assigned to the full height of the rows, and 2186 // the row count reduced by the corresponding amount 2187 2188 // TODO: if width = 0, fully remove 2189 2190 boolean isFirstSpacer = true; 2191 for (int row = start; row < end; row++) { 2192 Collection<ViewData> spacers = rowSpacers.get(row); 2193 if (spacers != null && !spacers.isEmpty()) { 2194 // Avoid ConcurrentModificationException since we're inserting into the 2195 // map within this loop (always at a different index, but the map doesn't 2196 // know that) 2197 spacers = new ArrayList<ViewData>(spacers); 2198 for (ViewData spacer : spacers) { 2199 if (isFirstSpacer) { 2200 isFirstSpacer = false; 2201 spacer.row = rowMap[start]; 2202 setGridAttribute(spacer.node, ATTR_LAYOUT_ROW, spacer.row); 2203 if (end - start > 1) { 2204 // Compute a merged width for all the spacers (not needed if 2205 // there's just one spacer; it should already have the correct height) 2206 int rowHeightDp = mRulesEngine.pxToDp(height); 2207 spacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, 2208 String.format(VALUE_N_DP, rowHeightDp)); 2209 } 2210 rowSpacers.put(start, spacer); 2211 } else { 2212 removedViews.add(spacer); // Mark for model removal 2213 layout.removeChild(spacer.node); 2214 } 2215 } 2216 } 2217 } 2218 2219 if (isFirstSpacer) { 2220 // No spacer: create one 2221 int rowWidthDp = mRulesEngine.pxToDp(height); 2222 addSpacer(layout, -1, rowMap[start], UNDEFINED, DEFAULT_CELL_WIDTH, rowWidthDp); 2223 } 2224 2225 start = end; 2226 } 2227 actualRowCount = newRow; 2228 // if (usedRows.contains(newRow)) { 2229 // actualRowCount++; 2230 // } 2231 2232 // Update the model: remove removed children from the view data list 2233 if (removedViews.size() <= 2) { 2234 mChildViews.removeAll(removedViews); 2235 } else { 2236 List<ViewData> remaining = 2237 new ArrayList<ViewData>(mChildViews.size() - removedViews.size()); 2238 for (ViewData view : mChildViews) { 2239 if (!removedViews.contains(view)) { 2240 remaining.add(view); 2241 } 2242 } 2243 mChildViews = remaining; 2244 } 2245 2246 // Update the final column and row declared attributes 2247 if (declaredColumnCount != UNDEFINED) { 2248 declaredColumnCount = actualColumnCount; 2249 setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount); 2250 } 2251 if (declaredRowCount != UNDEFINED) { 2252 declaredRowCount = actualRowCount; 2253 setGridAttribute(layout, ATTR_ROW_COUNT, actualRowCount); 2254 } 2255 } 2256 2257 /** 2258 * Adds a spacer to the given parent, at the given index. 2259 * 2260 * @param parent the GridLayout 2261 * @param index the index to insert the spacer at, or -1 to append 2262 * @param row the row to add the spacer to (or {@link #UNDEFINED} to not set a row yet 2263 * @param column the column to add the spacer to (or {@link #UNDEFINED} to not set a 2264 * column yet 2265 * @param widthDp the width in device independent pixels to assign to the spacer 2266 * @param heightDp the height in device independent pixels to assign to the spacer 2267 * @return the newly added spacer 2268 */ addSpacer(INode parent, int index, int row, int column, int widthDp, int heightDp)2269 ViewData addSpacer(INode parent, int index, int row, int column, 2270 int widthDp, int heightDp) { 2271 INode spacer; 2272 2273 String tag = FQCN_SPACE; 2274 String gridLayout = parent.getFqcn(); 2275 if (!gridLayout.equals(GRID_LAYOUT) && gridLayout.length() > GRID_LAYOUT.length()) { 2276 String pkg = gridLayout.substring(0, gridLayout.length() - GRID_LAYOUT.length()); 2277 tag = pkg + SPACE; 2278 } 2279 if (index != -1) { 2280 spacer = parent.insertChildAt(tag, index); 2281 } else { 2282 spacer = parent.appendChild(tag); 2283 } 2284 2285 ViewData view = new ViewData(spacer, index != -1 ? index : mChildViews.size()); 2286 mChildViews.add(view); 2287 2288 if (row != UNDEFINED) { 2289 view.row = row; 2290 setGridAttribute(spacer, ATTR_LAYOUT_ROW, row); 2291 } 2292 if (column != UNDEFINED) { 2293 view.column = column; 2294 setGridAttribute(spacer, ATTR_LAYOUT_COLUMN, column); 2295 } 2296 if (widthDp > 0) { 2297 spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, 2298 String.format(VALUE_N_DP, widthDp)); 2299 } 2300 if (heightDp > 0) { 2301 spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, 2302 String.format(VALUE_N_DP, heightDp)); 2303 } 2304 2305 // Temporary hack 2306 if (GridLayoutRule.sDebugGridLayout) { 2307 //String id = NEW_ID_PREFIX + "s"; 2308 //if (row == 0) { 2309 // id += "c"; 2310 //} 2311 //if (column == 0) { 2312 // id += "r"; 2313 //} 2314 //if (row > 0) { 2315 // id += Integer.toString(row); 2316 //} 2317 //if (column > 0) { 2318 // id += Integer.toString(column); 2319 //} 2320 String id = NEW_ID_PREFIX + "spacer_" //$NON-NLS-1$ 2321 + Integer.toString(System.identityHashCode(spacer)).substring(0, 3); 2322 spacer.setAttribute(ANDROID_URI, ATTR_ID, id); 2323 } 2324 2325 2326 return view; 2327 } 2328 2329 /** 2330 * Returns the string value of the given attribute, or null if it does not 2331 * exist. This only works for attributes that are GridLayout specific, such 2332 * as columnCount, layout_column, layout_row_span, etc. 2333 * 2334 * @param node the target node 2335 * @param name the attribute name (which must be in the android: namespace) 2336 * @return the attribute value or null 2337 */ 2338 getGridAttribute(INode node, String name)2339 public String getGridAttribute(INode node, String name) { 2340 return node.getStringAttr(getNamespace(), name); 2341 } 2342 2343 /** 2344 * Returns the integer value of the given attribute, or the given defaultValue if the 2345 * attribute was not set. This only works for attributes that are GridLayout specific, 2346 * such as columnCount, layout_column, layout_row_span, etc. 2347 * 2348 * @param node the target node 2349 * @param attribute the attribute name (which must be in the android: namespace) 2350 * @param defaultValue the default value to use if the value is not set 2351 * @return the attribute integer value 2352 */ getGridAttribute(INode node, String attribute, int defaultValue)2353 private int getGridAttribute(INode node, String attribute, int defaultValue) { 2354 String valueString = node.getStringAttr(getNamespace(), attribute); 2355 if (valueString != null) { 2356 try { 2357 return Integer.decode(valueString); 2358 } catch (NumberFormatException nufe) { 2359 // Ignore - error in user's XML 2360 } 2361 } 2362 2363 return defaultValue; 2364 } 2365 2366 /** 2367 * Returns the number of children views in the GridLayout 2368 * 2369 * @return the number of children views in the GridLayout 2370 */ getViewCount()2371 public int getViewCount() { 2372 return mChildViews.size(); 2373 } 2374 2375 /** 2376 * Returns true if the given class name represents a spacer 2377 * 2378 * @param fqcn the fully qualified class name 2379 * @return true if this is a spacer 2380 */ isSpace(String fqcn)2381 public static boolean isSpace(String fqcn) { 2382 return FQCN_SPACE.equals(fqcn) || FQCN_SPACE_V7.equals(fqcn); 2383 } 2384 } 2385