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