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