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