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