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_COLUMN_COUNT;
     21 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
     22 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
     23 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
     24 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
     25 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
     26 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
     27 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
     28 import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
     29 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
     30 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
     31 import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
     32 import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
     33 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
     34 import static com.android.SdkConstants.ATTR_ORIENTATION;
     35 import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
     36 import static com.android.SdkConstants.FQCN_SPACE;
     37 import static com.android.SdkConstants.GRAVITY_VALUE_FILL;
     38 import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
     39 import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL;
     40 import static com.android.SdkConstants.ID_PREFIX;
     41 import static com.android.SdkConstants.LINEAR_LAYOUT;
     42 import static com.android.SdkConstants.NEW_ID_PREFIX;
     43 import static com.android.SdkConstants.RADIO_GROUP;
     44 import static com.android.SdkConstants.RELATIVE_LAYOUT;
     45 import static com.android.SdkConstants.SPACE;
     46 import static com.android.SdkConstants.TABLE_LAYOUT;
     47 import static com.android.SdkConstants.TABLE_ROW;
     48 import static com.android.SdkConstants.VALUE_FILL_PARENT;
     49 import static com.android.SdkConstants.VALUE_HORIZONTAL;
     50 import static com.android.SdkConstants.VALUE_MATCH_PARENT;
     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_HORIZ_MASK;
     54 import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK;
     55 
     56 import com.android.ide.common.api.IViewMetadata.FillPreference;
     57 import com.android.ide.common.layout.BaseLayoutRule;
     58 import com.android.ide.common.layout.GravityHelper;
     59 import com.android.ide.common.layout.GridLayoutRule;
     60 import com.android.ide.eclipse.adt.AdtPlugin;
     61 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     62 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     63 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
     64 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
     65 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     66 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
     67 import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper;
     68 
     69 import org.eclipse.core.resources.IFile;
     70 import org.eclipse.core.runtime.IStatus;
     71 import org.eclipse.swt.graphics.Rectangle;
     72 import org.eclipse.text.edits.InsertEdit;
     73 import org.eclipse.text.edits.MalformedTreeException;
     74 import org.eclipse.text.edits.MultiTextEdit;
     75 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     76 import org.w3c.dom.Attr;
     77 import org.w3c.dom.Element;
     78 import org.w3c.dom.NamedNodeMap;
     79 import org.w3c.dom.Node;
     80 
     81 import java.util.ArrayList;
     82 import java.util.Collection;
     83 import java.util.Collections;
     84 import java.util.HashMap;
     85 import java.util.HashSet;
     86 import java.util.Iterator;
     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 grid layout
     93  * <p>
     94  * Future enhancements:
     95  * <ul>
     96  * <li>Render the layout at multiple screen sizes and analyze how the widget bounds
     97  *  change and use this to infer gravity
     98  *  <li> Use the layout_width and layout_height attributes on views to infer column and
     99  *  row flexibility (and as mentioned above, possibly layout_weight).
    100  * move and stretch and use that to add in additional constraints
    101  *  <li> Take into account existing margins and add/subtract those from the
    102  *  bounds computations and either clear or update them.
    103  * <li>Try to reorder elements into their natural order
    104  * <li> Try to preserve spacing? Right now everything gets converted into a compact
    105  *   grid with no spacing between the views; consider inserting {@code <Space>} views
    106  *   with dimensions based on existing distances.
    107  * </ul>
    108  */
    109 @SuppressWarnings("restriction") // DOM model access
    110 class GridLayoutConverter {
    111     private final MultiTextEdit mRootEdit;
    112     private final boolean mFlatten;
    113     private final Element mLayout;
    114     private final ChangeLayoutRefactoring mRefactoring;
    115     private final CanvasViewInfo mRootView;
    116 
    117     private List<View> mViews;
    118     private String mNamespace;
    119     private int mColumnCount;
    120 
    121     /** Creates a new {@link GridLayoutConverter} */
    122     GridLayoutConverter(ChangeLayoutRefactoring refactoring,
    123             Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) {
    124         mRefactoring = refactoring;
    125         mLayout = layout;
    126         mFlatten = flatten;
    127         mRootEdit = rootEdit;
    128         mRootView = rootView;
    129     }
    130 
    131     /** Performs conversion from any layout to a RelativeLayout */
    132     public void convertToGridLayout() {
    133         if (mRootView == null) {
    134             return;
    135         }
    136 
    137         // Locate the view for the layout
    138         CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout);
    139         if (layoutView == null || layoutView.getChildren().size() == 0) {
    140             // No children. THAT was an easy conversion!
    141             return;
    142         }
    143 
    144         // Study the layout and get information about how to place individual elements
    145         GridModel gridModel = new GridModel(layoutView, mLayout, mFlatten);
    146         mViews = gridModel.getViews();
    147         mColumnCount = gridModel.computeColumnCount();
    148 
    149         deleteRemovedElements(gridModel.getDeletedElements());
    150         mNamespace = mRefactoring.getAndroidNamespacePrefix();
    151 
    152         processGravities();
    153 
    154         // Insert space views if necessary
    155         insertStretchableSpans();
    156 
    157         // Create/update relative layout constraints
    158         assignGridAttributes();
    159 
    160         removeUndefinedAttrs();
    161 
    162         if (mColumnCount > 0) {
    163             mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI,
    164                 mNamespace, ATTR_COLUMN_COUNT, Integer.toString(mColumnCount));
    165         }
    166     }
    167 
    168     private void insertStretchableSpans() {
    169         // Look at the rows and columns and determine if we need to have a stretchable
    170         // row and/or a stretchable column in the layout.
    171         // In a GridLayout, a row or column is stretchable if it defines a gravity (regardless
    172         // of what the gravity is -- in other words, a column is not just stretchable if it
    173         // has gravity=fill but also if it has gravity=left). Furthermore, ALL the elements
    174         // in the row/column have to be stretchable for the overall row/column to be
    175         // considered stretchable.
    176 
    177         // Map from row index to boolean for "is the row fixed/inflexible?"
    178         Map<Integer, Boolean> rowFixed = new HashMap<Integer, Boolean>();
    179         Map<Integer, Boolean> columnFixed = new HashMap<Integer, Boolean>();
    180         for (View view : mViews) {
    181             if (view.mElement == mLayout) {
    182                 continue;
    183             }
    184 
    185             int gravity = GravityHelper.getGravity(view.mGravity, 0);
    186             if ((gravity & GRAVITY_HORIZ_MASK) == 0) {
    187                 columnFixed.put(view.mCol, true);
    188             } else if (!columnFixed.containsKey(view.mCol)) {
    189                 columnFixed.put(view.mCol, false);
    190             }
    191             if ((gravity & GRAVITY_VERT_MASK) == 0) {
    192                 rowFixed.put(view.mRow, true);
    193             } else if (!rowFixed.containsKey(view.mRow)) {
    194                 rowFixed.put(view.mRow, false);
    195             }
    196         }
    197 
    198         boolean hasStretchableRow = false;
    199         boolean hasStretchableColumn = false;
    200         for (boolean fixed : rowFixed.values()) {
    201             if (!fixed) {
    202                 hasStretchableRow = true;
    203             }
    204         }
    205         for (boolean fixed : columnFixed.values()) {
    206             if (!fixed) {
    207                 hasStretchableColumn = true;
    208             }
    209         }
    210 
    211         if (!hasStretchableRow || !hasStretchableColumn) {
    212             // Insert <Space> to hold stretchable space
    213             // TODO: May also have to increment column count!
    214             int offset = 0; // WHERE?
    215 
    216             String gridLayout = mLayout.getTagName();
    217             if (mLayout instanceof IndexedRegion) {
    218                 IndexedRegion region = (IndexedRegion) mLayout;
    219                 int end = region.getEndOffset();
    220                 // TODO: Look backwards for the "</"
    221                 // (and can it ever be <foo/>) ?
    222                 end -= (gridLayout.length() + 3); // 3: <, /, >
    223                 offset = end;
    224             }
    225 
    226             int row = rowFixed.size();
    227             int column = columnFixed.size();
    228             StringBuilder sb = new StringBuilder(64);
    229             String spaceTag = SPACE;
    230             IFile file = mRefactoring.getFile();
    231             if (file != null) {
    232                 spaceTag = SupportLibraryHelper.getTagFor(file.getProject(), FQCN_SPACE);
    233                 if (spaceTag.equals(FQCN_SPACE)) {
    234                     spaceTag = SPACE;
    235                 }
    236             }
    237 
    238             sb.append('<').append(spaceTag).append(' ');
    239             String gravity;
    240             if (!hasStretchableRow && !hasStretchableColumn) {
    241                 gravity = GRAVITY_VALUE_FILL;
    242             } else if (!hasStretchableRow) {
    243                 gravity = GRAVITY_VALUE_FILL_VERTICAL;
    244             } else {
    245                 assert !hasStretchableColumn;
    246                 gravity = GRAVITY_VALUE_FILL_HORIZONTAL;
    247             }
    248 
    249             sb.append(mNamespace).append(':');
    250             sb.append(ATTR_LAYOUT_GRAVITY).append('=').append('"').append(gravity);
    251             sb.append('"').append(' ');
    252 
    253             sb.append(mNamespace).append(':');
    254             sb.append(ATTR_LAYOUT_ROW).append('=').append('"').append(Integer.toString(row));
    255             sb.append('"').append(' ');
    256 
    257             sb.append(mNamespace).append(':');
    258             sb.append(ATTR_LAYOUT_COLUMN).append('=').append('"').append(Integer.toString(column));
    259             sb.append('"').append('/').append('>');
    260 
    261             String space = sb.toString();
    262             InsertEdit replace = new InsertEdit(offset, space);
    263             mRootEdit.addChild(replace);
    264 
    265             mColumnCount++;
    266         }
    267     }
    268 
    269     private void removeUndefinedAttrs() {
    270         ViewElementDescriptor descriptor = mRefactoring.getElementDescriptor(FQCN_GRID_LAYOUT);
    271         if (descriptor == null) {
    272             return;
    273         }
    274 
    275         Set<String> defined = new HashSet<String>();
    276         AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes();
    277         for (AttributeDescriptor attribute : layoutAttributes) {
    278             defined.add(attribute.getXmlLocalName());
    279         }
    280 
    281         for (View view : mViews) {
    282             Element child = view.mElement;
    283 
    284             List<Attr> attributes = mRefactoring.findLayoutAttributes(child);
    285             for (Attr attribute : attributes) {
    286                 String name = attribute.getLocalName();
    287                 if (!defined.contains(name)) {
    288                     // Remove it
    289                     try {
    290                         mRefactoring.removeAttribute(mRootEdit, child, attribute.getNamespaceURI(),
    291                                 name);
    292                     } catch (MalformedTreeException mte) {
    293                         // Sometimes refactoring has modified attribute; not
    294                         // removing
    295                         // it is non-fatal so just warn instead of letting
    296                         // refactoring
    297                         // operation abort
    298                         AdtPlugin.log(IStatus.WARNING,
    299                                 "Could not remove unsupported attribute %1$s; " + //$NON-NLS-1$
    300                                         "already modified during refactoring?", //$NON-NLS-1$
    301                                 attribute.getLocalName());
    302                     }
    303                 }
    304             }
    305         }
    306     }
    307 
    308     /** Removes any elements targeted for deletion */
    309     private void deleteRemovedElements(List<Element> delete) {
    310         if (mFlatten && delete.size() > 0) {
    311             for (Element element : delete) {
    312                 mRefactoring.removeElementTags(mRootEdit, element, delete,
    313                         false /*changeIndentation*/);
    314             }
    315         }
    316     }
    317 
    318     /**
    319      * Creates refactoring edits which adds or updates the grid attributes
    320      */
    321     private void assignGridAttributes() {
    322         // We always convert to horizontal grid layouts for now
    323         mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI,
    324                 mNamespace, ATTR_ORIENTATION, VALUE_HORIZONTAL);
    325 
    326         assignCellAttributes();
    327     }
    328 
    329     /**
    330      * Assign cell attributes to the table, skipping those that will be implied
    331      * by the grid model
    332      */
    333     private void assignCellAttributes() {
    334         int implicitRow = 0;
    335         int implicitColumn = 0;
    336         int nextRow = 0;
    337         for (View view : mViews) {
    338             Element element = view.getElement();
    339             if (element == mLayout) {
    340                 continue;
    341             }
    342 
    343             int row = view.getRow();
    344             int column = view.getColumn();
    345 
    346             if (column != implicitColumn && (implicitColumn > 0 || implicitRow > 0)) {
    347                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
    348                         mNamespace, ATTR_LAYOUT_COLUMN, Integer.toString(column));
    349                 if (column < implicitColumn) {
    350                     implicitRow++;
    351                 }
    352                 implicitColumn = column;
    353             }
    354             if (row != implicitRow) {
    355                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
    356                         mNamespace, ATTR_LAYOUT_ROW, Integer.toString(row));
    357                 implicitRow = row;
    358             }
    359 
    360             int rowSpan = view.getRowSpan();
    361             int columnSpan = view.getColumnSpan();
    362             assert columnSpan >= 1;
    363 
    364             if (rowSpan > 1) {
    365                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
    366                         mNamespace, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan));
    367             }
    368             if (columnSpan > 1) {
    369                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
    370                         mNamespace, ATTR_LAYOUT_COLUMN_SPAN,
    371                         Integer.toString(columnSpan));
    372             }
    373             nextRow = Math.max(nextRow, row + rowSpan);
    374 
    375             // wrap_content is redundant in GridLayouts
    376             Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
    377             if (width != null && VALUE_WRAP_CONTENT.equals(width.getValue())) {
    378                 mRefactoring.removeAttribute(mRootEdit, width);
    379             }
    380             Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
    381             if (height != null && VALUE_WRAP_CONTENT.equals(height.getValue())) {
    382                 mRefactoring.removeAttribute(mRootEdit, height);
    383             }
    384 
    385             // Fix up children moved from LinearLayouts that have "invalid" sizes that
    386             // was intended for layout weight handling in their old parent
    387             if (LINEAR_LAYOUT.equals(element.getParentNode().getNodeName())) {
    388                 convert0dipToWrapContent(element);
    389             }
    390 
    391             implicitColumn += columnSpan;
    392             if (implicitColumn >= mColumnCount) {
    393                 implicitColumn = 0;
    394                 assert nextRow > implicitRow;
    395                 implicitRow = nextRow;
    396             }
    397         }
    398     }
    399 
    400     private void processGravities() {
    401         for (View view : mViews) {
    402             Element element = view.getElement();
    403             if (element == mLayout) {
    404                 continue;
    405             }
    406 
    407             Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
    408             Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
    409             String gravity = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY);
    410             String newGravity = null;
    411             if (width != null && (VALUE_MATCH_PARENT.equals(width.getValue()) ||
    412                     VALUE_FILL_PARENT.equals(width.getValue()))) {
    413                 mRefactoring.removeAttribute(mRootEdit, width);
    414                 newGravity = gravity = GRAVITY_VALUE_FILL_HORIZONTAL;
    415             }
    416             if (height != null && (VALUE_MATCH_PARENT.equals(height.getValue()) ||
    417                     VALUE_FILL_PARENT.equals(height.getValue()))) {
    418                 mRefactoring.removeAttribute(mRootEdit, height);
    419                 if (newGravity == GRAVITY_VALUE_FILL_HORIZONTAL) {
    420                     newGravity = GRAVITY_VALUE_FILL;
    421                 } else {
    422                     newGravity = GRAVITY_VALUE_FILL_VERTICAL;
    423                 }
    424                 gravity = newGravity;
    425             }
    426 
    427             if (gravity == null || gravity.length() == 0) {
    428                 ElementDescriptor descriptor = view.mInfo.getUiViewNode().getDescriptor();
    429                 if (descriptor instanceof ViewElementDescriptor) {
    430                     ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) descriptor;
    431                     String fqcn = viewDescriptor.getFullClassName();
    432                     FillPreference fill = ViewMetadataRepository.get().getFillPreference(fqcn);
    433                     gravity = GridLayoutRule.computeDefaultGravity(fill);
    434                     if (gravity != null) {
    435                         newGravity = gravity;
    436                     }
    437                 }
    438             }
    439 
    440             if (newGravity != null) {
    441                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
    442                         mNamespace, ATTR_LAYOUT_GRAVITY, newGravity);
    443             }
    444 
    445             view.mGravity = newGravity != null ? newGravity : gravity;
    446         }
    447     }
    448 
    449 
    450     /** Converts 0dip values in layout_width and layout_height to wrap_content instead */
    451     private void convert0dipToWrapContent(Element child) {
    452         // Must convert layout_height="0dip" to layout_height="wrap_content".
    453         // (And since wrap_content is the default, what we really do is remove
    454         // the attribute completely.)
    455         // 0dip is a special trick used in linear layouts in the presence of
    456         // weights where 0dip ensures that the height of the view is not taken
    457         // into account when distributing the weights. However, when converted
    458         // to RelativeLayout this will instead cause the view to actually be assigned
    459         // 0 height.
    460         Attr height = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
    461         // 0dip, 0dp, 0px, etc
    462         if (height != null && height.getValue().startsWith("0")) { //$NON-NLS-1$
    463             mRefactoring.removeAttribute(mRootEdit, height);
    464         }
    465         Attr width = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
    466         if (width != null && width.getValue().startsWith("0")) { //$NON-NLS-1$
    467             mRefactoring.removeAttribute(mRootEdit, width);
    468         }
    469     }
    470 
    471     /**
    472      * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given
    473      * {@link Element}
    474      *
    475      * @param info the root {@link CanvasViewInfo} to search below
    476      * @param element the target element
    477      * @return the {@link CanvasViewInfo} which corresponds to the given element
    478      */
    479     private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) {
    480         if (getElement(info) == element) {
    481             return info;
    482         }
    483 
    484         for (CanvasViewInfo child : info.getChildren()) {
    485             CanvasViewInfo result = findViewForElement(child, element);
    486             if (result != null) {
    487                 return result;
    488             }
    489         }
    490 
    491         return null;
    492     }
    493 
    494     /** Returns the {@link Element} for the given {@link CanvasViewInfo} */
    495     private static Element getElement(CanvasViewInfo info) {
    496         Node node = info.getUiViewNode().getXmlNode();
    497         if (node instanceof Element) {
    498             return (Element) node;
    499         }
    500 
    501         return null;
    502     }
    503 
    504 
    505     /** Holds layout information about an individual view */
    506     private static class View {
    507         private final Element mElement;
    508         private int mRow = -1;
    509         private int mCol = -1;
    510         private int mRowSpan = -1;
    511         private int mColSpan = -1;
    512         private int mX1;
    513         private int mY1;
    514         private int mX2;
    515         private int mY2;
    516         private CanvasViewInfo mInfo;
    517         private String mGravity;
    518 
    519         public View(CanvasViewInfo view, Element element) {
    520             mInfo = view;
    521             mElement = element;
    522 
    523             Rectangle b = mInfo.getAbsRect();
    524             mX1 = b.x;
    525             mX2 = b.x + b.width;
    526             mY1 = b.y;
    527             mY2 = b.y + b.height;
    528         }
    529 
    530         /**
    531          * Returns the element for this view
    532          *
    533          * @return the element for the view
    534          */
    535         public Element getElement() {
    536             return mElement;
    537         }
    538 
    539         /**
    540          * The assigned row for this view
    541          *
    542          * @return the assigned row
    543          */
    544         public int getRow() {
    545             return mRow;
    546         }
    547 
    548         /**
    549          * The assigned column for this view
    550          *
    551          * @return the assigned column
    552          */
    553         public int getColumn() {
    554             return mCol;
    555         }
    556 
    557         /**
    558          * The assigned row span for this view
    559          *
    560          * @return the assigned row span
    561          */
    562         public int getRowSpan() {
    563             return mRowSpan;
    564         }
    565 
    566         /**
    567          * The assigned column span for this view
    568          *
    569          * @return the assigned column span
    570          */
    571         public int getColumnSpan() {
    572             return mColSpan;
    573         }
    574 
    575         /**
    576          * The left edge of the view to be used for placement
    577          *
    578          * @return the left edge x coordinate
    579          */
    580         public int getLeftEdge() {
    581             return mX1;
    582         }
    583 
    584         /**
    585          * The top edge of the view to be used for placement
    586          *
    587          * @return the top edge y coordinate
    588          */
    589         public int getTopEdge() {
    590             return mY1;
    591         }
    592 
    593         /**
    594          * The right edge of the view to be used for placement
    595          *
    596          * @return the right edge x coordinate
    597          */
    598         public int getRightEdge() {
    599             return mX2;
    600         }
    601 
    602         /**
    603          * The bottom edge of the view to be used for placement
    604          *
    605          * @return the bottom edge y coordinate
    606          */
    607         public int getBottomEdge() {
    608             return mY2;
    609         }
    610 
    611         @Override
    612         public String toString() {
    613             return "View(" + VisualRefactoring.getId(mElement) + ": " + mX1 + "," + mY1 + ")";
    614         }
    615     }
    616 
    617     /** Grid model for the views found in the view hierarchy, partitioned into rows and columns */
    618     private static class GridModel {
    619         private final List<View> mViews = new ArrayList<View>();
    620         private final List<Element> mDelete = new ArrayList<Element>();
    621         private final Map<Element, View> mElementToView = new HashMap<Element, View>();
    622         private Element mLayout;
    623         private boolean mFlatten;
    624 
    625         GridModel(CanvasViewInfo view, Element layout, boolean flatten) {
    626             mLayout = layout;
    627             mFlatten = flatten;
    628 
    629             scan(view, true);
    630             analyzeKnownLayouts();
    631             initializeColumns();
    632             initializeRows();
    633             mDelete.remove(getElement(view));
    634         }
    635 
    636         /**
    637          * Returns the {@link View} objects to be placed in the grid
    638          *
    639          * @return list of {@link View} objects, never null but possibly empty
    640          */
    641         public List<View> getViews() {
    642             return mViews;
    643         }
    644 
    645         /**
    646          * Returns the list of elements that are scheduled for deletion in the
    647          * flattening operation
    648          *
    649          * @return elements to be deleted, never null but possibly empty
    650          */
    651         public List<Element> getDeletedElements() {
    652             return mDelete;
    653         }
    654 
    655         /**
    656          * Compute and return column count
    657          *
    658          * @return the column count
    659          */
    660         public int computeColumnCount() {
    661             int columnCount = 0;
    662             for (View view : mViews) {
    663                 if (view.getElement() == mLayout) {
    664                     continue;
    665                 }
    666 
    667                 int column = view.getColumn();
    668                 int columnSpan = view.getColumnSpan();
    669                 if (column + columnSpan > columnCount) {
    670                     columnCount = column + columnSpan;
    671                 }
    672             }
    673             return columnCount;
    674         }
    675 
    676         /**
    677          * Initializes the column and columnSpan attributes of the views
    678          */
    679         private void initializeColumns() {
    680             // Now initialize table view row, column and spans
    681             Map<Integer, List<View>> mColumnViews = new HashMap<Integer, List<View>>();
    682             for (View view : mViews) {
    683                 if (view.mElement == mLayout) {
    684                     continue;
    685                 }
    686                 int x = view.getLeftEdge();
    687                 List<View> list = mColumnViews.get(x);
    688                 if (list == null) {
    689                     list = new ArrayList<View>();
    690                     mColumnViews.put(x, list);
    691                 }
    692                 list.add(view);
    693             }
    694 
    695             List<Integer> columnOffsets = new ArrayList<Integer>(mColumnViews.keySet());
    696             Collections.sort(columnOffsets);
    697 
    698             int columnIndex = 0;
    699             for (Integer column : columnOffsets) {
    700                 List<View> views = mColumnViews.get(column);
    701                 if (views != null) {
    702                     for (View view : views) {
    703                         view.mCol = columnIndex;
    704                     }
    705                 }
    706                 columnIndex++;
    707             }
    708             // Initialize column spans
    709             for (View view : mViews) {
    710                 if (view.mElement == mLayout) {
    711                     continue;
    712                 }
    713                 int index = Collections.binarySearch(columnOffsets, view.getRightEdge());
    714                 int column;
    715                 if (index == -1) {
    716                     // Smaller than the first element; just use the first column
    717                     column = 0;
    718                 } else if (index < 0) {
    719                     column = -(index + 2);
    720                 } else {
    721                     column = index;
    722                 }
    723 
    724                 if (column < view.mCol) {
    725                     column = view.mCol;
    726                 }
    727 
    728                 view.mColSpan = column - view.mCol + 1;
    729             }
    730         }
    731 
    732         /**
    733          * Initializes the row and rowSpan attributes of the views
    734          */
    735         private void initializeRows() {
    736             Map<Integer, List<View>> mRowViews = new HashMap<Integer, List<View>>();
    737             for (View view : mViews) {
    738                 if (view.mElement == mLayout) {
    739                     continue;
    740                 }
    741                 int y = view.getTopEdge();
    742                 List<View> list = mRowViews.get(y);
    743                 if (list == null) {
    744                     list = new ArrayList<View>();
    745                     mRowViews.put(y, list);
    746                 }
    747                 list.add(view);
    748             }
    749 
    750             List<Integer> rowOffsets = new ArrayList<Integer>(mRowViews.keySet());
    751             Collections.sort(rowOffsets);
    752 
    753             int rowIndex = 0;
    754             for (Integer row : rowOffsets) {
    755                 List<View> views = mRowViews.get(row);
    756                 if (views != null) {
    757                     for (View view : views) {
    758                         view.mRow = rowIndex;
    759                     }
    760                 }
    761                 rowIndex++;
    762             }
    763 
    764             // Initialize row spans
    765             for (View view : mViews) {
    766                 if (view.mElement == mLayout) {
    767                     continue;
    768                 }
    769                 int index = Collections.binarySearch(rowOffsets, view.getBottomEdge());
    770                 int row;
    771                 if (index == -1) {
    772                     // Smaller than the first element; just use the first row
    773                     row = 0;
    774                 } else if (index < 0) {
    775                     row = -(index + 2);
    776                 } else {
    777                     row = index;
    778                 }
    779 
    780                 if (row < view.mRow) {
    781                     row = view.mRow;
    782                 }
    783 
    784                 view.mRowSpan = row - view.mRow + 1;
    785             }
    786         }
    787 
    788         /**
    789          * Walks over a given view hierarchy and locates views to be placed in
    790          * the grid layout (or deleted if we are flattening the hierarchy)
    791          *
    792          * @param view the view to analyze
    793          * @param isRoot whether this view is the root (which cannot be removed)
    794          * @return the {@link View} object for the {@link CanvasViewInfo}
    795          *         hierarchy we just analyzed, or null
    796          */
    797         private View scan(CanvasViewInfo view, boolean isRoot) {
    798             View added = null;
    799             if (!mFlatten || !isRemovableLayout(view)) {
    800                 added = add(view);
    801                 if (!isRoot) {
    802                     return added;
    803                 }
    804             } else {
    805                 mDelete.add(getElement(view));
    806             }
    807 
    808             // Build up a table model of the view
    809             for (CanvasViewInfo child : view.getChildren()) {
    810                 Element childElement = getElement(child);
    811 
    812                 // See if this view shares the edge with the removed
    813                 // parent layout, and if so, record that such that we can
    814                 // later handle attachments to the removed parent edges
    815 
    816                 if (mFlatten && isRemovableLayout(child)) {
    817                     // When flattening, we want to disregard all layouts and instead
    818                     // add their children!
    819                     for (CanvasViewInfo childView : child.getChildren()) {
    820                         scan(childView, false);
    821                     }
    822                     mDelete.add(childElement);
    823                 } else {
    824                     scan(child, false);
    825                 }
    826             }
    827 
    828             return added;
    829         }
    830 
    831         /** Adds the given {@link CanvasViewInfo} into our internal view list */
    832         private View add(CanvasViewInfo info) {
    833             Element element = getElement(info);
    834             View view = new View(info, element);
    835             mViews.add(view);
    836             mElementToView.put(element, view);
    837             return view;
    838         }
    839 
    840         private void analyzeKnownLayouts() {
    841             Set<Element> parents = new HashSet<Element>();
    842             for (View view : mViews) {
    843                 Node parent = view.getElement().getParentNode();
    844                 if (parent instanceof Element) {
    845                     parents.add((Element) parent);
    846                 }
    847             }
    848 
    849             List<Collection<View>> rowGroups = new ArrayList<Collection<View>>();
    850             List<Collection<View>> columnGroups = new ArrayList<Collection<View>>();
    851             for (Element parent : parents) {
    852                 String tagName = parent.getTagName();
    853                 if (tagName.equals(LINEAR_LAYOUT) || tagName.equals(TABLE_LAYOUT) ||
    854                         tagName.equals(TABLE_ROW) || tagName.equals(RADIO_GROUP)) {
    855                     Set<View> group = new HashSet<View>();
    856                     for (Element child : DomUtilities.getChildren(parent)) {
    857                         View view = mElementToView.get(child);
    858                         if (view != null) {
    859                             group.add(view);
    860                         }
    861                     }
    862                     if (group.size() > 1) {
    863                         boolean isVertical = VALUE_VERTICAL.equals(parent.getAttributeNS(
    864                                 ANDROID_URI, ATTR_ORIENTATION));
    865                         if (tagName.equals(TABLE_LAYOUT)) {
    866                             isVertical = true;
    867                         } else if (tagName.equals(TABLE_ROW)) {
    868                             isVertical = false;
    869                         }
    870                         if (isVertical) {
    871                             columnGroups.add(group);
    872                         } else {
    873                             rowGroups.add(group);
    874                         }
    875                     }
    876                 } else if (tagName.equals(RELATIVE_LAYOUT)) {
    877                     List<Element> children = DomUtilities.getChildren(parent);
    878                     for (Element child : children) {
    879                         View view = mElementToView.get(child);
    880                         if (view == null) {
    881                             continue;
    882                         }
    883                         NamedNodeMap attributes = child.getAttributes();
    884                         for (int i = 0, n = attributes.getLength(); i < n; i++) {
    885                             Attr attr = (Attr) attributes.item(i);
    886                             String name = attr.getLocalName();
    887                             if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
    888                                 boolean alignVertical =
    889                                         name.equals(ATTR_LAYOUT_ALIGN_TOP) ||
    890                                         name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) ||
    891                                         name.equals(ATTR_LAYOUT_ALIGN_BASELINE);
    892                                 boolean alignHorizontal =
    893                                         name.equals(ATTR_LAYOUT_ALIGN_LEFT) ||
    894                                         name.equals(ATTR_LAYOUT_ALIGN_RIGHT);
    895                                 if (!alignVertical && !alignHorizontal) {
    896                                     continue;
    897                                 }
    898                                 String value = attr.getValue();
    899                                 if (value.startsWith(ID_PREFIX)
    900                                         || value.startsWith(NEW_ID_PREFIX)) {
    901                                     String targetName = BaseLayoutRule.stripIdPrefix(value);
    902                                     Element target = null;
    903                                     for (Element c : children) {
    904                                         String id = VisualRefactoring.getId(c);
    905                                         if (targetName.equals(BaseLayoutRule.stripIdPrefix(id))) {
    906                                             target = c;
    907                                             break;
    908                                         }
    909                                     }
    910                                     View targetView = mElementToView.get(target);
    911                                     if (targetView != null) {
    912                                         List<View> group = new ArrayList<View>(2);
    913                                         group.add(view);
    914                                         group.add(targetView);
    915                                         if (alignHorizontal) {
    916                                             columnGroups.add(group);
    917                                         } else {
    918                                             assert alignVertical;
    919                                             rowGroups.add(group);
    920                                         }
    921                                     }
    922                                 }
    923                             }
    924                         }
    925                     }
    926                 } else {
    927                     // TODO: Consider looking for interesting metadata from other layouts
    928                 }
    929             }
    930 
    931             // Assign the same top or left coordinates to the groups to ensure that they
    932             // all get positioned in the same row or column
    933             for (Collection<View> rowGroup : rowGroups) {
    934                 // Find the smallest one
    935                 Iterator<View> iterator = rowGroup.iterator();
    936                 int smallest = iterator.next().mY1;
    937                 while (iterator.hasNext()) {
    938                     smallest = Math.min(smallest, iterator.next().mY1);
    939                 }
    940                 for (View view : rowGroup) {
    941                    view.mY2 -= (view.mY1 - smallest);
    942                    view.mY1 = smallest;
    943                 }
    944             }
    945             for (Collection<View> columnGroup : columnGroups) {
    946                 Iterator<View> iterator = columnGroup.iterator();
    947                 int smallest = iterator.next().mX1;
    948                 while (iterator.hasNext()) {
    949                     smallest = Math.min(smallest, iterator.next().mX1);
    950                 }
    951                 for (View view : columnGroup) {
    952                    view.mX2 -= (view.mX1 - smallest);
    953                    view.mX1 = smallest;
    954                 }
    955             }
    956         }
    957 
    958         /**
    959          * Returns true if the given {@link CanvasViewInfo} represents an element we
    960          * should remove in a flattening conversion. We don't want to remove non-layout
    961          * views, or layout views that for example contain drawables on their own.
    962          */
    963         private boolean isRemovableLayout(CanvasViewInfo child) {
    964             // The element being converted is NOT removable!
    965             Element element = getElement(child);
    966             if (element == mLayout) {
    967                 return false;
    968             }
    969 
    970             ElementDescriptor descriptor = child.getUiViewNode().getDescriptor();
    971             String name = descriptor.getXmlLocalName();
    972             if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT)
    973                     || name.equals(TABLE_LAYOUT) || name.equals(TABLE_ROW)) {
    974                 // Don't delete layouts that provide a background image or gradient
    975                 if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
    976                     AdtPlugin.log(IStatus.WARNING,
    977                             "Did not flatten layout %1$s because it defines a '%2$s' attribute",
    978                             VisualRefactoring.getId(element), ATTR_BACKGROUND);
    979                     return false;
    980                 }
    981 
    982                 return true;
    983             }
    984 
    985             return false;
    986         }
    987     }
    988 }
    989