RelativeLayoutConversionHelper.java revision 0757ce4af2764e4dd564acc0b1a013e910abc8da
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.eclipse.adt.internal.editors.layout.refactoring;
17
18import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
19import static com.android.ide.common.layout.LayoutConstants.ATTR_BACKGROUND;
20import static com.android.ide.common.layout.LayoutConstants.ATTR_BASELINE_ALIGNED;
21import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE;
22import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE;
23import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
24import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT;
25import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
26import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
27import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
28import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
29import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT;
30import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP;
31import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
32import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW;
33import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
34import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL;
35import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY;
36import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
37import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_LEFT;
38import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_TOP;
39import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
40import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF;
41import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF;
42import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WEIGHT;
43import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
44import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
45import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_BOTTOM;
46import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER;
47import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER_HORIZONTAL;
48import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER_VERTICAL;
49import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL;
50import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
51import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_VERTICAL;
52import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_LEFT;
53import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_RIGHT;
54import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_TOP;
55import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
56import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT;
57import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
58import static com.android.ide.common.layout.LayoutConstants.RELATIVE_LAYOUT;
59import static com.android.ide.common.layout.LayoutConstants.VALUE_FALSE;
60import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DIP;
61import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE;
62import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;
63import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;
64
65import com.android.ide.eclipse.adt.AdtPlugin;
66import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
67import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
68import com.android.util.Pair;
69
70import org.eclipse.core.runtime.IStatus;
71import org.eclipse.swt.graphics.Rectangle;
72import org.eclipse.text.edits.MultiTextEdit;
73import org.w3c.dom.Attr;
74import org.w3c.dom.Element;
75import org.w3c.dom.NamedNodeMap;
76import org.w3c.dom.Node;
77import org.w3c.dom.NodeList;
78
79import java.io.PrintWriter;
80import java.io.StringWriter;
81import java.util.ArrayList;
82import java.util.Collections;
83import java.util.Comparator;
84import java.util.HashMap;
85import java.util.HashSet;
86import java.util.List;
87import java.util.Map;
88import java.util.Set;
89
90/**
91 * Helper class which performs the bulk of the layout conversion to relative layout
92 * <p>
93 * Future enhancements:
94 * <ul>
95 * <li>Render the layout at multiple screen sizes and analyze how the widgets move and
96 * stretch and use that to add in additional constraints
97 * <li> Adapt the LinearLayout analysis code to work with TableLayouts and TableRows as well
98 * (just need to tweak the "isVertical" interpretation to account for the different defaults,
99 * and perhaps do something about column size properties.
100 * <li> We need to take into account existing margins and clear/update them
101 * </ul>
102 */
103class RelativeLayoutConversionHelper {
104    private final MultiTextEdit mRootEdit;
105    private final boolean mFlatten;
106    private final Element mLayout;
107    private final ChangeLayoutRefactoring mRefactoring;
108    private final CanvasViewInfo mRootView;
109
110    RelativeLayoutConversionHelper(ChangeLayoutRefactoring refactoring,
111            Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) {
112        mRefactoring = refactoring;
113        mLayout = layout;
114        mFlatten = flatten;
115        mRootEdit = rootEdit;
116        mRootView = rootView;
117    }
118
119    /** Performs conversion from any layout to a RelativeLayout */
120    public void convertToRelative() {
121        // Locate the view for the layout
122        CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout);
123        if (layoutView == null || layoutView.getChildren().size() == 0) {
124            // No children. THAT was an easy conversion!
125            return;
126        }
127
128        // Study the layout and get information about how to place individual elements
129        List<View> views = analyzeLayout(layoutView);
130
131        // Create/update relative layout constraints
132        createAttachments(views);
133    }
134
135    /**
136     * Analyzes the given view hierarchy and produces a list of {@link View} objects which
137     * contain placement information for each element
138     */
139    private List<View> analyzeLayout(CanvasViewInfo layoutView) {
140        EdgeList edgeList = new EdgeList(layoutView);
141        deleteRemovedElements(edgeList.getDeletedElements());
142
143        List<Integer> columnOffsets = edgeList.getColumnOffsets();
144        List<Integer> rowOffsets = edgeList.getRowOffsets();
145
146        // Compute x/y offsets for each row/column index
147        int[] left = new int[columnOffsets.size()];
148        int[] top = new int[rowOffsets.size()];
149
150        Map<Integer, Integer> xToCol = new HashMap<Integer, Integer>();
151        int columnIndex = 0;
152        for (Integer offset : columnOffsets) {
153            left[columnIndex] = offset;
154            xToCol.put(offset, columnIndex++);
155        }
156        Map<Integer, Integer> yToRow = new HashMap<Integer, Integer>();
157        int rowIndex = 0;
158        for (Integer offset : rowOffsets) {
159            top[rowIndex] = offset;
160            yToRow.put(offset, rowIndex++);
161        }
162
163        // Create a complete list of view objects
164        List<View> views = createViews(edgeList, columnOffsets);
165        initializeSpans(edgeList, columnOffsets, rowOffsets, xToCol, yToRow);
166
167        // Sanity check
168        for (View view : views) {
169            assert view.getLeftEdge() == left[view.mCol];
170            assert view.getTopEdge() == top[view.mRow];
171            assert view.getRightEdge() == left[view.mCol+view.mColSpan];
172            assert view.getBottomEdge() == top[view.mRow+view.mRowSpan];
173        }
174
175        // Ensure that every view has a proper id such that it can be referred to
176        // with a constraint
177        initializeIds(edgeList, views);
178
179        // Attempt to lay the views out in a grid with constraints (though not that widgets
180        // can overlap as well)
181        Grid grid = new Grid(views, left, top);
182        computeKnownConstraints(views, edgeList);
183        computeHorizontalConstraints(grid);
184        computeVerticalConstraints(grid);
185
186        return views;
187    }
188
189    /** Produces a list of {@link View} objects from an {@link EdgeList} */
190    private List<View> createViews(EdgeList edgeList, List<Integer> columnOffsets) {
191        List<View> views = new ArrayList<View>();
192        for (Integer offset : columnOffsets) {
193            List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
194            if (leftEdgeViews == null) {
195                // must have been a right edge
196                continue;
197            }
198            for (View view : leftEdgeViews) {
199                views.add(view);
200            }
201        }
202        return views;
203    }
204
205    /** Removes any elements targeted for deletion */
206    private void deleteRemovedElements(List<Element> delete) {
207        if (mFlatten && delete.size() > 0) {
208            for (Element element : delete) {
209                mRefactoring.removeElementTags(mRootEdit, element, delete);
210            }
211        }
212    }
213
214    /** Ensures that every element has an id such that it can be referenced from a constraint */
215    private void initializeIds(EdgeList edgeList, List<View> views) {
216        // Ensure that all views have a valid id
217        for (View view : views) {
218            String id = mRefactoring.ensureHasId(mRootEdit, view.mElement, null);
219            edgeList.setIdAttributeValue(view, id);
220        }
221    }
222
223    /**
224     * Initializes the column and row indices, as well as any column span and row span
225     * values
226     */
227    private void initializeSpans(EdgeList edgeList, List<Integer> columnOffsets,
228            List<Integer> rowOffsets, Map<Integer, Integer> xToCol, Map<Integer, Integer> yToRow) {
229        // Now initialize table view row, column and spans
230        for (Integer offset : columnOffsets) {
231            List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
232            if (leftEdgeViews == null) {
233                // must have been a right edge
234                continue;
235            }
236            for (View view : leftEdgeViews) {
237                Integer col = xToCol.get(view.getLeftEdge());
238                assert col != null;
239                Integer end = xToCol.get(view.getRightEdge());
240                assert end != null;
241
242                view.mCol = col;
243                view.mColSpan = end - col;
244            }
245        }
246
247        for (Integer offset : rowOffsets) {
248            List<View> topEdgeViews = edgeList.getTopEdgeViews(offset);
249            if (topEdgeViews == null) {
250                // must have been a bottom edge
251                continue;
252            }
253            for (View view : topEdgeViews) {
254                Integer row = yToRow.get(view.getTopEdge());
255                assert row != null;
256                Integer end = yToRow.get(view.getBottomEdge());
257                assert end != null;
258
259                view.mRow = row;
260                view.mRowSpan = end - row;
261            }
262        }
263    }
264
265    /**
266     * Creates refactoring edits which adds or updates constraints for the given list of
267     * views
268     */
269    private void createAttachments(List<View> views) {
270        // Make the attachments
271        String namespace = mRefactoring.getAndroidNamespacePrefix();
272        for (View view : views) {
273            for (Pair<String, String> constraint : view.getHorizConstraints()) {
274                mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
275                        namespace, constraint.getFirst(), constraint.getSecond());
276            }
277            for (Pair<String, String> constraint : view.getVerticalConstraints()) {
278                mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
279                        namespace, constraint.getFirst(), constraint.getSecond());
280            }
281        }
282    }
283
284    /**
285     * Analyzes the existing layouts and layout parameter objects in the document to infer
286     * constraints for layout types that we know about - such as LinearLayout baseline
287     * alignment, weights, gravity, etc.
288     */
289    private void computeKnownConstraints(List<View> views, EdgeList edgeList) {
290        // List of parent layout elements we've already processed. We iterate through all
291        // the -children-, and we ask each for its element parent (which won't have a view)
292        // and we look at the parent's layout attributes and its children layout constraints,
293        // and then we stash away constraints that we can infer. This means that we will
294        // encounter the same parent for every sibling, so that's why there's a map to
295        // prevent duplicate work.
296        Set<Node> seen = new HashSet<Node>();
297
298        for (View view : views) {
299            Element element = view.getElement();
300            Node parent = element.getParentNode();
301            if (seen.contains(parent)) {
302                continue;
303            }
304            seen.add(parent);
305
306            if (parent.getNodeType() != Node.ELEMENT_NODE) {
307                continue;
308            }
309            Element layout = (Element) parent;
310            String layoutName = layout.getTagName();
311
312            if (LINEAR_LAYOUT.equals(layoutName)) {
313                analyzeLinearLayout(edgeList, layout);
314            } else if (RELATIVE_LAYOUT.equals(layoutName)) {
315                analyzeRelativeLayout(edgeList, layout);
316            } else {
317                // Some other layout -- add more conditional handling here
318                // for framelayout, tables, etc.
319            }
320        }
321    }
322
323    private static final int GRAVITY_LEFT = 1 << 0;
324    private static final int GRAVITY_RIGHT = 1<< 1;
325    private static final int GRAVITY_CENTER_HORIZ = 1 << 2;
326    private static final int GRAVITY_FILL_HORIZ = 1 << 3;
327    private static final int GRAVITY_CENTER_VERT = 1 << 4;
328    private static final int GRAVITY_FILL_VERT = 1 << 5;
329    private static final int GRAVITY_TOP = 1 << 6;
330    private static final int GRAVITY_BOTTOM = 1 << 7;
331    private static final int GRAVITY_HORIZ_MASK = GRAVITY_CENTER_HORIZ | GRAVITY_FILL_HORIZ
332            | GRAVITY_LEFT | GRAVITY_RIGHT;
333    private static final int GRAVITY_VERT_MASK = GRAVITY_CENTER_VERT | GRAVITY_FILL_VERT
334            | GRAVITY_TOP | GRAVITY_BOTTOM;
335
336    /** Returns the gravity of the given element */
337    private static int getGravity(Element element) {
338        int gravity = GRAVITY_LEFT | GRAVITY_TOP;
339        String gravityString = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY);
340        if (gravityString != null && gravityString.length() > 0) {
341            String[] anchors = gravityString.split("\\|"); //$NON-NLS-1$
342            for (String anchor : anchors) {
343                if (GRAVITY_VALUE_CENTER.equals(anchor)) {
344                    gravity = GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT;
345                } else if (GRAVITY_VALUE_FILL.equals(anchor)) {
346                        gravity = GRAVITY_FILL_HORIZ | GRAVITY_FILL_VERT;
347                } else if (GRAVITY_VALUE_CENTER_VERTICAL.equals(anchor)) {
348                    gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_CENTER_VERT;
349                } else if (GRAVITY_VALUE_CENTER_HORIZONTAL.equals(anchor)) {
350                    gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_CENTER_HORIZ;
351                } else if (GRAVITY_VALUE_FILL_VERTICAL.equals(anchor)) {
352                    gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_FILL_VERT;
353                } else if (GRAVITY_VALUE_FILL_HORIZONTAL.equals(anchor)) {
354                    gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_FILL_HORIZ;
355                } else if (GRAVITY_VALUE_TOP.equals(anchor)) {
356                    gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_TOP;
357                } else if (GRAVITY_VALUE_BOTTOM.equals(anchor)) {
358                    gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_BOTTOM;
359                } else if (GRAVITY_VALUE_LEFT.equals(anchor)) {
360                    gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_LEFT;
361                } else if (GRAVITY_VALUE_RIGHT.equals(anchor)) {
362                    gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_RIGHT;
363                } else {
364                    // "clip" not supported
365                }
366            }
367        }
368
369        return gravity;
370    }
371
372    /**
373     * Returns the element children of the given element
374     *
375     * @param element the parent element
376     * @return a list of child elements, possibly empty but never null
377     */
378    public static List<Element> getChildren(Element element) {
379        // Convenience to avoid lots of ugly DOM access casting
380        NodeList children = element.getChildNodes();
381        // An iterator would have been more natural (to directly drive the child list
382        // iteration) but iterators can't be used in enhanced for loops...
383        List<Element> result = new ArrayList<Element>(children.getLength());
384        for (int i = 0, n = children.getLength(); i < n; i++) {
385            Node node = children.item(i);
386            if (node.getNodeType() == Node.ELEMENT_NODE) {
387                Element child = (Element) node;
388                result.add(child);
389            }
390        }
391
392        return result;
393    }
394
395    /**
396     * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it
397     * does not define a weight
398     */
399    private float getWeight(Element linearLayoutChild) {
400        String weight = linearLayoutChild.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
401        if (weight != null && weight.length() > 0) {
402            try {
403                return Float.parseFloat(weight);
404            } catch (NumberFormatException nfe) {
405                AdtPlugin.log(nfe, "Invalid weight %1$s", weight);
406            }
407        }
408
409        return 0.0f;
410    }
411
412    /**
413     * Returns the sum of all the layout weights of the children in the given LinearLayout
414     *
415     * @param linearLayout the layout to compute the total sum for
416     * @return the total sum of all the layout weights in the given layout
417     */
418    private float getWeightSum(Element linearLayout) {
419        float sum = 0;
420        for (Element child : getChildren(linearLayout)) {
421            sum += getWeight(child);
422        }
423
424        return sum;
425    }
426
427    /**
428     * Analyzes the given LinearLayout and updates the constraints to reflect
429     * relationships it can infer - based on baseline alignment, gravity, order and
430     * weights. This method also removes "0dip" as a special width/height used in
431     * LinearLayouts with weight distribution.
432     */
433    private void analyzeLinearLayout(EdgeList edgeList, Element layout) {
434        boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
435                ATTR_ORIENTATION));
436        View baselineRef = null;
437        if (!isVertical &&
438            !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED))) {
439            // Baseline alignment. Find the tallest child and set it as the baseline reference.
440            int tallestHeight = 0;
441            View tallest = null;
442            for (Element child : getChildren(layout)) {
443                View view = edgeList.getView(child);
444                if (view != null && view.getHeight() > tallestHeight) {
445                    tallestHeight = view.getHeight();
446                    tallest = view;
447                }
448            }
449            if (tallest != null) {
450                baselineRef = tallest;
451            }
452        }
453
454        float weightSum = getWeightSum(layout);
455        float cumulativeWeight = 0;
456
457        List<Element> children = getChildren(layout);
458        String prevId = null;
459        boolean isFirstChild = true;
460        boolean linkBackwards = true;
461        boolean linkForwards = false;
462
463        for (int index = 0, childCount = children.size(); index < childCount; index++) {
464            Element child = children.get(index);
465
466            View childView = edgeList.getView(child);
467            if (childView == null) {
468                // Could be a nested layout that is being removed etc
469                prevId = null;
470                isFirstChild = false;
471                continue;
472            }
473
474            // Look at the layout_weight attributes and determine whether we should be
475            // attached on the bottom/right or on the top/left
476            if (weightSum > 0.0f) {
477                float weight = getWeight(child);
478
479                // We can't emulate a LinearLayout where multiple children have positive
480                // weights. However, we CAN support the common scenario where a single
481                // child has a non-zero weight, and all children after it are pushed
482                // to the end and the weighted child fills the remaining space.
483                if (cumulativeWeight == 0 && weight > 0) {
484                    // See if we have a bottom/right edge to attach the forwards link to
485                    // (at the end of the forwards chains). Only if so can we link forwards.
486                    View referenced;
487                    if (isVertical) {
488                        referenced = edgeList.getSharedBottomEdge(layout);
489                    } else {
490                        referenced = edgeList.getSharedRightEdge(layout);
491                    }
492                    if (referenced != null) {
493                        linkForwards = true;
494                    }
495                } else if (cumulativeWeight > 0) {
496                    linkBackwards = false;
497                }
498
499                cumulativeWeight += weight;
500            }
501
502            analyzeGravity(edgeList, layout, isVertical, child, childView);
503            convert0dipToWrapContent(child);
504
505            // Chain elements together in the flow direction of the linear layout
506            if (prevId != null) { // No constraint for first child
507                if (linkBackwards) {
508                    if (isVertical) {
509                        childView.addVerticalConstraint(ATTR_LAYOUT_BELOW, prevId);
510                    } else {
511                        childView.addHorizConstraint(ATTR_LAYOUT_TO_RIGHT_OF, prevId);
512                    }
513                }
514            } else if (isFirstChild) {
515                assert linkBackwards;
516
517                // First element; attach it to the parent if we can
518                if (isVertical) {
519                    View referenced = edgeList.getSharedTopEdge(layout);
520                    if (referenced != null) {
521                        if (isAncestor(referenced.getElement(), child)) {
522                            childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
523                                VALUE_TRUE);
524                        } else {
525                            childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
526                                    referenced.getId());
527                        }
528                    }
529                } else {
530                    View referenced = edgeList.getSharedLeftEdge(layout);
531                    if (referenced != null) {
532                        if (isAncestor(referenced.getElement(), child)) {
533                            childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
534                                    VALUE_TRUE);
535                        } else {
536                            childView.addHorizConstraint(
537                                    ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
538                        }
539                    }
540                }
541            }
542
543            if (linkForwards) {
544                if (index < (childCount - 1)) {
545                    Element nextChild = children.get(index + 1);
546                    String nextId = mRefactoring.ensureHasId(mRootEdit, nextChild, null);
547                    if (nextId != null) {
548                        if (isVertical) {
549                            childView.addVerticalConstraint(ATTR_LAYOUT_ABOVE, nextId);
550                        } else {
551                            childView.addHorizConstraint(ATTR_LAYOUT_TO_LEFT_OF, nextId);
552                        }
553                    }
554                } else {
555                    // Attach to right/bottom edge of the layout
556                    if (isVertical) {
557                        View referenced = edgeList.getSharedBottomEdge(layout);
558                        if (referenced != null) {
559                            if (isAncestor(referenced.getElement(), child)) {
560                                childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
561                                    VALUE_TRUE);
562                            } else {
563                                childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
564                                        referenced.getId());
565                            }
566                        }
567                    } else {
568                        View referenced = edgeList.getSharedRightEdge(layout);
569                        if (referenced != null) {
570                            if (isAncestor(referenced.getElement(), child)) {
571                                childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
572                                        VALUE_TRUE);
573                            } else {
574                                childView.addHorizConstraint(
575                                        ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
576                            }
577                        }
578                    }
579                }
580            }
581
582            if (baselineRef != null && !baselineRef.equals(childView.getId())) {
583                assert !isVertical;
584                // Only align if they share the same gravity
585                if ((childView.getGravity() & GRAVITY_VERT_MASK) ==
586                        (baselineRef.getGravity() & GRAVITY_VERT_MASK)) {
587                    childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_BASELINE, baselineRef.getId());
588                }
589            }
590
591            prevId = mRefactoring.ensureHasId(mRootEdit, child, null);
592            isFirstChild = false;
593        }
594    }
595
596    /**
597     * Checks the layout "gravity" value for the given child and updates the constraints
598     * to account for the gravity
599     */
600    private int analyzeGravity(EdgeList edgeList, Element layout, boolean isVertical,
601            Element child, View childView) {
602        // Use gravity to constrain elements in the axis orthogonal to the
603        // direction of the layout
604        int gravity = childView.getGravity();
605        if (isVertical) {
606            if ((gravity & GRAVITY_RIGHT) != 0) {
607                View referenced = edgeList.getSharedRightEdge(layout);
608                if (referenced != null) {
609                    if (isAncestor(referenced.getElement(), child)) {
610                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
611                                VALUE_TRUE);
612                    } else {
613                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
614                                referenced.getId());
615                    }
616                }
617            } else if ((gravity & GRAVITY_CENTER_HORIZ) != 0) {
618                View referenced1 = edgeList.getSharedLeftEdge(layout);
619                View referenced2 = edgeList.getSharedRightEdge(layout);
620                if (referenced1 != null && referenced2 == referenced1) {
621                    if (isAncestor(referenced1.getElement(), child)) {
622                        childView.addHorizConstraint(ATTR_LAYOUT_CENTER_HORIZONTAL,
623                                VALUE_TRUE);
624                    }
625                }
626            } else if ((gravity & GRAVITY_FILL_HORIZ) != 0) {
627                View referenced1 = edgeList.getSharedLeftEdge(layout);
628                View referenced2 = edgeList.getSharedRightEdge(layout);
629                if (referenced1 != null && referenced2 == referenced1) {
630                    if (isAncestor(referenced1.getElement(), child)) {
631                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
632                                VALUE_TRUE);
633                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
634                                VALUE_TRUE);
635                    } else {
636                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
637                                referenced1.getId());
638                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
639                                referenced2.getId());
640                    }
641                }
642            } else if ((gravity & GRAVITY_LEFT) != 0) {
643                View referenced = edgeList.getSharedLeftEdge(layout);
644                if (referenced != null) {
645                    if (isAncestor(referenced.getElement(), child)) {
646                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
647                                VALUE_TRUE);
648                    } else {
649                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
650                                referenced.getId());
651                    }
652                }
653            }
654        } else {
655            // Handle horizontal layout: perform vertical gravity attachments
656            if ((gravity & GRAVITY_BOTTOM) != 0) {
657                View referenced = edgeList.getSharedBottomEdge(layout);
658                if (referenced != null) {
659                    if (isAncestor(referenced.getElement(), child)) {
660                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
661                                VALUE_TRUE);
662                    } else {
663                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
664                                referenced.getId());
665                    }
666                }
667            } else if ((gravity & GRAVITY_CENTER_VERT) != 0) {
668                View referenced1 = edgeList.getSharedTopEdge(layout);
669                View referenced2 = edgeList.getSharedBottomEdge(layout);
670                if (referenced1 != null && referenced2 == referenced1) {
671                    if (isAncestor(referenced1.getElement(), child)) {
672                        childView.addVerticalConstraint(ATTR_LAYOUT_CENTER_VERTICAL,
673                                VALUE_TRUE);
674                    }
675                }
676            } else if ((gravity & GRAVITY_FILL_VERT) != 0) {
677                View referenced1 = edgeList.getSharedTopEdge(layout);
678                View referenced2 = edgeList.getSharedBottomEdge(layout);
679                if (referenced1 != null && referenced2 == referenced1) {
680                    if (isAncestor(referenced1.getElement(), child)) {
681                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
682                                VALUE_TRUE);
683                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
684                                VALUE_TRUE);
685                    } else {
686                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
687                                referenced1.getId());
688                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
689                                referenced2.getId());
690                    }
691                }
692            } else if ((gravity & GRAVITY_TOP) != 0) {
693                View referenced = edgeList.getSharedTopEdge(layout);
694                if (referenced != null) {
695                    if (isAncestor(referenced.getElement(), child)) {
696                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
697                                VALUE_TRUE);
698                    } else {
699                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
700                                referenced.getId());
701                    }
702                }
703            }
704        }
705        return gravity;
706    }
707
708    /** Converts 0dip values in layout_width and layout_height to wrap_content instead */
709    private void convert0dipToWrapContent(Element child) {
710        // Must convert layout_height="0dip" to layout_height="wrap_content".
711        // 0dip is a special trick used in linear layouts in the presence of
712        // weights where 0dip ensures that the height of the view is not taken
713        // into account when distributing the weights. However, when converted
714        // to RelativeLayout this will instead cause the view to actually be assigned
715        // 0 height.
716        String height = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
717        // 0dip, 0dp, 0px, etc
718        if (height != null && height.startsWith("0")) { //$NON-NLS-1$
719            mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
720                    mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_HEIGHT,
721                    VALUE_WRAP_CONTENT);
722        }
723        String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
724        if (width != null && width.startsWith("0")) { //$NON-NLS-1$
725            mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
726                    mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_WIDTH,
727                    VALUE_WRAP_CONTENT);
728        }
729    }
730
731    /**
732     * Analyzes an embedded RelativeLayout within a layout hierarchy and updates the
733     * constraints in the EdgeList with those relationships which can continue in the
734     * outer single RelativeLayout.
735     */
736    private void analyzeRelativeLayout(EdgeList edgeList, Element layout) {
737        NodeList children = layout.getChildNodes();
738        for (int i = 0, n = children.getLength(); i < n; i++) {
739            Node node = children.item(i);
740            if (node.getNodeType() == Node.ELEMENT_NODE) {
741                Element child = (Element) node;
742                View childView = edgeList.getView(child);
743                if (childView == null) {
744                    // Could be a nested layout that is being removed etc
745                    continue;
746                }
747
748                NamedNodeMap attributes = child.getAttributes();
749                for (int j = 0, m = attributes.getLength(); j < m; j++) {
750                    Attr attribute = (Attr) attributes.item(j);
751                    String name = attribute.getLocalName();
752                    String value = attribute.getValue();
753                    if (name.equals(ATTR_LAYOUT_WIDTH)
754                            || name.equals(ATTR_LAYOUT_HEIGHT)) {
755                        // Ignore these for now
756                    } else if (name.startsWith(ATTR_LAYOUT_PREFIX)
757                            && ANDROID_URI.equals(attribute.getNamespaceURI())) {
758                        // Determine if the reference is to a known edge
759                        String id = getIdBasename(value);
760                        if (id != null) {
761                            View referenced = edgeList.getView(id);
762                            if (referenced != null) {
763                                // This is a valid reference, so preserve
764                                // the attribute
765                                if (name.equals(ATTR_LAYOUT_BELOW) ||
766                                        name.equals(ATTR_LAYOUT_ABOVE) ||
767                                        name.equals(ATTR_LAYOUT_ALIGN_TOP) ||
768                                        name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) ||
769                                        name.equals(ATTR_LAYOUT_ALIGN_BASELINE)) {
770                                    // Vertical constraint
771                                    childView.addVerticalConstraint(name, value);
772                                } else if (name.equals(ATTR_LAYOUT_ALIGN_LEFT) ||
773                                        name.equals(ATTR_LAYOUT_TO_LEFT_OF) ||
774                                        name.equals(ATTR_LAYOUT_TO_RIGHT_OF) ||
775                                        name.equals(ATTR_LAYOUT_ALIGN_RIGHT)) {
776                                    // Horizontal constraint
777                                    childView.addHorizConstraint(name, value);
778                                } else {
779                                    // We don't expect this
780                                    assert false : name;
781                                }
782                            } else {
783                                // Reference to some layout that is not included here.
784                                // TODO: See if the given layout has an edge
785                                // that corresponds to one of our known views
786                                // so we can adjust the constraints and keep it after all.
787                            }
788                        } else {
789                            // It's a parent-relative constraint (such
790                            // as aligning with a parent edge, or centering
791                            // in the parent view)
792                            boolean remove = true;
793                            if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
794                                View referenced = edgeList.getSharedLeftEdge(layout);
795                                if (referenced != null) {
796                                    if (isAncestor(referenced.getElement(), child)) {
797                                        childView.addHorizConstraint(name, VALUE_TRUE);
798                                    } else {
799                                        childView.addHorizConstraint(
800                                                ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
801                                    }
802                                    remove = false;
803                                }
804                            } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
805                                View referenced = edgeList.getSharedRightEdge(layout);
806                                if (referenced != null) {
807                                    if (isAncestor(referenced.getElement(), child)) {
808                                        childView.addHorizConstraint(name, VALUE_TRUE);
809                                    } else {
810                                        childView.addHorizConstraint(
811                                            ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
812                                    }
813                                    remove = false;
814                                }
815                            } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_TOP)) {
816                                View referenced = edgeList.getSharedTopEdge(layout);
817                                if (referenced != null) {
818                                    if (isAncestor(referenced.getElement(), child)) {
819                                        childView.addVerticalConstraint(name, VALUE_TRUE);
820                                    } else {
821                                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
822                                                referenced.getId());
823                                    }
824                                    remove = false;
825                                }
826                            } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM)) {
827                                View referenced = edgeList.getSharedBottomEdge(layout);
828                                if (referenced != null) {
829                                    if (isAncestor(referenced.getElement(), child)) {
830                                        childView.addVerticalConstraint(name, VALUE_TRUE);
831                                    } else {
832                                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
833                                                referenced.getId());
834                                    }
835                                    remove = false;
836                                }
837                            }
838
839                            boolean alignWithParent =
840                                    name.equals(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING);
841                            if (remove && alignWithParent) {
842                                // TODO - look for this one AFTER we have processed
843                                // everything else, and then set constraints as necessary
844                                // IF there are no other conflicting constraints!
845                            }
846
847                            // Otherwise it's some kind of centering which we don't support
848                            // yet.
849
850                            // TODO: Find a way to determine whether we have
851                            // a corresponding edge for the parent (e.g. if
852                            // the ViewInfo bounds match our outer parent or
853                            // some other edge) and if so, substitute for that
854                            // id.
855                            // For example, if this element was centered
856                            // horizontally in a RelativeLayout that actually
857                            // occupies the entire width of our outer layout,
858                            // then it can be preserved after all!
859
860                            if (remove) {
861                                if (name.startsWith("layout_margin")) { //$NON-NLS-1$
862                                    continue;
863                                }
864
865                                // Remove unknown attributes?
866                                // It's too early to do this, because we may later want
867                                // to *set* this value and it would result in an overlapping edits
868                                // exception. Therefore, we need to RECORD which attributes should
869                                // be removed, which lines should have its indentation adjusted
870                                // etc and finally process it all at the end!
871                                //mRefactoring.removeAttribute(mRootEdit, child,
872                                //        attribute.getNamespaceURI(), name);
873                            }
874                        }
875                    }
876                }
877            }
878        }
879    }
880
881    /**
882     * Given {@code @id/foo} or {@code @+id/foo}, returns foo. Note that given foo it will
883     * return null.
884     */
885    private static String getIdBasename(String id) {
886        if (id.startsWith(NEW_ID_PREFIX)) {
887            return id.substring(NEW_ID_PREFIX.length());
888        } else if (id.startsWith(ID_PREFIX)) {
889            return id.substring(ID_PREFIX.length());
890        }
891
892        return null;
893    }
894
895    /** Returns true if the given second argument is a descendant of the first argument */
896    private static boolean isAncestor(Node ancestor, Node node) {
897        while (node != null) {
898            if (node == ancestor) {
899                return true;
900            }
901            node = node.getParentNode();
902        }
903        return false;
904    }
905
906    /**
907     * Computes horizontal constraints for the views in the grid for any remaining views
908     * that do not have constraints (as the result of the analysis of known layouts). This
909     * will look at the rendered layout coordinates and attempt to connect elements based
910     * on a spatial layout in the grid.
911     */
912    private void computeHorizontalConstraints(Grid grid) {
913        int columns = grid.getColumns();
914
915        String attachLeftProperty = ATTR_LAYOUT_ALIGN_PARENT_LEFT;
916        String attachLeftValue = VALUE_TRUE;
917        int marginLeft = 0;
918        for (int col = 0; col < columns; col++) {
919            if (!grid.colContainsTopLeftCorner(col)) {
920                // Just accumulate margins for the next column
921                marginLeft += grid.getColumnWidth(col);
922            } else {
923                // Add horizontal attachments
924                String firstId = null;
925                for (View view : grid.viewsStartingInCol(col, true)) {
926                    assert view.getId() != null;
927                    if (firstId == null) {
928                        firstId = view.getId();
929                        if (view.isConstrainedHorizontally()) {
930                            // Nothing to do -- we already have an accurate position for
931                            // this view
932                        } else if (attachLeftProperty != null) {
933                            view.addHorizConstraint(attachLeftProperty, attachLeftValue);
934                            if (marginLeft > 0) {
935                                view.addHorizConstraint(ATTR_LAYOUT_MARGIN_LEFT,
936                                        String.format(VALUE_N_DIP, marginLeft));
937                                marginLeft = 0;
938                            }
939                        } else {
940                            assert false;
941                        }
942                    } else if (!view.isConstrainedHorizontally()) {
943                        view.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, firstId);
944                    }
945                }
946            }
947
948            // Figure out edge for the next column
949            View view = grid.findRightEdgeView(col);
950            if (view != null) {
951                assert view.getId() != null;
952                attachLeftProperty = ATTR_LAYOUT_TO_RIGHT_OF;
953                attachLeftValue = view.getId();
954
955                marginLeft = 0;
956            } else if (marginLeft == 0) {
957                marginLeft = grid.getColumnWidth(col);
958            }
959        }
960    }
961
962    /**
963     * Performs vertical layout just like the {@link #computeHorizontalConstraints} method
964     * did horizontally
965     */
966    private void computeVerticalConstraints(Grid grid) {
967        int rows = grid.getRows();
968
969        String attachTopProperty = ATTR_LAYOUT_ALIGN_PARENT_TOP;
970        String attachTopValue = VALUE_TRUE;
971        int marginTop = 0;
972        for (int row = 0; row < rows; row++) {
973            if (!grid.rowContainsTopLeftCorner(row)) {
974                // Just accumulate margins for the next column
975                marginTop += grid.getRowHeight(row);
976            } else {
977                // Add horizontal attachments
978                String firstId = null;
979                for (View view : grid.viewsStartingInRow(row, true)) {
980                    assert view.getId() != null;
981                    if (firstId == null) {
982                        firstId = view.getId();
983                        if (view.isConstrainedVertically()) {
984                            // Nothing to do -- we already have an accurate position for
985                            // this view
986                        } else if (attachTopProperty != null) {
987                            view.addVerticalConstraint(attachTopProperty, attachTopValue);
988                            if (marginTop > 0) {
989                                view.addVerticalConstraint(ATTR_LAYOUT_MARGIN_TOP,
990                                        String.format(VALUE_N_DIP, marginTop));
991                                marginTop = 0;
992                            }
993                        } else {
994                            assert false;
995                        }
996                    } else if (!view.isConstrainedVertically()) {
997                        view.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, firstId);
998                    }
999                }
1000            }
1001
1002            // Figure out edge for the next row
1003            View view = grid.findBottomEdgeView(row);
1004            if (view != null) {
1005                assert view.getId() != null;
1006                attachTopProperty = ATTR_LAYOUT_BELOW;
1007                attachTopValue = view.getId();
1008                marginTop = 0;
1009            } else if (marginTop == 0) {
1010                marginTop = grid.getRowHeight(row);
1011            }
1012        }
1013    }
1014
1015    /**
1016     * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given
1017     * {@link Element}
1018     *
1019     * @param info the root {@link CanvasViewInfo} to search below
1020     * @param element the target element
1021     * @return the {@link CanvasViewInfo} which corresponds to the given element
1022     */
1023    private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) {
1024        if (getElement(info) == element) {
1025            return info;
1026        }
1027
1028        for (CanvasViewInfo child : info.getChildren()) {
1029            CanvasViewInfo result = findViewForElement(child, element);
1030            if (result != null) {
1031                return result;
1032            }
1033        }
1034
1035        return null;
1036    }
1037
1038    /** Returns the {@link Element} for the given {@link CanvasViewInfo} */
1039    private static Element getElement(CanvasViewInfo info) {
1040        Node node = info.getUiViewNode().getXmlNode();
1041        if (node instanceof Element) {
1042            return (Element) node;
1043        }
1044
1045        return null;
1046    }
1047
1048    /**
1049     * A grid of cells which can contain views, used to infer spatial relationships when
1050     * computing constraints. Note that a view can appear in than one cell; they will
1051     * appear in all cells that their bounds overlap with!
1052     */
1053    private class Grid {
1054        private final int[] mLeft;
1055        private final int[] mTop;
1056        // A list from row to column to cell, where a cell is a list of views
1057        private final List<List<List<View>>> mRowList;
1058        private int mRowCount;
1059        private int mColCount;
1060
1061        Grid(List<View> views, int[] left, int[] top) {
1062            mLeft = left;
1063            mTop = top;
1064
1065            // The left/top arrays should include the ending point too
1066            mColCount = left.length - 1;
1067            mRowCount = top.length - 1;
1068
1069            // Using nested lists rather than arrays to avoid lack of typed arrays
1070            // (can't create List<View>[row][column] arrays)
1071            mRowList = new ArrayList<List<List<View>>>(top.length);
1072            for (int row = 0; row < top.length; row++) {
1073                List<List<View>> columnList = new ArrayList<List<View>>(left.length);
1074                for (int col = 0; col < left.length; col++) {
1075                    columnList.add(new ArrayList<View>(4));
1076                }
1077                mRowList.add(columnList);
1078            }
1079
1080            for (View view : views) {
1081                // Get rid of the root view; we don't want that in the attachments logic;
1082                // it was there originally such that it would contribute the outermost
1083                // edges.
1084                if (view.mElement == mLayout) {
1085                    continue;
1086                }
1087
1088                for (int i = 0; i < view.mRowSpan; i++) {
1089                    for (int j = 0; j < view.mColSpan; j++) {
1090                        mRowList.get(view.mRow + i).get(view.mCol + j).add(view);
1091                    }
1092                }
1093            }
1094        }
1095
1096        /**
1097         * Returns the number of rows in the grid
1098         *
1099         * @return the row count
1100         */
1101        public int getRows() {
1102            return mRowCount;
1103        }
1104
1105        /**
1106         * Returns the number of columns in the grid
1107         *
1108         * @return the column count
1109         */
1110        public int getColumns() {
1111            return mColCount;
1112        }
1113
1114        /**
1115         * Returns the list of views overlapping the given cell
1116         *
1117         * @param row the row of the target cell
1118         * @param col the column of the target cell
1119         * @return a list of views overlapping the given column
1120         */
1121        public List<View> get(int row, int col) {
1122            return mRowList.get(row).get(col);
1123        }
1124
1125        /**
1126         * Returns true if the given column contains a top left corner of a view
1127         *
1128         * @param column the column to check
1129         * @return true if one or more views have their top left corner in this column
1130         */
1131        public boolean colContainsTopLeftCorner(int column) {
1132            for (int row = 0; row < mRowCount; row++) {
1133                View view = getTopLeftCorner(row, column);
1134                if (view != null) {
1135                    return true;
1136                }
1137            }
1138
1139            return false;
1140        }
1141
1142        /**
1143         * Returns true if the given row contains a top left corner of a view
1144         *
1145         * @param row the row to check
1146         * @return true if one or more views have their top left corner in this row
1147         */
1148        public boolean rowContainsTopLeftCorner(int row) {
1149            for (int col = 0; col < mColCount; col++) {
1150                View view = getTopLeftCorner(row, col);
1151                if (view != null) {
1152                    return true;
1153                }
1154            }
1155
1156            return false;
1157        }
1158
1159        /**
1160         * Returns a list of views (optionally sorted by increasing row index) that have
1161         * their left edge starting in the given column
1162         *
1163         * @param col the column to look up views for
1164         * @param sort whether to sort the result in increasing row order
1165         * @return a list of views starting in the given column
1166         */
1167        public List<View> viewsStartingInCol(int col, boolean sort) {
1168            List<View> views = new ArrayList<View>();
1169            for (int row = 0; row < mRowCount; row++) {
1170                View view = getTopLeftCorner(row, col);
1171                if (view != null) {
1172                    views.add(view);
1173                }
1174            }
1175
1176            if (sort) {
1177                View.sortByRow(views);
1178            }
1179
1180            return views;
1181        }
1182
1183        /**
1184         * Returns a list of views (optionally sorted by increasing column index) that have
1185         * their top edge starting in the given row
1186         *
1187         * @param row the row to look up views for
1188         * @param sort whether to sort the result in increasing column order
1189         * @return a list of views starting in the given row
1190         */
1191        public List<View> viewsStartingInRow(int row, boolean sort) {
1192            List<View> views = new ArrayList<View>();
1193            for (int col = 0; col < mColCount; col++) {
1194                View view = getTopLeftCorner(row, col);
1195                if (view != null) {
1196                    views.add(view);
1197                }
1198            }
1199
1200            if (sort) {
1201                View.sortByColumn(views);
1202            }
1203
1204            return views;
1205        }
1206
1207        /**
1208         * Returns the pixel width of the given column
1209         *
1210         * @param col the column to look up the width of
1211         * @return the width of the column
1212         */
1213        public int getColumnWidth(int col) {
1214            return mLeft[col + 1] - mLeft[col];
1215        }
1216
1217        /**
1218         * Returns the pixel height of the given row
1219         *
1220         * @param row the row to look up the height of
1221         * @return the height of the row
1222         */
1223        public int getRowHeight(int row) {
1224            return mTop[row + 1] - mTop[row];
1225        }
1226
1227        /**
1228         * Returns the first view found that has its top left corner in the cell given by
1229         * the row and column indexes, or null if not found.
1230         *
1231         * @param row the row of the target cell
1232         * @param col the column of the target cell
1233         * @return a view with its top left corner in the given cell, or null if not found
1234         */
1235        View getTopLeftCorner(int row, int col) {
1236            List<View> views = get(row, col);
1237            if (views.size() > 0) {
1238                for (View view : views) {
1239                    if (view.mRow == row && view.mCol == col) {
1240                        return view;
1241                    }
1242                }
1243            }
1244
1245            return null;
1246        }
1247
1248        public View findRightEdgeView(int col) {
1249            for (int row = 0; row < mRowCount; row++) {
1250                List<View> views = get(row, col);
1251                if (views.size() > 0) {
1252                    List<View> result = new ArrayList<View>();
1253                    for (View view : views) {
1254                        // Ends on the right edge of this column?
1255                        if (view.mCol + view.mColSpan == col + 1) {
1256                            result.add(view);
1257                        }
1258                    }
1259                    if (result.size() > 1) {
1260                        View.sortByColumn(result);
1261                    }
1262                    if (result.size() > 0) {
1263                        return result.get(0);
1264                    }
1265                }
1266            }
1267
1268            return null;
1269        }
1270
1271        public View findBottomEdgeView(int row) {
1272            for (int col = 0; col < mColCount; col++) {
1273                List<View> views = get(row, col);
1274                if (views.size() > 0) {
1275                    List<View> result = new ArrayList<View>();
1276                    for (View view : views) {
1277                        // Ends on the bottom edge of this column?
1278                        if (view.mRow + view.mRowSpan == row + 1) {
1279                            result.add(view);
1280                        }
1281                    }
1282                    if (result.size() > 1) {
1283                        View.sortByRow(result);
1284                    }
1285                    if (result.size() > 0) {
1286                        return result.get(0);
1287                    }
1288
1289                }
1290            }
1291
1292            return null;
1293        }
1294
1295        /**
1296         * Produces a display of view contents along with the pixel positions of each row/column,
1297         * like the following (used for diagnostics only)
1298         * <pre>
1299         *          |0                  |49                 |143                |192           |240
1300         *        36|                   |                   |button2            |
1301         *        72|                   |radioButton1       |button2            |
1302         *        74|button1            |radioButton1       |button2            |
1303         *       108|button1            |                   |button2            |
1304         *       110|                   |                   |button2            |
1305         *       149|                   |                   |                   |
1306         *       320
1307         * </pre>
1308         */
1309        @Override
1310        public String toString() {
1311            // Dump out the view table
1312            int cellWidth = 20;
1313
1314            StringWriter stringWriter = new StringWriter();
1315            PrintWriter out = new PrintWriter(stringWriter);
1316            out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
1317            for (int col = 0; col < mColCount + 1; col++) {
1318                out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$
1319            }
1320            out.printf("\n"); //$NON-NLS-1$
1321            for (int row = 0; row < mRowCount + 1; row++) {
1322                out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$
1323                if (row == mRowCount) {
1324                    break;
1325                }
1326                for (int col = 0; col < mColCount; col++) {
1327                    List<View> views = get(row, col);
1328                    StringBuilder sb = new StringBuilder();
1329                    for (View view : views) {
1330                        String id = view != null ? view.getId() : ""; //$NON-NLS-1$
1331                        if (id.startsWith(NEW_ID_PREFIX)) {
1332                            id = id.substring(NEW_ID_PREFIX.length());
1333                        }
1334                        if (id.length() > cellWidth - 2) {
1335                            id = id.substring(0, cellWidth - 2);
1336                        }
1337                        if (sb.length() > 0) {
1338                            sb.append(","); //$NON-NLS-1$
1339                        }
1340                        sb.append(id);
1341                    }
1342                    String cellString = sb.toString();
1343                    if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$
1344                        cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$
1345                    }
1346                    out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$
1347                }
1348                out.printf("\n"); //$NON-NLS-1$
1349            }
1350
1351            out.flush();
1352            return stringWriter.toString();
1353        }
1354    }
1355
1356    /** Holds layout information about an individual view. */
1357    private static class View {
1358        private final Element mElement;
1359        private int mRow = -1;
1360        private int mCol = -1;
1361        private int mRowSpan = -1;
1362        private int mColSpan = -1;
1363        private CanvasViewInfo mInfo;
1364        private String mId;
1365        private List<Pair<String, String>> mHorizConstraints =
1366            new ArrayList<Pair<String, String>>(4);
1367        private List<Pair<String, String>> mVerticalConstraints =
1368            new ArrayList<Pair<String, String>>(4);
1369        private int mGravity;
1370
1371        public View(CanvasViewInfo view, Element element) {
1372            mInfo = view;
1373            mElement = element;
1374            mGravity = RelativeLayoutConversionHelper.getGravity(element);
1375        }
1376
1377        public int getHeight() {
1378            return mInfo.getAbsRect().height;
1379        }
1380
1381        public int getGravity() {
1382            return mGravity;
1383        }
1384
1385        public String getId() {
1386            return mId;
1387        }
1388
1389        public Element getElement() {
1390            return mElement;
1391        }
1392
1393        public List<Pair<String, String>> getHorizConstraints() {
1394            return mHorizConstraints;
1395        }
1396
1397        public List<Pair<String, String>> getVerticalConstraints() {
1398            return mVerticalConstraints;
1399        }
1400
1401        public boolean isConstrainedHorizontally() {
1402            return mHorizConstraints.size() > 0;
1403        }
1404
1405        public boolean isConstrainedVertically() {
1406            return mVerticalConstraints.size() > 0;
1407        }
1408
1409        public void addHorizConstraint(String property, String value) {
1410            assert property != null && value != null;
1411            // TODO - look for duplicates?
1412            mHorizConstraints.add(Pair.of(property, value));
1413        }
1414
1415        public void addVerticalConstraint(String property, String value) {
1416            assert property != null && value != null;
1417            mVerticalConstraints.add(Pair.of(property, value));
1418        }
1419
1420        public int getLeftEdge() {
1421            return mInfo.getAbsRect().x;
1422        }
1423
1424        public int getTopEdge() {
1425            return mInfo.getAbsRect().y;
1426        }
1427
1428        public int getRightEdge() {
1429            Rectangle bounds = mInfo.getAbsRect();
1430            // +1: make the bounds overlap, so the right edge is the same as the
1431            // left edge of the neighbor etc. Otherwise we end up with lots of 1-pixel wide
1432            // columns between adjacent items.
1433            return bounds.x + bounds.width + 1;
1434        }
1435
1436        public int getBottomEdge() {
1437            Rectangle bounds = mInfo.getAbsRect();
1438            return bounds.y + bounds.height + 1;
1439        }
1440
1441        @Override
1442        public String toString() {
1443            return "View [mId=" + mId + "]"; //$NON-NLS-1$ //$NON-NLS-2$
1444        }
1445
1446        public static void sortByRow(List<View> views) {
1447            Collections.sort(views, new ViewComparator(true/*rowSort*/));
1448        }
1449
1450        public static void sortByColumn(List<View> views) {
1451            Collections.sort(views, new ViewComparator(false/*rowSort*/));
1452        }
1453
1454        /** Comparator to help sort views by row or column index */
1455        private static class ViewComparator implements Comparator<View> {
1456            boolean mRowSort;
1457
1458            public ViewComparator(boolean rowSort) {
1459                mRowSort = rowSort;
1460            }
1461
1462            public int compare(View view1, View view2) {
1463                if (mRowSort) {
1464                    return view1.mRow - view2.mRow;
1465                } else {
1466                    return view1.mCol - view2.mCol;
1467                }
1468            }
1469        }
1470    }
1471
1472    /**
1473     * An edge list takes a hierarchy of elements and records the bounds of each element
1474     * into various lists such that it can answer queries about shared edges, about which
1475     * particular pixels occur as a boundary edge, etc.
1476     */
1477    private class EdgeList {
1478        private final Map<Element, View> mElementToViewMap = new HashMap<Element, View>(100);
1479        private final Map<String, View> mIdToViewMap = new HashMap<String, View>(100);
1480        private final Map<Integer, List<View>> mLeft = new HashMap<Integer, List<View>>();
1481        private final Map<Integer, List<View>> mTop = new HashMap<Integer, List<View>>();
1482        private final Map<Integer, List<View>> mRight = new HashMap<Integer, List<View>>();
1483        private final Map<Integer, List<View>> mBottom = new HashMap<Integer, List<View>>();
1484        private final Map<Element, Element> mSharedLeftEdge = new HashMap<Element, Element>();
1485        private final Map<Element, Element> mSharedTopEdge = new HashMap<Element, Element>();
1486        private final Map<Element, Element> mSharedRightEdge = new HashMap<Element, Element>();
1487        private final Map<Element, Element> mSharedBottomEdge = new HashMap<Element, Element>();
1488        private final List<Element> mDelete = new ArrayList<Element>();
1489
1490        EdgeList(CanvasViewInfo view) {
1491            analyze(view, true);
1492            mDelete.remove(getElement(view));
1493        }
1494
1495        public void setIdAttributeValue(View view, String id) {
1496            assert id.startsWith(NEW_ID_PREFIX) || id.startsWith(ID_PREFIX);
1497            view.mId = id;
1498            mIdToViewMap.put(getIdBasename(id), view);
1499        }
1500
1501        public View getView(Element element) {
1502            return mElementToViewMap.get(element);
1503        }
1504
1505        public View getView(String id) {
1506            return mIdToViewMap.get(id);
1507        }
1508
1509        public List<View> getTopEdgeViews(Integer topOffset) {
1510            return mTop.get(topOffset);
1511        }
1512
1513        public List<View> getLeftEdgeViews(Integer leftOffset) {
1514            return mLeft.get(leftOffset);
1515        }
1516
1517        void record(Map<Integer, List<View>> map, Integer edge, View info) {
1518            List<View> list = map.get(edge);
1519            if (list == null) {
1520                list = new ArrayList<View>();
1521                map.put(edge, list);
1522            }
1523            list.add(info);
1524        }
1525
1526        private List<Integer> getOffsets(Set<Integer> first, Set<Integer> second) {
1527            Set<Integer> joined = new HashSet<Integer>(first.size() + second.size());
1528            joined.addAll(first);
1529            joined.addAll(second);
1530            List<Integer> unique = new ArrayList<Integer>(joined);
1531            Collections.sort(unique);
1532
1533            return unique;
1534        }
1535
1536        public List<Element> getDeletedElements() {
1537            return mDelete;
1538        }
1539
1540        public List<Integer> getColumnOffsets() {
1541            return getOffsets(mLeft.keySet(), mRight.keySet());
1542        }
1543        public List<Integer> getRowOffsets() {
1544            return getOffsets(mTop.keySet(), mBottom.keySet());
1545        }
1546
1547        private View analyze(CanvasViewInfo view, boolean isRoot) {
1548            View added = null;
1549            if (!mFlatten || !isRemovableLayout(view)) {
1550                added = add(view);
1551                if (!isRoot) {
1552                    return added;
1553                }
1554            } else {
1555                mDelete.add(getElement(view));
1556            }
1557
1558            Element parentElement = getElement(view);
1559            Rectangle parentBounds = view.getAbsRect();
1560
1561            // Build up a table model of the view
1562            for (CanvasViewInfo child : view.getChildren()) {
1563                Rectangle childBounds = child.getAbsRect();
1564                Element childElement = getElement(child);
1565
1566                // See if this view shares the edge with the removed
1567                // parent layout, and if so, record that such that we can
1568                // later handle attachments to the removed parent edges
1569                if (parentBounds.x == childBounds.x) {
1570                    mSharedLeftEdge.put(childElement, parentElement);
1571                }
1572                if (parentBounds.y == childBounds.y) {
1573                    mSharedTopEdge.put(childElement, parentElement);
1574                }
1575                if (parentBounds.x + parentBounds.width == childBounds.x + childBounds.width) {
1576                    mSharedRightEdge.put(childElement, parentElement);
1577                }
1578                if (parentBounds.y + parentBounds.height == childBounds.y + childBounds.height) {
1579                    mSharedBottomEdge.put(childElement, parentElement);
1580                }
1581
1582                if (mFlatten && isRemovableLayout(child)) {
1583                    // When flattening, we want to disregard all layouts and instead
1584                    // add their children!
1585                    for (CanvasViewInfo childView : child.getChildren()) {
1586                        analyze(childView, false);
1587
1588                        Element childViewElement = getElement(childView);
1589                        Rectangle childViewBounds = childView.getAbsRect();
1590
1591                        // See if this view shares the edge with the removed
1592                        // parent layout, and if so, record that such that we can
1593                        // later handle attachments to the removed parent edges
1594                        if (parentBounds.x == childViewBounds.x) {
1595                            mSharedLeftEdge.put(childViewElement, parentElement);
1596                        }
1597                        if (parentBounds.y == childViewBounds.y) {
1598                            mSharedTopEdge.put(childViewElement, parentElement);
1599                        }
1600                        if (parentBounds.x + parentBounds.width == childViewBounds.x
1601                                + childViewBounds.width) {
1602                            mSharedRightEdge.put(childViewElement, parentElement);
1603                        }
1604                        if (parentBounds.y + parentBounds.height == childViewBounds.y
1605                                + childViewBounds.height) {
1606                            mSharedBottomEdge.put(childViewElement, parentElement);
1607                        }
1608                    }
1609                    mDelete.add(childElement);
1610                } else {
1611                    analyze(child, false);
1612                }
1613            }
1614
1615            return added;
1616        }
1617
1618        public View getSharedLeftEdge(Element element) {
1619            return getSharedEdge(element, mSharedLeftEdge);
1620        }
1621
1622        public View getSharedRightEdge(Element element) {
1623            return getSharedEdge(element, mSharedRightEdge);
1624        }
1625
1626        public View getSharedTopEdge(Element element) {
1627            return getSharedEdge(element, mSharedTopEdge);
1628        }
1629
1630        public View getSharedBottomEdge(Element element) {
1631            return getSharedEdge(element, mSharedBottomEdge);
1632        }
1633
1634        private View getSharedEdge(Element element, Map<Element, Element> sharedEdgeMap) {
1635            Element original = element;
1636
1637            while (element != null) {
1638                View view = getView(element);
1639                if (view != null) {
1640                    assert isAncestor(element, original);
1641                    return view;
1642                }
1643                element = sharedEdgeMap.get(element);
1644            }
1645
1646            return null;
1647        }
1648
1649        private View add(CanvasViewInfo info) {
1650            Rectangle bounds = info.getAbsRect();
1651            Element element = getElement(info);
1652            View view = new View(info, element);
1653            mElementToViewMap.put(element, view);
1654            record(mLeft, Integer.valueOf(bounds.x), view);
1655            record(mTop, Integer.valueOf(bounds.y), view);
1656            record(mRight, Integer.valueOf(view.getRightEdge()), view);
1657            record(mBottom, Integer.valueOf(view.getBottomEdge()), view);
1658            return view;
1659        }
1660
1661        /**
1662         * Returns true if the given {@link CanvasViewInfo} represents an element we
1663         * should remove in a flattening conversion. We don't want to remove non-layout
1664         * views, or layout views that for example contain drawables on their own.
1665         */
1666        private boolean isRemovableLayout(CanvasViewInfo child) {
1667            // The element being converted is NOT removable!
1668            Element element = getElement(child);
1669            if (element == mLayout) {
1670                return false;
1671            }
1672
1673            ElementDescriptor descriptor = child.getUiViewNode().getDescriptor();
1674            String name = descriptor.getXmlLocalName();
1675            if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT)) {
1676                // Don't delete layouts that provide a background image or gradient
1677                if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
1678                    AdtPlugin.log(IStatus.WARNING,
1679                            "Did not flatten layout %1$s because it defines a '%2$s' attribute",
1680                            VisualRefactoring.getId(element), ATTR_BACKGROUND);
1681                    return false;
1682                }
1683
1684                return true;
1685            }
1686
1687            return false;
1688        }
1689    }
1690}
1691