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