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