diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java | 988 |
1 files changed, 988 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java new file mode 100644 index 000000000..fe673a5b7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java @@ -0,0 +1,988 @@ +/* + * 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; + } + } +} |