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