/* * 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.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 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 com.android.SdkConstants; import static com.android.SdkConstants.ANDROID_URI; 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; } } }