blob: e0d6313bfac9316f103aad1e977e6c284ee9c415 [file] [log] [blame]
/*
* 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;
}
}
}