/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.refactoring; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_BACKGROUND; import static com.android.SdkConstants.ATTR_COLUMN_COUNT; import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE; import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM; import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT; import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT; import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP; import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN; import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN; import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; import static com.android.SdkConstants.ATTR_LAYOUT_ROW; import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN; import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; import static com.android.SdkConstants.ATTR_ORIENTATION; import static com.android.SdkConstants.FQCN_GRID_LAYOUT; import static com.android.SdkConstants.FQCN_SPACE; import static com.android.SdkConstants.GRAVITY_VALUE_FILL; import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL; import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.LINEAR_LAYOUT; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.RADIO_GROUP; import static com.android.SdkConstants.RELATIVE_LAYOUT; import static com.android.SdkConstants.SPACE; import static com.android.SdkConstants.TABLE_LAYOUT; import static com.android.SdkConstants.TABLE_ROW; import static com.android.SdkConstants.VALUE_FILL_PARENT; import static com.android.SdkConstants.VALUE_HORIZONTAL; import static com.android.SdkConstants.VALUE_MATCH_PARENT; import static com.android.SdkConstants.VALUE_VERTICAL; import static com.android.SdkConstants.VALUE_WRAP_CONTENT; import static com.android.ide.common.layout.GravityHelper.GRAVITY_HORIZ_MASK; import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK; import com.android.ide.common.api.IViewMetadata.FillPreference; import com.android.ide.common.layout.BaseLayoutRule; import com.android.ide.common.layout.GravityHelper; import com.android.ide.common.layout.GridLayoutRule; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.IStatus; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * Helper class which performs the bulk of the layout conversion to grid layout * <p> * Future enhancements: * <ul> * <li>Render the layout at multiple screen sizes and analyze how the widget bounds * change and use this to infer gravity * <li> Use the layout_width and layout_height attributes on views to infer column and * row flexibility (and as mentioned above, possibly layout_weight). * move and stretch and use that to add in additional constraints * <li> Take into account existing margins and add/subtract those from the * bounds computations and either clear or update them. * <li>Try to reorder elements into their natural order * <li> Try to preserve spacing? Right now everything gets converted into a compact * grid with no spacing between the views; consider inserting {@code <Space>} views * with dimensions based on existing distances. * </ul> */ @SuppressWarnings("restriction") // DOM model access class GridLayoutConverter { private final MultiTextEdit mRootEdit; private final boolean mFlatten; private final Element mLayout; private final ChangeLayoutRefactoring mRefactoring; private final CanvasViewInfo mRootView; private List<View> mViews; private String mNamespace; private int mColumnCount; /** Creates a new {@link GridLayoutConverter} */ GridLayoutConverter(ChangeLayoutRefactoring refactoring, Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) { mRefactoring = refactoring; mLayout = layout; mFlatten = flatten; mRootEdit = rootEdit; mRootView = rootView; } /** Performs conversion from any layout to a RelativeLayout */ public void convertToGridLayout() { if (mRootView == null) { return; } // Locate the view for the layout CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout); if (layoutView == null || layoutView.getChildren().size() == 0) { // No children. THAT was an easy conversion! return; } // Study the layout and get information about how to place individual elements GridModel gridModel = new GridModel(layoutView, mLayout, mFlatten); mViews = gridModel.getViews(); mColumnCount = gridModel.computeColumnCount(); deleteRemovedElements(gridModel.getDeletedElements()); mNamespace = mRefactoring.getAndroidNamespacePrefix(); processGravities(); // Insert space views if necessary insertStretchableSpans(); // Create/update relative layout constraints assignGridAttributes(); removeUndefinedAttrs(); if (mColumnCount > 0) { mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI, mNamespace, ATTR_COLUMN_COUNT, Integer.toString(mColumnCount)); } } private void insertStretchableSpans() { // Look at the rows and columns and determine if we need to have a stretchable // row and/or a stretchable column in the layout. // In a GridLayout, a row or column is stretchable if it defines a gravity (regardless // of what the gravity is -- in other words, a column is not just stretchable if it // has gravity=fill but also if it has gravity=left). Furthermore, ALL the elements // in the row/column have to be stretchable for the overall row/column to be // considered stretchable. // Map from row index to boolean for "is the row fixed/inflexible?" Map<Integer, Boolean> rowFixed = new HashMap<Integer, Boolean>(); Map<Integer, Boolean> columnFixed = new HashMap<Integer, Boolean>(); for (View view : mViews) { if (view.mElement == mLayout) { continue; } int gravity = GravityHelper.getGravity(view.mGravity, 0); if ((gravity & GRAVITY_HORIZ_MASK) == 0) { columnFixed.put(view.mCol, true); } else if (!columnFixed.containsKey(view.mCol)) { columnFixed.put(view.mCol, false); } if ((gravity & GRAVITY_VERT_MASK) == 0) { rowFixed.put(view.mRow, true); } else if (!rowFixed.containsKey(view.mRow)) { rowFixed.put(view.mRow, false); } } boolean hasStretchableRow = false; boolean hasStretchableColumn = false; for (boolean fixed : rowFixed.values()) { if (!fixed) { hasStretchableRow = true; } } for (boolean fixed : columnFixed.values()) { if (!fixed) { hasStretchableColumn = true; } } if (!hasStretchableRow || !hasStretchableColumn) { // Insert <Space> to hold stretchable space // TODO: May also have to increment column count! int offset = 0; // WHERE? String gridLayout = mLayout.getTagName(); if (mLayout instanceof IndexedRegion) { IndexedRegion region = (IndexedRegion) mLayout; int end = region.getEndOffset(); // TODO: Look backwards for the "</" // (and can it ever be <foo/>) ? end -= (gridLayout.length() + 3); // 3: <, /, > offset = end; } int row = rowFixed.size(); int column = columnFixed.size(); StringBuilder sb = new StringBuilder(64); String spaceTag = SPACE; IFile file = mRefactoring.getFile(); if (file != null) { spaceTag = SupportLibraryHelper.getTagFor(file.getProject(), FQCN_SPACE); if (spaceTag.equals(FQCN_SPACE)) { spaceTag = SPACE; } } sb.append('<').append(spaceTag).append(' '); String gravity; if (!hasStretchableRow && !hasStretchableColumn) { gravity = GRAVITY_VALUE_FILL; } else if (!hasStretchableRow) { gravity = GRAVITY_VALUE_FILL_VERTICAL; } else { assert !hasStretchableColumn; gravity = GRAVITY_VALUE_FILL_HORIZONTAL; } sb.append(mNamespace).append(':'); sb.append(ATTR_LAYOUT_GRAVITY).append('=').append('"').append(gravity); sb.append('"').append(' '); sb.append(mNamespace).append(':'); sb.append(ATTR_LAYOUT_ROW).append('=').append('"').append(Integer.toString(row)); sb.append('"').append(' '); sb.append(mNamespace).append(':'); sb.append(ATTR_LAYOUT_COLUMN).append('=').append('"').append(Integer.toString(column)); sb.append('"').append('/').append('>'); String space = sb.toString(); InsertEdit replace = new InsertEdit(offset, space); mRootEdit.addChild(replace); mColumnCount++; } } private void removeUndefinedAttrs() { ViewElementDescriptor descriptor = mRefactoring.getElementDescriptor(FQCN_GRID_LAYOUT); if (descriptor == null) { return; } Set<String> defined = new HashSet<String>(); AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes(); for (AttributeDescriptor attribute : layoutAttributes) { defined.add(attribute.getXmlLocalName()); } for (View view : mViews) { Element child = view.mElement; List<Attr> attributes = mRefactoring.findLayoutAttributes(child); for (Attr attribute : attributes) { String name = attribute.getLocalName(); if (!defined.contains(name)) { // Remove it try { mRefactoring.removeAttribute(mRootEdit, child, attribute.getNamespaceURI(), name); } catch (MalformedTreeException mte) { // Sometimes refactoring has modified attribute; not // removing // it is non-fatal so just warn instead of letting // refactoring // operation abort AdtPlugin.log(IStatus.WARNING, "Could not remove unsupported attribute %1$s; " + //$NON-NLS-1$ "already modified during refactoring?", //$NON-NLS-1$ attribute.getLocalName()); } } } } } /** Removes any elements targeted for deletion */ private void deleteRemovedElements(List<Element> delete) { if (mFlatten && delete.size() > 0) { for (Element element : delete) { mRefactoring.removeElementTags(mRootEdit, element, delete, false /*changeIndentation*/); } } } /** * Creates refactoring edits which adds or updates the grid attributes */ private void assignGridAttributes() { // We always convert to horizontal grid layouts for now mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI, mNamespace, ATTR_ORIENTATION, VALUE_HORIZONTAL); assignCellAttributes(); } /** * Assign cell attributes to the table, skipping those that will be implied * by the grid model */ private void assignCellAttributes() { int implicitRow = 0; int implicitColumn = 0; int nextRow = 0; for (View view : mViews) { Element element = view.getElement(); if (element == mLayout) { continue; } int row = view.getRow(); int column = view.getColumn(); if (column != implicitColumn && (implicitColumn > 0 || implicitRow > 0)) { mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, mNamespace, ATTR_LAYOUT_COLUMN, Integer.toString(column)); if (column < implicitColumn) { implicitRow++; } implicitColumn = column; } if (row != implicitRow) { mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, mNamespace, ATTR_LAYOUT_ROW, Integer.toString(row)); implicitRow = row; } int rowSpan = view.getRowSpan(); int columnSpan = view.getColumnSpan(); assert columnSpan >= 1; if (rowSpan > 1) { mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, mNamespace, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan)); } if (columnSpan > 1) { mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, mNamespace, ATTR_LAYOUT_COLUMN_SPAN, Integer.toString(columnSpan)); } nextRow = Math.max(nextRow, row + rowSpan); // wrap_content is redundant in GridLayouts Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); if (width != null && VALUE_WRAP_CONTENT.equals(width.getValue())) { mRefactoring.removeAttribute(mRootEdit, width); } Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); if (height != null && VALUE_WRAP_CONTENT.equals(height.getValue())) { mRefactoring.removeAttribute(mRootEdit, height); } // Fix up children moved from LinearLayouts that have "invalid" sizes that // was intended for layout weight handling in their old parent if (LINEAR_LAYOUT.equals(element.getParentNode().getNodeName())) { convert0dipToWrapContent(element); } implicitColumn += columnSpan; if (implicitColumn >= mColumnCount) { implicitColumn = 0; assert nextRow > implicitRow; implicitRow = nextRow; } } } private void processGravities() { for (View view : mViews) { Element element = view.getElement(); if (element == mLayout) { continue; } Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); String gravity = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY); String newGravity = null; if (width != null && (VALUE_MATCH_PARENT.equals(width.getValue()) || VALUE_FILL_PARENT.equals(width.getValue()))) { mRefactoring.removeAttribute(mRootEdit, width); newGravity = gravity = GRAVITY_VALUE_FILL_HORIZONTAL; } if (height != null && (VALUE_MATCH_PARENT.equals(height.getValue()) || VALUE_FILL_PARENT.equals(height.getValue()))) { mRefactoring.removeAttribute(mRootEdit, height); if (newGravity == GRAVITY_VALUE_FILL_HORIZONTAL) { newGravity = GRAVITY_VALUE_FILL; } else { newGravity = GRAVITY_VALUE_FILL_VERTICAL; } gravity = newGravity; } if (gravity == null || gravity.length() == 0) { ElementDescriptor descriptor = view.mInfo.getUiViewNode().getDescriptor(); if (descriptor instanceof ViewElementDescriptor) { ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) descriptor; String fqcn = viewDescriptor.getFullClassName(); FillPreference fill = ViewMetadataRepository.get().getFillPreference(fqcn); gravity = GridLayoutRule.computeDefaultGravity(fill); if (gravity != null) { newGravity = gravity; } } } if (newGravity != null) { mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, mNamespace, ATTR_LAYOUT_GRAVITY, newGravity); } view.mGravity = newGravity != null ? newGravity : gravity; } } /** Converts 0dip values in layout_width and layout_height to wrap_content instead */ private void convert0dipToWrapContent(Element child) { // Must convert layout_height="0dip" to layout_height="wrap_content". // (And since wrap_content is the default, what we really do is remove // the attribute completely.) // 0dip is a special trick used in linear layouts in the presence of // weights where 0dip ensures that the height of the view is not taken // into account when distributing the weights. However, when converted // to RelativeLayout this will instead cause the view to actually be assigned // 0 height. Attr height = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); // 0dip, 0dp, 0px, etc if (height != null && height.getValue().startsWith("0")) { //$NON-NLS-1$ mRefactoring.removeAttribute(mRootEdit, height); } Attr width = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); if (width != null && width.getValue().startsWith("0")) { //$NON-NLS-1$ mRefactoring.removeAttribute(mRootEdit, width); } } /** * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given * {@link Element} * * @param info the root {@link CanvasViewInfo} to search below * @param element the target element * @return the {@link CanvasViewInfo} which corresponds to the given element */ private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) { if (getElement(info) == element) { return info; } for (CanvasViewInfo child : info.getChildren()) { CanvasViewInfo result = findViewForElement(child, element); if (result != null) { return result; } } return null; } /** Returns the {@link Element} for the given {@link CanvasViewInfo} */ private static Element getElement(CanvasViewInfo info) { Node node = info.getUiViewNode().getXmlNode(); if (node instanceof Element) { return (Element) node; } return null; } /** Holds layout information about an individual view */ private static class View { private final Element mElement; private int mRow = -1; private int mCol = -1; private int mRowSpan = -1; private int mColSpan = -1; private int mX1; private int mY1; private int mX2; private int mY2; private CanvasViewInfo mInfo; private String mGravity; public View(CanvasViewInfo view, Element element) { mInfo = view; mElement = element; Rectangle b = mInfo.getAbsRect(); mX1 = b.x; mX2 = b.x + b.width; mY1 = b.y; mY2 = b.y + b.height; } /** * Returns the element for this view * * @return the element for the view */ public Element getElement() { return mElement; } /** * The assigned row for this view * * @return the assigned row */ public int getRow() { return mRow; } /** * The assigned column for this view * * @return the assigned column */ public int getColumn() { return mCol; } /** * The assigned row span for this view * * @return the assigned row span */ public int getRowSpan() { return mRowSpan; } /** * The assigned column span for this view * * @return the assigned column span */ public int getColumnSpan() { return mColSpan; } /** * The left edge of the view to be used for placement * * @return the left edge x coordinate */ public int getLeftEdge() { return mX1; } /** * The top edge of the view to be used for placement * * @return the top edge y coordinate */ public int getTopEdge() { return mY1; } /** * The right edge of the view to be used for placement * * @return the right edge x coordinate */ public int getRightEdge() { return mX2; } /** * The bottom edge of the view to be used for placement * * @return the bottom edge y coordinate */ public int getBottomEdge() { return mY2; } @Override public String toString() { return "View(" + VisualRefactoring.getId(mElement) + ": " + mX1 + "," + mY1 + ")"; } } /** Grid model for the views found in the view hierarchy, partitioned into rows and columns */ private static class GridModel { private final List<View> mViews = new ArrayList<View>(); private final List<Element> mDelete = new ArrayList<Element>(); private final Map<Element, View> mElementToView = new HashMap<Element, View>(); private Element mLayout; private boolean mFlatten; GridModel(CanvasViewInfo view, Element layout, boolean flatten) { mLayout = layout; mFlatten = flatten; scan(view, true); analyzeKnownLayouts(); initializeColumns(); initializeRows(); mDelete.remove(getElement(view)); } /** * Returns the {@link View} objects to be placed in the grid * * @return list of {@link View} objects, never null but possibly empty */ public List<View> getViews() { return mViews; } /** * Returns the list of elements that are scheduled for deletion in the * flattening operation * * @return elements to be deleted, never null but possibly empty */ public List<Element> getDeletedElements() { return mDelete; } /** * Compute and return column count * * @return the column count */ public int computeColumnCount() { int columnCount = 0; for (View view : mViews) { if (view.getElement() == mLayout) { continue; } int column = view.getColumn(); int columnSpan = view.getColumnSpan(); if (column + columnSpan > columnCount) { columnCount = column + columnSpan; } } return columnCount; } /** * Initializes the column and columnSpan attributes of the views */ private void initializeColumns() { // Now initialize table view row, column and spans Map<Integer, List<View>> mColumnViews = new HashMap<Integer, List<View>>(); for (View view : mViews) { if (view.mElement == mLayout) { continue; } int x = view.getLeftEdge(); List<View> list = mColumnViews.get(x); if (list == null) { list = new ArrayList<View>(); mColumnViews.put(x, list); } list.add(view); } List<Integer> columnOffsets = new ArrayList<Integer>(mColumnViews.keySet()); Collections.sort(columnOffsets); int columnIndex = 0; for (Integer column : columnOffsets) { List<View> views = mColumnViews.get(column); if (views != null) { for (View view : views) { view.mCol = columnIndex; } } columnIndex++; } // Initialize column spans for (View view : mViews) { if (view.mElement == mLayout) { continue; } int index = Collections.binarySearch(columnOffsets, view.getRightEdge()); int column; if (index == -1) { // Smaller than the first element; just use the first column column = 0; } else if (index < 0) { column = -(index + 2); } else { column = index; } if (column < view.mCol) { column = view.mCol; } view.mColSpan = column - view.mCol + 1; } } /** * Initializes the row and rowSpan attributes of the views */ private void initializeRows() { Map<Integer, List<View>> mRowViews = new HashMap<Integer, List<View>>(); for (View view : mViews) { if (view.mElement == mLayout) { continue; } int y = view.getTopEdge(); List<View> list = mRowViews.get(y); if (list == null) { list = new ArrayList<View>(); mRowViews.put(y, list); } list.add(view); } List<Integer> rowOffsets = new ArrayList<Integer>(mRowViews.keySet()); Collections.sort(rowOffsets); int rowIndex = 0; for (Integer row : rowOffsets) { List<View> views = mRowViews.get(row); if (views != null) { for (View view : views) { view.mRow = rowIndex; } } rowIndex++; } // Initialize row spans for (View view : mViews) { if (view.mElement == mLayout) { continue; } int index = Collections.binarySearch(rowOffsets, view.getBottomEdge()); int row; if (index == -1) { // Smaller than the first element; just use the first row row = 0; } else if (index < 0) { row = -(index + 2); } else { row = index; } if (row < view.mRow) { row = view.mRow; } view.mRowSpan = row - view.mRow + 1; } } /** * Walks over a given view hierarchy and locates views to be placed in * the grid layout (or deleted if we are flattening the hierarchy) * * @param view the view to analyze * @param isRoot whether this view is the root (which cannot be removed) * @return the {@link View} object for the {@link CanvasViewInfo} * hierarchy we just analyzed, or null */ private View scan(CanvasViewInfo view, boolean isRoot) { View added = null; if (!mFlatten || !isRemovableLayout(view)) { added = add(view); if (!isRoot) { return added; } } else { mDelete.add(getElement(view)); } // Build up a table model of the view for (CanvasViewInfo child : view.getChildren()) { Element childElement = getElement(child); // See if this view shares the edge with the removed // parent layout, and if so, record that such that we can // later handle attachments to the removed parent edges if (mFlatten && isRemovableLayout(child)) { // When flattening, we want to disregard all layouts and instead // add their children! for (CanvasViewInfo childView : child.getChildren()) { scan(childView, false); } mDelete.add(childElement); } else { scan(child, false); } } return added; } /** Adds the given {@link CanvasViewInfo} into our internal view list */ private View add(CanvasViewInfo info) { Element element = getElement(info); View view = new View(info, element); mViews.add(view); mElementToView.put(element, view); return view; } private void analyzeKnownLayouts() { Set<Element> parents = new HashSet<Element>(); for (View view : mViews) { Node parent = view.getElement().getParentNode(); if (parent instanceof Element) { parents.add((Element) parent); } } List<Collection<View>> rowGroups = new ArrayList<Collection<View>>(); List<Collection<View>> columnGroups = new ArrayList<Collection<View>>(); for (Element parent : parents) { String tagName = parent.getTagName(); if (tagName.equals(LINEAR_LAYOUT) || tagName.equals(TABLE_LAYOUT) || tagName.equals(TABLE_ROW) || tagName.equals(RADIO_GROUP)) { Set<View> group = new HashSet<View>(); for (Element child : DomUtilities.getChildren(parent)) { View view = mElementToView.get(child); if (view != null) { group.add(view); } } if (group.size() > 1) { boolean isVertical = VALUE_VERTICAL.equals(parent.getAttributeNS( ANDROID_URI, ATTR_ORIENTATION)); if (tagName.equals(TABLE_LAYOUT)) { isVertical = true; } else if (tagName.equals(TABLE_ROW)) { isVertical = false; } if (isVertical) { columnGroups.add(group); } else { rowGroups.add(group); } } } else if (tagName.equals(RELATIVE_LAYOUT)) { List<Element> children = DomUtilities.getChildren(parent); for (Element child : children) { View view = mElementToView.get(child); if (view == null) { continue; } NamedNodeMap attributes = child.getAttributes(); for (int i = 0, n = attributes.getLength(); i < n; i++) { Attr attr = (Attr) attributes.item(i); String name = attr.getLocalName(); if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { boolean alignVertical = name.equals(ATTR_LAYOUT_ALIGN_TOP) || name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) || name.equals(ATTR_LAYOUT_ALIGN_BASELINE); boolean alignHorizontal = name.equals(ATTR_LAYOUT_ALIGN_LEFT) || name.equals(ATTR_LAYOUT_ALIGN_RIGHT); if (!alignVertical && !alignHorizontal) { continue; } String value = attr.getValue(); if (value.startsWith(ID_PREFIX) || value.startsWith(NEW_ID_PREFIX)) { String targetName = BaseLayoutRule.stripIdPrefix(value); Element target = null; for (Element c : children) { String id = VisualRefactoring.getId(c); if (targetName.equals(BaseLayoutRule.stripIdPrefix(id))) { target = c; break; } } View targetView = mElementToView.get(target); if (targetView != null) { List<View> group = new ArrayList<View>(2); group.add(view); group.add(targetView); if (alignHorizontal) { columnGroups.add(group); } else { assert alignVertical; rowGroups.add(group); } } } } } } } else { // TODO: Consider looking for interesting metadata from other layouts } } // Assign the same top or left coordinates to the groups to ensure that they // all get positioned in the same row or column for (Collection<View> rowGroup : rowGroups) { // Find the smallest one Iterator<View> iterator = rowGroup.iterator(); int smallest = iterator.next().mY1; while (iterator.hasNext()) { smallest = Math.min(smallest, iterator.next().mY1); } for (View view : rowGroup) { view.mY2 -= (view.mY1 - smallest); view.mY1 = smallest; } } for (Collection<View> columnGroup : columnGroups) { Iterator<View> iterator = columnGroup.iterator(); int smallest = iterator.next().mX1; while (iterator.hasNext()) { smallest = Math.min(smallest, iterator.next().mX1); } for (View view : columnGroup) { view.mX2 -= (view.mX1 - smallest); view.mX1 = smallest; } } } /** * Returns true if the given {@link CanvasViewInfo} represents an element we * should remove in a flattening conversion. We don't want to remove non-layout * views, or layout views that for example contain drawables on their own. */ private boolean isRemovableLayout(CanvasViewInfo child) { // The element being converted is NOT removable! Element element = getElement(child); if (element == mLayout) { return false; } ElementDescriptor descriptor = child.getUiViewNode().getDescriptor(); String name = descriptor.getXmlLocalName(); if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT) || name.equals(TABLE_LAYOUT) || name.equals(TABLE_ROW)) { // Don't delete layouts that provide a background image or gradient if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) { AdtPlugin.log(IStatus.WARNING, "Did not flatten layout %1$s because it defines a '%2$s' attribute", VisualRefactoring.getId(element), ATTR_BACKGROUND); return false; } return true; } return false; } } }