diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java | 1633 |
1 files changed, 1633 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java new file mode 100644 index 000000000..e0d6313bf --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java @@ -0,0 +1,1633 @@ +/* + * 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_BASELINE_ALIGNED; +import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE; +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_PARENT_BOTTOM; +import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; +import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; +import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP; +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_ALIGN_WITH_PARENT_MISSING; +import static com.android.SdkConstants.ATTR_LAYOUT_BELOW; +import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL; +import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL; +import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; +import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT; +import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP; +import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; +import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF; +import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF; +import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT; +import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.ATTR_ORIENTATION; +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.RELATIVE_LAYOUT; +import static com.android.SdkConstants.VALUE_FALSE; +import static com.android.SdkConstants.VALUE_N_DP; +import static com.android.SdkConstants.VALUE_TRUE; +import static com.android.SdkConstants.VALUE_VERTICAL; +import static com.android.SdkConstants.VALUE_WRAP_CONTENT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_HORIZ; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_VERT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_LEFT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_TOP; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK; + +import com.android.ide.common.layout.GravityHelper; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +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.preferences.AdtPrefs; +import com.android.utils.Pair; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.text.edits.MultiTextEdit; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Helper class which performs the bulk of the layout conversion to relative layout + * <p> + * Future enhancements: + * <ul> + * <li>Render the layout at multiple screen sizes and analyze how the widgets move and + * stretch and use that to add in additional constraints + * <li> Adapt the LinearLayout analysis code to work with TableLayouts and TableRows as well + * (just need to tweak the "isVertical" interpretation to account for the different defaults, + * and perhaps do something about column size properties. + * <li> We need to take into account existing margins and clear/update them + * </ul> + */ +class RelativeLayoutConversionHelper { + private final MultiTextEdit mRootEdit; + private final boolean mFlatten; + private final Element mLayout; + private final ChangeLayoutRefactoring mRefactoring; + private final CanvasViewInfo mRootView; + private List<Element> mDeletedElements; + + RelativeLayoutConversionHelper(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 convertToRelative() { + 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 + List<View> views = analyzeLayout(layoutView); + + // Create/update relative layout constraints + createAttachments(views); + } + + /** Returns the elements that were deleted, or null */ + List<Element> getDeletedElements() { + return mDeletedElements; + } + + /** + * Analyzes the given view hierarchy and produces a list of {@link View} objects which + * contain placement information for each element + */ + private List<View> analyzeLayout(CanvasViewInfo layoutView) { + EdgeList edgeList = new EdgeList(layoutView); + mDeletedElements = edgeList.getDeletedElements(); + deleteRemovedElements(mDeletedElements); + + List<Integer> columnOffsets = edgeList.getColumnOffsets(); + List<Integer> rowOffsets = edgeList.getRowOffsets(); + + // Compute x/y offsets for each row/column index + int[] left = new int[columnOffsets.size()]; + int[] top = new int[rowOffsets.size()]; + + Map<Integer, Integer> xToCol = new HashMap<Integer, Integer>(); + int columnIndex = 0; + for (Integer offset : columnOffsets) { + left[columnIndex] = offset; + xToCol.put(offset, columnIndex++); + } + Map<Integer, Integer> yToRow = new HashMap<Integer, Integer>(); + int rowIndex = 0; + for (Integer offset : rowOffsets) { + top[rowIndex] = offset; + yToRow.put(offset, rowIndex++); + } + + // Create a complete list of view objects + List<View> views = createViews(edgeList, columnOffsets); + initializeSpans(edgeList, columnOffsets, rowOffsets, xToCol, yToRow); + + // Sanity check + for (View view : views) { + assert view.getLeftEdge() == left[view.mCol]; + assert view.getTopEdge() == top[view.mRow]; + assert view.getRightEdge() == left[view.mCol+view.mColSpan]; + assert view.getBottomEdge() == top[view.mRow+view.mRowSpan]; + } + + // Ensure that every view has a proper id such that it can be referred to + // with a constraint + initializeIds(edgeList, views); + + // Attempt to lay the views out in a grid with constraints (though not that widgets + // can overlap as well) + Grid grid = new Grid(views, left, top); + computeKnownConstraints(views, edgeList); + computeHorizontalConstraints(grid); + computeVerticalConstraints(grid); + + return views; + } + + /** Produces a list of {@link View} objects from an {@link EdgeList} */ + private List<View> createViews(EdgeList edgeList, List<Integer> columnOffsets) { + List<View> views = new ArrayList<View>(); + for (Integer offset : columnOffsets) { + List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset); + if (leftEdgeViews == null) { + // must have been a right edge + continue; + } + for (View view : leftEdgeViews) { + views.add(view); + } + } + return views; + } + + /** 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, + !AdtPrefs.getPrefs().getFormatGuiXml() /*changeIndentation*/); + } + } + } + + /** Ensures that every element has an id such that it can be referenced from a constraint */ + private void initializeIds(EdgeList edgeList, List<View> views) { + // Ensure that all views have a valid id + for (View view : views) { + String id = mRefactoring.ensureHasId(mRootEdit, view.mElement, null); + edgeList.setIdAttributeValue(view, id); + } + } + + /** + * Initializes the column and row indices, as well as any column span and row span + * values + */ + private void initializeSpans(EdgeList edgeList, List<Integer> columnOffsets, + List<Integer> rowOffsets, Map<Integer, Integer> xToCol, Map<Integer, Integer> yToRow) { + // Now initialize table view row, column and spans + for (Integer offset : columnOffsets) { + List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset); + if (leftEdgeViews == null) { + // must have been a right edge + continue; + } + for (View view : leftEdgeViews) { + Integer col = xToCol.get(view.getLeftEdge()); + assert col != null; + Integer end = xToCol.get(view.getRightEdge()); + assert end != null; + + view.mCol = col; + view.mColSpan = end - col; + } + } + + for (Integer offset : rowOffsets) { + List<View> topEdgeViews = edgeList.getTopEdgeViews(offset); + if (topEdgeViews == null) { + // must have been a bottom edge + continue; + } + for (View view : topEdgeViews) { + Integer row = yToRow.get(view.getTopEdge()); + assert row != null; + Integer end = yToRow.get(view.getBottomEdge()); + assert end != null; + + view.mRow = row; + view.mRowSpan = end - row; + } + } + } + + /** + * Creates refactoring edits which adds or updates constraints for the given list of + * views + */ + private void createAttachments(List<View> views) { + // Make the attachments + String namespace = mRefactoring.getAndroidNamespacePrefix(); + for (View view : views) { + for (Pair<String, String> constraint : view.getHorizConstraints()) { + mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI, + namespace, constraint.getFirst(), constraint.getSecond()); + } + for (Pair<String, String> constraint : view.getVerticalConstraints()) { + mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI, + namespace, constraint.getFirst(), constraint.getSecond()); + } + } + } + + /** + * Analyzes the existing layouts and layout parameter objects in the document to infer + * constraints for layout types that we know about - such as LinearLayout baseline + * alignment, weights, gravity, etc. + */ + private void computeKnownConstraints(List<View> views, EdgeList edgeList) { + // List of parent layout elements we've already processed. We iterate through all + // the -children-, and we ask each for its element parent (which won't have a view) + // and we look at the parent's layout attributes and its children layout constraints, + // and then we stash away constraints that we can infer. This means that we will + // encounter the same parent for every sibling, so that's why there's a map to + // prevent duplicate work. + Set<Node> seen = new HashSet<Node>(); + + for (View view : views) { + Element element = view.getElement(); + Node parent = element.getParentNode(); + if (seen.contains(parent)) { + continue; + } + seen.add(parent); + + if (parent.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + Element layout = (Element) parent; + String layoutName = layout.getTagName(); + + if (LINEAR_LAYOUT.equals(layoutName)) { + analyzeLinearLayout(edgeList, layout); + } else if (RELATIVE_LAYOUT.equals(layoutName)) { + analyzeRelativeLayout(edgeList, layout); + } else { + // Some other layout -- add more conditional handling here + // for framelayout, tables, etc. + } + } + } + + /** + * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it + * does not define a weight + */ + private float getWeight(Element linearLayoutChild) { + String weight = linearLayoutChild.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT); + if (weight != null && weight.length() > 0) { + try { + return Float.parseFloat(weight); + } catch (NumberFormatException nfe) { + AdtPlugin.log(nfe, "Invalid weight %1$s", weight); + } + } + + return 0.0f; + } + + /** + * Returns the sum of all the layout weights of the children in the given LinearLayout + * + * @param linearLayout the layout to compute the total sum for + * @return the total sum of all the layout weights in the given layout + */ + private float getWeightSum(Element linearLayout) { + float sum = 0; + for (Element child : DomUtilities.getChildren(linearLayout)) { + sum += getWeight(child); + } + + return sum; + } + + /** + * Analyzes the given LinearLayout and updates the constraints to reflect + * relationships it can infer - based on baseline alignment, gravity, order and + * weights. This method also removes "0dip" as a special width/height used in + * LinearLayouts with weight distribution. + */ + private void analyzeLinearLayout(EdgeList edgeList, Element layout) { + boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI, + ATTR_ORIENTATION)); + View baselineRef = null; + if (!isVertical && + !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED))) { + // Baseline alignment. Find the tallest child and set it as the baseline reference. + int tallestHeight = 0; + View tallest = null; + for (Element child : DomUtilities.getChildren(layout)) { + View view = edgeList.getView(child); + if (view != null && view.getHeight() > tallestHeight) { + tallestHeight = view.getHeight(); + tallest = view; + } + } + if (tallest != null) { + baselineRef = tallest; + } + } + + float weightSum = getWeightSum(layout); + float cumulativeWeight = 0; + + List<Element> children = DomUtilities.getChildren(layout); + String prevId = null; + boolean isFirstChild = true; + boolean linkBackwards = true; + boolean linkForwards = false; + + for (int index = 0, childCount = children.size(); index < childCount; index++) { + Element child = children.get(index); + + View childView = edgeList.getView(child); + if (childView == null) { + // Could be a nested layout that is being removed etc + prevId = null; + isFirstChild = false; + continue; + } + + // Look at the layout_weight attributes and determine whether we should be + // attached on the bottom/right or on the top/left + if (weightSum > 0.0f) { + float weight = getWeight(child); + + // We can't emulate a LinearLayout where multiple children have positive + // weights. However, we CAN support the common scenario where a single + // child has a non-zero weight, and all children after it are pushed + // to the end and the weighted child fills the remaining space. + if (cumulativeWeight == 0 && weight > 0) { + // See if we have a bottom/right edge to attach the forwards link to + // (at the end of the forwards chains). Only if so can we link forwards. + View referenced; + if (isVertical) { + referenced = edgeList.getSharedBottomEdge(layout); + } else { + referenced = edgeList.getSharedRightEdge(layout); + } + if (referenced != null) { + linkForwards = true; + } + } else if (cumulativeWeight > 0) { + linkBackwards = false; + } + + cumulativeWeight += weight; + } + + analyzeGravity(edgeList, layout, isVertical, child, childView); + convert0dipToWrapContent(child); + + // Chain elements together in the flow direction of the linear layout + if (prevId != null) { // No constraint for first child + if (linkBackwards) { + if (isVertical) { + childView.addVerticalConstraint(ATTR_LAYOUT_BELOW, prevId); + } else { + childView.addHorizConstraint(ATTR_LAYOUT_TO_RIGHT_OF, prevId); + } + } + } else if (isFirstChild) { + assert linkBackwards; + + // First element; attach it to the parent if we can + if (isVertical) { + View referenced = edgeList.getSharedTopEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP, + VALUE_TRUE); + } else { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, + referenced.getId()); + } + } + } else { + View referenced = edgeList.getSharedLeftEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT, + VALUE_TRUE); + } else { + childView.addHorizConstraint( + ATTR_LAYOUT_ALIGN_LEFT, referenced.getId()); + } + } + } + } + + if (linkForwards) { + if (index < (childCount - 1)) { + Element nextChild = children.get(index + 1); + String nextId = mRefactoring.ensureHasId(mRootEdit, nextChild, null); + if (nextId != null) { + if (isVertical) { + childView.addVerticalConstraint(ATTR_LAYOUT_ABOVE, nextId); + } else { + childView.addHorizConstraint(ATTR_LAYOUT_TO_LEFT_OF, nextId); + } + } + } else { + // Attach to right/bottom edge of the layout + if (isVertical) { + View referenced = edgeList.getSharedBottomEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, + VALUE_TRUE); + } else { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM, + referenced.getId()); + } + } + } else { + View referenced = edgeList.getSharedRightEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, + VALUE_TRUE); + } else { + childView.addHorizConstraint( + ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId()); + } + } + } + } + } + + if (baselineRef != null && baselineRef.getId() != null + && !baselineRef.getId().equals(childView.getId())) { + assert !isVertical; + // Only align if they share the same gravity + if ((childView.getGravity() & GRAVITY_VERT_MASK) == + (baselineRef.getGravity() & GRAVITY_VERT_MASK)) { + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_BASELINE, baselineRef.getId()); + } + } + + prevId = mRefactoring.ensureHasId(mRootEdit, child, null); + isFirstChild = false; + } + } + + /** + * Checks the layout "gravity" value for the given child and updates the constraints + * to account for the gravity + */ + private int analyzeGravity(EdgeList edgeList, Element layout, boolean isVertical, + Element child, View childView) { + // Use gravity to constrain elements in the axis orthogonal to the + // direction of the layout + int gravity = childView.getGravity(); + if (isVertical) { + if ((gravity & GRAVITY_RIGHT) != 0) { + View referenced = edgeList.getSharedRightEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, + VALUE_TRUE); + } else { + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT, + referenced.getId()); + } + } + } else if ((gravity & GRAVITY_CENTER_HORIZ) != 0) { + View referenced1 = edgeList.getSharedLeftEdge(layout); + View referenced2 = edgeList.getSharedRightEdge(layout); + if (referenced1 != null && referenced2 == referenced1) { + if (isAncestor(referenced1.getElement(), child)) { + childView.addHorizConstraint(ATTR_LAYOUT_CENTER_HORIZONTAL, + VALUE_TRUE); + } + } + } else if ((gravity & GRAVITY_FILL_HORIZ) != 0) { + View referenced1 = edgeList.getSharedLeftEdge(layout); + View referenced2 = edgeList.getSharedRightEdge(layout); + if (referenced1 != null && referenced2 == referenced1) { + if (isAncestor(referenced1.getElement(), child)) { + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT, + VALUE_TRUE); + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, + VALUE_TRUE); + } else { + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, + referenced1.getId()); + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT, + referenced2.getId()); + } + } + } else if ((gravity & GRAVITY_LEFT) != 0) { + View referenced = edgeList.getSharedLeftEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT, + VALUE_TRUE); + } else { + childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, + referenced.getId()); + } + } + } + } else { + // Handle horizontal layout: perform vertical gravity attachments + if ((gravity & GRAVITY_BOTTOM) != 0) { + View referenced = edgeList.getSharedBottomEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, + VALUE_TRUE); + } else { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM, + referenced.getId()); + } + } + } else if ((gravity & GRAVITY_CENTER_VERT) != 0) { + View referenced1 = edgeList.getSharedTopEdge(layout); + View referenced2 = edgeList.getSharedBottomEdge(layout); + if (referenced1 != null && referenced2 == referenced1) { + if (isAncestor(referenced1.getElement(), child)) { + childView.addVerticalConstraint(ATTR_LAYOUT_CENTER_VERTICAL, + VALUE_TRUE); + } + } + } else if ((gravity & GRAVITY_FILL_VERT) != 0) { + View referenced1 = edgeList.getSharedTopEdge(layout); + View referenced2 = edgeList.getSharedBottomEdge(layout); + if (referenced1 != null && referenced2 == referenced1) { + if (isAncestor(referenced1.getElement(), child)) { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP, + VALUE_TRUE); + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, + VALUE_TRUE); + } else { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, + referenced1.getId()); + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM, + referenced2.getId()); + } + } + } else if ((gravity & GRAVITY_TOP) != 0) { + View referenced = edgeList.getSharedTopEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP, + VALUE_TRUE); + } else { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, + referenced.getId()); + } + } + } + } + return 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". + // 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. + String height = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); + // 0dip, 0dp, 0px, etc + if (height != null && height.startsWith("0")) { //$NON-NLS-1$ + mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI, + mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_HEIGHT, + VALUE_WRAP_CONTENT); + } + String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); + if (width != null && width.startsWith("0")) { //$NON-NLS-1$ + mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI, + mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_WIDTH, + VALUE_WRAP_CONTENT); + } + } + + /** + * Analyzes an embedded RelativeLayout within a layout hierarchy and updates the + * constraints in the EdgeList with those relationships which can continue in the + * outer single RelativeLayout. + */ + private void analyzeRelativeLayout(EdgeList edgeList, Element layout) { + NodeList children = layout.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node node = children.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element child = (Element) node; + View childView = edgeList.getView(child); + if (childView == null) { + // Could be a nested layout that is being removed etc + continue; + } + + NamedNodeMap attributes = child.getAttributes(); + for (int j = 0, m = attributes.getLength(); j < m; j++) { + Attr attribute = (Attr) attributes.item(j); + String name = attribute.getLocalName(); + String value = attribute.getValue(); + if (name.equals(ATTR_LAYOUT_WIDTH) + || name.equals(ATTR_LAYOUT_HEIGHT)) { + // Ignore these for now + } else if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) + && ANDROID_URI.equals(attribute.getNamespaceURI())) { + // Determine if the reference is to a known edge + String id = getIdBasename(value); + if (id != null) { + View referenced = edgeList.getView(id); + if (referenced != null) { + // This is a valid reference, so preserve + // the attribute + if (name.equals(ATTR_LAYOUT_BELOW) || + name.equals(ATTR_LAYOUT_ABOVE) || + name.equals(ATTR_LAYOUT_ALIGN_TOP) || + name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) || + name.equals(ATTR_LAYOUT_ALIGN_BASELINE)) { + // Vertical constraint + childView.addVerticalConstraint(name, value); + } else if (name.equals(ATTR_LAYOUT_ALIGN_LEFT) || + name.equals(ATTR_LAYOUT_TO_LEFT_OF) || + name.equals(ATTR_LAYOUT_TO_RIGHT_OF) || + name.equals(ATTR_LAYOUT_ALIGN_RIGHT)) { + // Horizontal constraint + childView.addHorizConstraint(name, value); + } else { + // We don't expect this + assert false : name; + } + } else { + // Reference to some layout that is not included here. + // TODO: See if the given layout has an edge + // that corresponds to one of our known views + // so we can adjust the constraints and keep it after all. + } + } else { + // It's a parent-relative constraint (such + // as aligning with a parent edge, or centering + // in the parent view) + boolean remove = true; + if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_LEFT)) { + View referenced = edgeList.getSharedLeftEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addHorizConstraint(name, VALUE_TRUE); + } else { + childView.addHorizConstraint( + ATTR_LAYOUT_ALIGN_LEFT, referenced.getId()); + } + remove = false; + } + } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) { + View referenced = edgeList.getSharedRightEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addHorizConstraint(name, VALUE_TRUE); + } else { + childView.addHorizConstraint( + ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId()); + } + remove = false; + } + } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_TOP)) { + View referenced = edgeList.getSharedTopEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addVerticalConstraint(name, VALUE_TRUE); + } else { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, + referenced.getId()); + } + remove = false; + } + } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM)) { + View referenced = edgeList.getSharedBottomEdge(layout); + if (referenced != null) { + if (isAncestor(referenced.getElement(), child)) { + childView.addVerticalConstraint(name, VALUE_TRUE); + } else { + childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM, + referenced.getId()); + } + remove = false; + } + } + + boolean alignWithParent = + name.equals(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING); + if (remove && alignWithParent) { + // TODO - look for this one AFTER we have processed + // everything else, and then set constraints as necessary + // IF there are no other conflicting constraints! + } + + // Otherwise it's some kind of centering which we don't support + // yet. + + // TODO: Find a way to determine whether we have + // a corresponding edge for the parent (e.g. if + // the ViewInfo bounds match our outer parent or + // some other edge) and if so, substitute for that + // id. + // For example, if this element was centered + // horizontally in a RelativeLayout that actually + // occupies the entire width of our outer layout, + // then it can be preserved after all! + + if (remove) { + if (name.startsWith("layout_margin")) { //$NON-NLS-1$ + continue; + } + + // Remove unknown attributes? + // It's too early to do this, because we may later want + // to *set* this value and it would result in an overlapping edits + // exception. Therefore, we need to RECORD which attributes should + // be removed, which lines should have its indentation adjusted + // etc and finally process it all at the end! + //mRefactoring.removeAttribute(mRootEdit, child, + // attribute.getNamespaceURI(), name); + } + } + } + } + } + } + } + + /** + * Given {@code @id/foo} or {@code @+id/foo}, returns foo. Note that given foo it will + * return null. + */ + private static String getIdBasename(String id) { + if (id.startsWith(NEW_ID_PREFIX)) { + return id.substring(NEW_ID_PREFIX.length()); + } else if (id.startsWith(ID_PREFIX)) { + return id.substring(ID_PREFIX.length()); + } + + return null; + } + + /** Returns true if the given second argument is a descendant of the first argument */ + private static boolean isAncestor(Node ancestor, Node node) { + while (node != null) { + if (node == ancestor) { + return true; + } + node = node.getParentNode(); + } + return false; + } + + /** + * Computes horizontal constraints for the views in the grid for any remaining views + * that do not have constraints (as the result of the analysis of known layouts). This + * will look at the rendered layout coordinates and attempt to connect elements based + * on a spatial layout in the grid. + */ + private void computeHorizontalConstraints(Grid grid) { + int columns = grid.getColumns(); + + String attachLeftProperty = ATTR_LAYOUT_ALIGN_PARENT_LEFT; + String attachLeftValue = VALUE_TRUE; + int marginLeft = 0; + for (int col = 0; col < columns; col++) { + if (!grid.colContainsTopLeftCorner(col)) { + // Just accumulate margins for the next column + marginLeft += grid.getColumnWidth(col); + } else { + // Add horizontal attachments + String firstId = null; + for (View view : grid.viewsStartingInCol(col, true)) { + assert view.getId() != null; + if (firstId == null) { + firstId = view.getId(); + if (view.isConstrainedHorizontally()) { + // Nothing to do -- we already have an accurate position for + // this view + } else if (attachLeftProperty != null) { + view.addHorizConstraint(attachLeftProperty, attachLeftValue); + if (marginLeft > 0) { + view.addHorizConstraint(ATTR_LAYOUT_MARGIN_LEFT, + String.format(VALUE_N_DP, marginLeft)); + marginLeft = 0; + } + } else { + assert false; + } + } else if (!view.isConstrainedHorizontally()) { + view.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, firstId); + } + } + } + + // Figure out edge for the next column + View view = grid.findRightEdgeView(col); + if (view != null) { + assert view.getId() != null; + attachLeftProperty = ATTR_LAYOUT_TO_RIGHT_OF; + attachLeftValue = view.getId(); + + marginLeft = 0; + } else if (marginLeft == 0) { + marginLeft = grid.getColumnWidth(col); + } + } + } + + /** + * Performs vertical layout just like the {@link #computeHorizontalConstraints} method + * did horizontally + */ + private void computeVerticalConstraints(Grid grid) { + int rows = grid.getRows(); + + String attachTopProperty = ATTR_LAYOUT_ALIGN_PARENT_TOP; + String attachTopValue = VALUE_TRUE; + int marginTop = 0; + for (int row = 0; row < rows; row++) { + if (!grid.rowContainsTopLeftCorner(row)) { + // Just accumulate margins for the next column + marginTop += grid.getRowHeight(row); + } else { + // Add horizontal attachments + String firstId = null; + for (View view : grid.viewsStartingInRow(row, true)) { + assert view.getId() != null; + if (firstId == null) { + firstId = view.getId(); + if (view.isConstrainedVertically()) { + // Nothing to do -- we already have an accurate position for + // this view + } else if (attachTopProperty != null) { + view.addVerticalConstraint(attachTopProperty, attachTopValue); + if (marginTop > 0) { + view.addVerticalConstraint(ATTR_LAYOUT_MARGIN_TOP, + String.format(VALUE_N_DP, marginTop)); + marginTop = 0; + } + } else { + assert false; + } + } else if (!view.isConstrainedVertically()) { + view.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, firstId); + } + } + } + + // Figure out edge for the next row + View view = grid.findBottomEdgeView(row); + if (view != null) { + assert view.getId() != null; + attachTopProperty = ATTR_LAYOUT_BELOW; + attachTopValue = view.getId(); + marginTop = 0; + } else if (marginTop == 0) { + marginTop = grid.getRowHeight(row); + } + } + } + + /** + * 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; + } + + /** + * A grid of cells which can contain views, used to infer spatial relationships when + * computing constraints. Note that a view can appear in than one cell; they will + * appear in all cells that their bounds overlap with! + */ + private class Grid { + private final int[] mLeft; + private final int[] mTop; + // A list from row to column to cell, where a cell is a list of views + private final List<List<List<View>>> mRowList; + private int mRowCount; + private int mColCount; + + Grid(List<View> views, int[] left, int[] top) { + mLeft = left; + mTop = top; + + // The left/top arrays should include the ending point too + mColCount = left.length - 1; + mRowCount = top.length - 1; + + // Using nested lists rather than arrays to avoid lack of typed arrays + // (can't create List<View>[row][column] arrays) + mRowList = new ArrayList<List<List<View>>>(top.length); + for (int row = 0; row < top.length; row++) { + List<List<View>> columnList = new ArrayList<List<View>>(left.length); + for (int col = 0; col < left.length; col++) { + columnList.add(new ArrayList<View>(4)); + } + mRowList.add(columnList); + } + + for (View view : views) { + // Get rid of the root view; we don't want that in the attachments logic; + // it was there originally such that it would contribute the outermost + // edges. + if (view.mElement == mLayout) { + continue; + } + + for (int i = 0; i < view.mRowSpan; i++) { + for (int j = 0; j < view.mColSpan; j++) { + mRowList.get(view.mRow + i).get(view.mCol + j).add(view); + } + } + } + } + + /** + * Returns the number of rows in the grid + * + * @return the row count + */ + public int getRows() { + return mRowCount; + } + + /** + * Returns the number of columns in the grid + * + * @return the column count + */ + public int getColumns() { + return mColCount; + } + + /** + * Returns the list of views overlapping the given cell + * + * @param row the row of the target cell + * @param col the column of the target cell + * @return a list of views overlapping the given column + */ + public List<View> get(int row, int col) { + return mRowList.get(row).get(col); + } + + /** + * Returns true if the given column contains a top left corner of a view + * + * @param column the column to check + * @return true if one or more views have their top left corner in this column + */ + public boolean colContainsTopLeftCorner(int column) { + for (int row = 0; row < mRowCount; row++) { + View view = getTopLeftCorner(row, column); + if (view != null) { + return true; + } + } + + return false; + } + + /** + * Returns true if the given row contains a top left corner of a view + * + * @param row the row to check + * @return true if one or more views have their top left corner in this row + */ + public boolean rowContainsTopLeftCorner(int row) { + for (int col = 0; col < mColCount; col++) { + View view = getTopLeftCorner(row, col); + if (view != null) { + return true; + } + } + + return false; + } + + /** + * Returns a list of views (optionally sorted by increasing row index) that have + * their left edge starting in the given column + * + * @param col the column to look up views for + * @param sort whether to sort the result in increasing row order + * @return a list of views starting in the given column + */ + public List<View> viewsStartingInCol(int col, boolean sort) { + List<View> views = new ArrayList<View>(); + for (int row = 0; row < mRowCount; row++) { + View view = getTopLeftCorner(row, col); + if (view != null) { + views.add(view); + } + } + + if (sort) { + View.sortByRow(views); + } + + return views; + } + + /** + * Returns a list of views (optionally sorted by increasing column index) that have + * their top edge starting in the given row + * + * @param row the row to look up views for + * @param sort whether to sort the result in increasing column order + * @return a list of views starting in the given row + */ + public List<View> viewsStartingInRow(int row, boolean sort) { + List<View> views = new ArrayList<View>(); + for (int col = 0; col < mColCount; col++) { + View view = getTopLeftCorner(row, col); + if (view != null) { + views.add(view); + } + } + + if (sort) { + View.sortByColumn(views); + } + + return views; + } + + /** + * Returns the pixel width of the given column + * + * @param col the column to look up the width of + * @return the width of the column + */ + public int getColumnWidth(int col) { + return mLeft[col + 1] - mLeft[col]; + } + + /** + * Returns the pixel height of the given row + * + * @param row the row to look up the height of + * @return the height of the row + */ + public int getRowHeight(int row) { + return mTop[row + 1] - mTop[row]; + } + + /** + * Returns the first view found that has its top left corner in the cell given by + * the row and column indexes, or null if not found. + * + * @param row the row of the target cell + * @param col the column of the target cell + * @return a view with its top left corner in the given cell, or null if not found + */ + View getTopLeftCorner(int row, int col) { + List<View> views = get(row, col); + if (views.size() > 0) { + for (View view : views) { + if (view.mRow == row && view.mCol == col) { + return view; + } + } + } + + return null; + } + + public View findRightEdgeView(int col) { + for (int row = 0; row < mRowCount; row++) { + List<View> views = get(row, col); + if (views.size() > 0) { + List<View> result = new ArrayList<View>(); + for (View view : views) { + // Ends on the right edge of this column? + if (view.mCol + view.mColSpan == col + 1) { + result.add(view); + } + } + if (result.size() > 1) { + View.sortByColumn(result); + } + if (result.size() > 0) { + return result.get(0); + } + } + } + + return null; + } + + public View findBottomEdgeView(int row) { + for (int col = 0; col < mColCount; col++) { + List<View> views = get(row, col); + if (views.size() > 0) { + List<View> result = new ArrayList<View>(); + for (View view : views) { + // Ends on the bottom edge of this column? + if (view.mRow + view.mRowSpan == row + 1) { + result.add(view); + } + } + if (result.size() > 1) { + View.sortByRow(result); + } + if (result.size() > 0) { + return result.get(0); + } + + } + } + + return null; + } + + /** + * Produces a display of view contents along with the pixel positions of each row/column, + * like the following (used for diagnostics only) + * <pre> + * |0 |49 |143 |192 |240 + * 36| | |button2 | + * 72| |radioButton1 |button2 | + * 74|button1 |radioButton1 |button2 | + * 108|button1 | |button2 | + * 110| | |button2 | + * 149| | | | + * 320 + * </pre> + */ + @Override + public String toString() { + // Dump out the view table + int cellWidth = 20; + + StringWriter stringWriter = new StringWriter(); + PrintWriter out = new PrintWriter(stringWriter); + out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + for (int col = 0; col < mColCount + 1; col++) { + out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$ + } + out.printf("\n"); //$NON-NLS-1$ + for (int row = 0; row < mRowCount + 1; row++) { + out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$ + if (row == mRowCount) { + break; + } + for (int col = 0; col < mColCount; col++) { + List<View> views = get(row, col); + StringBuilder sb = new StringBuilder(); + for (View view : views) { + String id = view != null ? view.getId() : ""; //$NON-NLS-1$ + if (id.startsWith(NEW_ID_PREFIX)) { + id = id.substring(NEW_ID_PREFIX.length()); + } + if (id.length() > cellWidth - 2) { + id = id.substring(0, cellWidth - 2); + } + if (sb.length() > 0) { + sb.append(','); + } + sb.append(id); + } + String cellString = sb.toString(); + if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$ + cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$ + } + out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$ + } + out.printf("\n"); //$NON-NLS-1$ + } + + out.flush(); + return stringWriter.toString(); + } + } + + /** 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 CanvasViewInfo mInfo; + private String mId; + private List<Pair<String, String>> mHorizConstraints = + new ArrayList<Pair<String, String>>(4); + private List<Pair<String, String>> mVerticalConstraints = + new ArrayList<Pair<String, String>>(4); + private int mGravity; + + public View(CanvasViewInfo view, Element element) { + mInfo = view; + mElement = element; + mGravity = GravityHelper.getGravity(element); + } + + public int getHeight() { + return mInfo.getAbsRect().height; + } + + public int getGravity() { + return mGravity; + } + + public String getId() { + return mId; + } + + public Element getElement() { + return mElement; + } + + public List<Pair<String, String>> getHorizConstraints() { + return mHorizConstraints; + } + + public List<Pair<String, String>> getVerticalConstraints() { + return mVerticalConstraints; + } + + public boolean isConstrainedHorizontally() { + return mHorizConstraints.size() > 0; + } + + public boolean isConstrainedVertically() { + return mVerticalConstraints.size() > 0; + } + + public void addHorizConstraint(String property, String value) { + assert property != null && value != null; + // TODO - look for duplicates? + mHorizConstraints.add(Pair.of(property, value)); + } + + public void addVerticalConstraint(String property, String value) { + assert property != null && value != null; + mVerticalConstraints.add(Pair.of(property, value)); + } + + public int getLeftEdge() { + return mInfo.getAbsRect().x; + } + + public int getTopEdge() { + return mInfo.getAbsRect().y; + } + + public int getRightEdge() { + Rectangle bounds = mInfo.getAbsRect(); + // +1: make the bounds overlap, so the right edge is the same as the + // left edge of the neighbor etc. Otherwise we end up with lots of 1-pixel wide + // columns between adjacent items. + return bounds.x + bounds.width + 1; + } + + public int getBottomEdge() { + Rectangle bounds = mInfo.getAbsRect(); + return bounds.y + bounds.height + 1; + } + + @Override + public String toString() { + return "View [mId=" + mId + "]"; //$NON-NLS-1$ //$NON-NLS-2$ + } + + public static void sortByRow(List<View> views) { + Collections.sort(views, new ViewComparator(true/*rowSort*/)); + } + + public static void sortByColumn(List<View> views) { + Collections.sort(views, new ViewComparator(false/*rowSort*/)); + } + + /** Comparator to help sort views by row or column index */ + private static class ViewComparator implements Comparator<View> { + boolean mRowSort; + + public ViewComparator(boolean rowSort) { + mRowSort = rowSort; + } + + @Override + public int compare(View view1, View view2) { + if (mRowSort) { + return view1.mRow - view2.mRow; + } else { + return view1.mCol - view2.mCol; + } + } + } + } + + /** + * An edge list takes a hierarchy of elements and records the bounds of each element + * into various lists such that it can answer queries about shared edges, about which + * particular pixels occur as a boundary edge, etc. + */ + private class EdgeList { + private final Map<Element, View> mElementToViewMap = new HashMap<Element, View>(100); + private final Map<String, View> mIdToViewMap = new HashMap<String, View>(100); + private final Map<Integer, List<View>> mLeft = new HashMap<Integer, List<View>>(); + private final Map<Integer, List<View>> mTop = new HashMap<Integer, List<View>>(); + private final Map<Integer, List<View>> mRight = new HashMap<Integer, List<View>>(); + private final Map<Integer, List<View>> mBottom = new HashMap<Integer, List<View>>(); + private final Map<Element, Element> mSharedLeftEdge = new HashMap<Element, Element>(); + private final Map<Element, Element> mSharedTopEdge = new HashMap<Element, Element>(); + private final Map<Element, Element> mSharedRightEdge = new HashMap<Element, Element>(); + private final Map<Element, Element> mSharedBottomEdge = new HashMap<Element, Element>(); + private final List<Element> mDelete = new ArrayList<Element>(); + + EdgeList(CanvasViewInfo view) { + analyze(view, true); + mDelete.remove(getElement(view)); + } + + public void setIdAttributeValue(View view, String id) { + assert id.startsWith(NEW_ID_PREFIX) || id.startsWith(ID_PREFIX); + view.mId = id; + mIdToViewMap.put(getIdBasename(id), view); + } + + public View getView(Element element) { + return mElementToViewMap.get(element); + } + + public View getView(String id) { + return mIdToViewMap.get(id); + } + + public List<View> getTopEdgeViews(Integer topOffset) { + return mTop.get(topOffset); + } + + public List<View> getLeftEdgeViews(Integer leftOffset) { + return mLeft.get(leftOffset); + } + + void record(Map<Integer, List<View>> map, Integer edge, View info) { + List<View> list = map.get(edge); + if (list == null) { + list = new ArrayList<View>(); + map.put(edge, list); + } + list.add(info); + } + + private List<Integer> getOffsets(Set<Integer> first, Set<Integer> second) { + Set<Integer> joined = new HashSet<Integer>(first.size() + second.size()); + joined.addAll(first); + joined.addAll(second); + List<Integer> unique = new ArrayList<Integer>(joined); + Collections.sort(unique); + + return unique; + } + + public List<Element> getDeletedElements() { + return mDelete; + } + + public List<Integer> getColumnOffsets() { + return getOffsets(mLeft.keySet(), mRight.keySet()); + } + public List<Integer> getRowOffsets() { + return getOffsets(mTop.keySet(), mBottom.keySet()); + } + + private View analyze(CanvasViewInfo view, boolean isRoot) { + View added = null; + if (!mFlatten || !isRemovableLayout(view)) { + added = add(view); + if (!isRoot) { + return added; + } + } else { + mDelete.add(getElement(view)); + } + + Element parentElement = getElement(view); + Rectangle parentBounds = view.getAbsRect(); + + // Build up a table model of the view + for (CanvasViewInfo child : view.getChildren()) { + Rectangle childBounds = child.getAbsRect(); + 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 (parentBounds.x == childBounds.x) { + mSharedLeftEdge.put(childElement, parentElement); + } + if (parentBounds.y == childBounds.y) { + mSharedTopEdge.put(childElement, parentElement); + } + if (parentBounds.x + parentBounds.width == childBounds.x + childBounds.width) { + mSharedRightEdge.put(childElement, parentElement); + } + if (parentBounds.y + parentBounds.height == childBounds.y + childBounds.height) { + mSharedBottomEdge.put(childElement, parentElement); + } + + if (mFlatten && isRemovableLayout(child)) { + // When flattening, we want to disregard all layouts and instead + // add their children! + for (CanvasViewInfo childView : child.getChildren()) { + analyze(childView, false); + + Element childViewElement = getElement(childView); + Rectangle childViewBounds = childView.getAbsRect(); + + // 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 (parentBounds.x == childViewBounds.x) { + mSharedLeftEdge.put(childViewElement, parentElement); + } + if (parentBounds.y == childViewBounds.y) { + mSharedTopEdge.put(childViewElement, parentElement); + } + if (parentBounds.x + parentBounds.width == childViewBounds.x + + childViewBounds.width) { + mSharedRightEdge.put(childViewElement, parentElement); + } + if (parentBounds.y + parentBounds.height == childViewBounds.y + + childViewBounds.height) { + mSharedBottomEdge.put(childViewElement, parentElement); + } + } + mDelete.add(childElement); + } else { + analyze(child, false); + } + } + + return added; + } + + public View getSharedLeftEdge(Element element) { + return getSharedEdge(element, mSharedLeftEdge); + } + + public View getSharedRightEdge(Element element) { + return getSharedEdge(element, mSharedRightEdge); + } + + public View getSharedTopEdge(Element element) { + return getSharedEdge(element, mSharedTopEdge); + } + + public View getSharedBottomEdge(Element element) { + return getSharedEdge(element, mSharedBottomEdge); + } + + private View getSharedEdge(Element element, Map<Element, Element> sharedEdgeMap) { + Element original = element; + + while (element != null) { + View view = getView(element); + if (view != null) { + assert isAncestor(element, original); + return view; + } + element = sharedEdgeMap.get(element); + } + + return null; + } + + private View add(CanvasViewInfo info) { + Rectangle bounds = info.getAbsRect(); + Element element = getElement(info); + View view = new View(info, element); + mElementToViewMap.put(element, view); + record(mLeft, Integer.valueOf(bounds.x), view); + record(mTop, Integer.valueOf(bounds.y), view); + record(mRight, Integer.valueOf(view.getRightEdge()), view); + record(mBottom, Integer.valueOf(view.getBottomEdge()), view); + return view; + } + + /** + * 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)) { + // 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; + } + } +} |