Home | History | Annotate | Download | only in refactoring
      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 
    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 */
    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 */
    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      */
    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} */
    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 */
    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 */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 */
    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      */
    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      */
    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 */
    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      */
    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      */
    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      */
    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} */
    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 
   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          */
   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          */
   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          */
   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          */
   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          */
   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          */
   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          */
   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          */
   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          */
   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          */
   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 
   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 
   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
   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 
   1313         public View(CanvasViewInfo view, Element element) {
   1314             mInfo = view;
   1315             mElement = element;
   1316             mGravity = GravityHelper.getGravity(element);
   1317         }
   1318 
   1319         public int getHeight() {
   1320             return mInfo.getAbsRect().height;
   1321         }
   1322 
   1323         public int getGravity() {
   1324             return mGravity;
   1325         }
   1326 
   1327         public String getId() {
   1328             return mId;
   1329         }
   1330 
   1331         public Element getElement() {
   1332             return mElement;
   1333         }
   1334 
   1335         public List<Pair<String, String>> getHorizConstraints() {
   1336             return mHorizConstraints;
   1337         }
   1338 
   1339         public List<Pair<String, String>> getVerticalConstraints() {
   1340             return mVerticalConstraints;
   1341         }
   1342 
   1343         public boolean isConstrainedHorizontally() {
   1344             return mHorizConstraints.size() > 0;
   1345         }
   1346 
   1347         public boolean isConstrainedVertically() {
   1348             return mVerticalConstraints.size() > 0;
   1349         }
   1350 
   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 
   1357         public void addVerticalConstraint(String property, String value) {
   1358             assert property != null && value != null;
   1359             mVerticalConstraints.add(Pair.of(property, value));
   1360         }
   1361 
   1362         public int getLeftEdge() {
   1363             return mInfo.getAbsRect().x;
   1364         }
   1365 
   1366         public int getTopEdge() {
   1367             return mInfo.getAbsRect().y;
   1368         }
   1369 
   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 
   1378         public int getBottomEdge() {
   1379             Rectangle bounds = mInfo.getAbsRect();
   1380             return bounds.y + bounds.height + 1;
   1381         }
   1382 
   1383         @Override
   1384         public String toString() {
   1385             return "View [mId=" + mId + "]"; //$NON-NLS-1$ //$NON-NLS-2$
   1386         }
   1387 
   1388         public static void sortByRow(List<View> views) {
   1389             Collections.sort(views, new ViewComparator(true/*rowSort*/));
   1390         }
   1391 
   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 
   1400             public ViewComparator(boolean rowSort) {
   1401                 mRowSort = rowSort;
   1402             }
   1403 
   1404             @Override
   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 
   1433         EdgeList(CanvasViewInfo view) {
   1434             analyze(view, true);
   1435             mDelete.remove(getElement(view));
   1436         }
   1437 
   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 
   1444         public View getView(Element element) {
   1445             return mElementToViewMap.get(element);
   1446         }
   1447 
   1448         public View getView(String id) {
   1449             return mIdToViewMap.get(id);
   1450         }
   1451 
   1452         public List<View> getTopEdgeViews(Integer topOffset) {
   1453             return mTop.get(topOffset);
   1454         }
   1455 
   1456         public List<View> getLeftEdgeViews(Integer leftOffset) {
   1457             return mLeft.get(leftOffset);
   1458         }
   1459 
   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 
   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 
   1479         public List<Element> getDeletedElements() {
   1480             return mDelete;
   1481         }
   1482 
   1483         public List<Integer> getColumnOffsets() {
   1484             return getOffsets(mLeft.keySet(), mRight.keySet());
   1485         }
   1486         public List<Integer> getRowOffsets() {
   1487             return getOffsets(mTop.keySet(), mBottom.keySet());
   1488         }
   1489 
   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 
   1561         public View getSharedLeftEdge(Element element) {
   1562             return getSharedEdge(element, mSharedLeftEdge);
   1563         }
   1564 
   1565         public View getSharedRightEdge(Element element) {
   1566             return getSharedEdge(element, mSharedRightEdge);
   1567         }
   1568 
   1569         public View getSharedTopEdge(Element element) {
   1570             return getSharedEdge(element, mSharedTopEdge);
   1571         }
   1572 
   1573         public View getSharedBottomEdge(Element element) {
   1574             return getSharedEdge(element, mSharedBottomEdge);
   1575         }
   1576 
   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 
   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          */
   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