aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java
diff options
context:
space:
mode:
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.java1633
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;
+ }
+ }
+}