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