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