/* * Copyright (C) 2009 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; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkConstants; import org.eclipse.core.resources.IProject; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; /** * This class computes the new screen size in "exploded rendering" mode. * It goes through the whole layout tree and figures out how many embedded layouts will have * extra padding and compute how that will affect the screen size. * * TODO * - find a better class name :) * - move the logic for each layout to the layout rule classes? * - support custom classes (by querying JDT for its super class and reverting to its behavior) */ public final class ExplodedRenderingHelper { /** value of the padding in pixel. * TODO: make a preference? */ public final static int PADDING_VALUE = 10; private final int[] mPadding = new int[] { 0, 0 }; private Set<String> mLayoutNames; /** * Computes the padding. access the result through {@link #getWidthPadding()} and * {@link #getHeightPadding()}. * @param root the root node (ie the top layout). * @param iProject the project to which the layout belong. */ public ExplodedRenderingHelper(Node root, IProject iProject) { // get the layout descriptors to get the name of all the layout classes. IAndroidTarget target = Sdk.getCurrent().getTarget(iProject); AndroidTargetData data = Sdk.getCurrent().getTargetData(target); LayoutDescriptors descriptors = data.getLayoutDescriptors(); mLayoutNames = new HashSet<String>(); List<ViewElementDescriptor> layoutDescriptors = descriptors.getLayoutDescriptors(); for (ViewElementDescriptor desc : layoutDescriptors) { mLayoutNames.add(desc.getXmlLocalName()); } computePadding(root, mPadding); } /** * (Unit tests only) * Computes the padding. access the result through {@link #getWidthPadding()} and * {@link #getHeightPadding()}. * @param root the root node (ie the top layout). * @param layoutNames the list of layout classes */ public ExplodedRenderingHelper(Node root, Set<String> layoutNames) { mLayoutNames = layoutNames; computePadding(root, mPadding); } /** * Returns the number of extra padding in the X axis. This doesn't return a number of pixel * or dip, but how many paddings are pushing the screen dimension out. */ public int getWidthPadding() { return mPadding[0]; } /** * Returns the number of extra padding in the Y axis. This doesn't return a number of pixel * or dip, but how many paddings are pushing the screen dimension out. */ public int getHeightPadding() { return mPadding[1]; } /** * Computes the number of padding for a given view, and fills the given array of int. * <p/>index 0 is X axis, index 1 is Y axis * @param view the view to compute * @param padding the result padding (index 0 is X axis, index 1 is Y axis) */ private void computePadding(Node view, int[] padding) { String localName = view.getLocalName(); // first compute for each children NodeList children = view.getChildNodes(); int count = children.getLength(); if (count > 0) { // compute the padding for all the children. Map<Node, int[]> childrenPadding = new HashMap<Node, int[]>(count); for (int i = 0 ; i < count ; i++) { Node child = children.item(i); short type = child.getNodeType(); if (type == Node.ELEMENT_NODE) { // ignore TEXT/CDATA nodes. int[] p = new int[] { 0, 0 }; childrenPadding.put(child, p); computePadding(child, p); } } // since the non ELEMENT_NODE children were filtered out, count must be updated. count = childrenPadding.size(); // now combine/compare based on the parent. if (count == 1) { int[] p = childrenPadding.get(childrenPadding.keySet().iterator().next()); padding[0] = p[0]; padding[1] = p[1]; } else { if ("LinearLayout".equals(localName)) { //$NON-NLS-1$ String orientation = getAttribute(view, "orientation", null); //$NON-NLS-1$ // default value is horizontal boolean horizontal = orientation == null || "horizontal".equals("vertical"); //$NON-NLS-1$ //$NON-NLS-2$ combineLinearLayout(childrenPadding.values(), padding, horizontal); } else if ("TableLayout".equals(localName)) { //$NON-NLS-1$ combineLinearLayout(childrenPadding.values(), padding, false /*horizontal*/); } else if ("TableRow".equals(localName)) { //$NON-NLS-1$ combineLinearLayout(childrenPadding.values(), padding, true /*true*/); // TODO: properly support Relative Layouts. // } else if ("RelativeLayout".equals(localName)) { //$NON-NLS-1$ // combineRelativeLayout(childrenPadding, padding); } else { // unknown layout. For now, let's consider it's better to add the children // margins in both dimensions than not at all. for (int[] p : childrenPadding.values()) { padding[0] += p[0]; padding[1] += p[1]; } } } } // if the view itself is a layout, add its padding if (mLayoutNames.contains(localName)) { padding[0]++; padding[1]++; } } /** * Combines the padding of the children of a linear layout. * <p/>For this layout, the padding of the children are added in the direction of * the layout, while the max is taken for the other direction. * @param paddings the list of the padding for the children. * @param resultPadding the result padding array to fill. * @param horizontal whether this layout is horizontal (<code>true</code>) or vertical * (<code>false</code>) */ private void combineLinearLayout(Collection<int[]> paddings, int[] resultPadding, boolean horizontal) { // The way the children are combined will depend on the direction. // For instance in a vertical layout, we add the y padding as they all add to the length // of the needed canvas, while we take the biggest x padding needed by the children // the axis in which we take the sum of the padding of the children int sumIndex = horizontal ? 0 : 1; // the axis in which we take the max of the padding of the children int maxIndex = horizontal ? 1 : 0; int max = -1; for (int[] p : paddings) { resultPadding[sumIndex] += p[sumIndex]; if (max == -1 || max < p[maxIndex]) { max = p[maxIndex]; } } resultPadding[maxIndex] = max; } /** * Combine the padding of children of a relative layout. * @param childrenPadding a map of the children. This is guaranteed that the node object * are of type ELEMENT_NODE * @param padding * * TODO: Not used yet. Still need (lots of) work. */ private void combineRelativeLayout(Map<Node, int[]> childrenPadding, int[] padding) { /* * Combines the children of the layout. * The way this works: for each children, for each direction, look for all the chidrens * connected and compute the combined margin in that direction. * * There's a chance the returned value will be too much. this is due to the layout sometimes * dropping views which will not be dropped here. It's ok, as it's better to have too * much than not enough. * We could fix this by matching those UiElementNode with their bounds as returned * by the rendering (ie if bounds is 0/0 in h/w, then ignore the child) */ // list of the UiElementNode Set<Node> nodeSet = childrenPadding.keySet(); // map of Id -> node Map<String, Node> idNodeMap = computeIdNodeMap(nodeSet); for (Entry<Node, int[]> entry : childrenPadding.entrySet()) { Node node = entry.getKey(); // first horizontal, to the left. int[] leftResult = getBiggestMarginInDirection(node, 0 /*horizontal*/, "layout_toRightOf", "layout_toLeftOf", //$NON-NLS-1$ //$NON-NLS-2$ childrenPadding, nodeSet, idNodeMap, false /*includeThisPadding*/); // then to the right int[] rightResult = getBiggestMarginInDirection(node, 0 /*horizontal*/, "layout_toLeftOf", "layout_toRightOf", //$NON-NLS-1$ //$NON-NLS-2$ childrenPadding, nodeSet, idNodeMap, false /*includeThisPadding*/); // compute total horizontal margins int[] thisPadding = childrenPadding.get(node); int combinedMargin = (thisPadding != null ? thisPadding[0] : 0) + (leftResult != null ? leftResult[0] : 0) + (rightResult != null ? rightResult[0] : 0); if (combinedMargin > padding[0]) { padding[0] = combinedMargin; } // first vertical, above. int[] topResult = getBiggestMarginInDirection(node, 1 /*horizontal*/, "layout_below", "layout_above", //$NON-NLS-1$ //$NON-NLS-2$ childrenPadding, nodeSet, idNodeMap, false /*includeThisPadding*/); // then below int[] bottomResult = getBiggestMarginInDirection(node, 1 /*horizontal*/, "layout_above", "layout_below", //$NON-NLS-1$ //$NON-NLS-2$ childrenPadding, nodeSet, idNodeMap, false /*includeThisPadding*/); // compute total horizontal margins combinedMargin = (thisPadding != null ? thisPadding[1] : 0) + (topResult != null ? topResult[1] : 0) + (bottomResult != null ? bottomResult[1] : 0); if (combinedMargin > padding[1]) { padding[1] = combinedMargin; } } } /** * Computes the biggest margin in a given direction. * * TODO: Not used yet. Still need (lots of) work. */ private int[] getBiggestMarginInDirection(Node node, int resIndex, String relativeTo, String inverseRelation, Map<Node, int[]> childrenPadding, Set<Node> nodeSet, Map<String, Node> idNodeMap, boolean includeThisPadding) { NamedNodeMap attributes = node.getAttributes(); String viewId = getAttribute(node, "id", attributes); //$NON-NLS-1$ // first get the item this one is positioned relative to. String toLeftOfRef = getAttribute(node, relativeTo, attributes); Node toLeftOf = null; if (toLeftOfRef != null) { toLeftOf = idNodeMap.get(cleanUpIdReference(toLeftOfRef)); } ArrayList<Node> list = null; if (viewId != null) { // now to the left for items being placed to the left of this one. list = getMatchingNode(nodeSet, cleanUpIdReference(viewId), inverseRelation); } // now process each children in the same direction. if (toLeftOf != null) { if (list == null) { list = new ArrayList<Node>(); } if (list.indexOf(toLeftOf) == -1) { list.add(toLeftOf); } } int[] thisPadding = childrenPadding.get(node); if (list != null) { // since there's a combination to do, we'll return a new result object int[] result = null; for (Node nodeOnLeft : list) { int[] tempRes = getBiggestMarginInDirection(nodeOnLeft, resIndex, relativeTo, inverseRelation, childrenPadding, nodeSet, idNodeMap, true); if (tempRes != null && (result == null || result[resIndex] < tempRes[resIndex])) { result = tempRes; } } // return the combined padding if (includeThisPadding == false || thisPadding[resIndex] == 0) { // just return the one we got since this object adds no padding (or doesn't // need to be comibined) return result; } else if (result != null) { // if result is null, the main return below is used. // add the result we got with the padding from the current node int[] realRes = new int [2]; realRes[resIndex] = thisPadding[resIndex] + result[resIndex]; return realRes; } } // if we reach this, there were no other views to the left of this one, so just return // the view padding. return includeThisPadding ? thisPadding : null; } /** * Computes and returns a map of (id, node) for each node of a given {@link Set}. * <p/> * Nodes with no id are ignored and not put in the map. * @param nodes the nodes to fill the map with. * @return a newly allocated, non-null, map of (id, node) */ private Map<String, Node> computeIdNodeMap(Set<Node> nodes) { Map<String, Node> map = new HashMap<String, Node>(); for (Node node : nodes) { String viewId = getAttribute(node, "id", null); //$NON-NLS-1$ if (viewId != null) { map.put(cleanUpIdReference(viewId), node); } } return map; } /** * Cleans up a reference to an ID to return the ID itself only. * @param reference the reference to "clean up". * @return the id string only. */ private String cleanUpIdReference(String reference) { // format is @id/foo or @+id/foo or @android:id/foo, or something similar. int slash = reference.indexOf('/'); return reference.substring(slash); } /** * Returns a list of nodes for which a given attribute contains a reference to a given ID. * * @param nodes the list of nodes to search through * @param resId the requested ID * @param attribute the name of the attribute to test. * @return a newly allocated, non-null, list of nodes. Could be empty. */ private ArrayList<Node> getMatchingNode(Set<Node> nodes, String resId, String attribute) { ArrayList<Node> list = new ArrayList<Node>(); for (Node node : nodes) { String value = getAttribute(node, attribute, null); if (value != null) { value = cleanUpIdReference(value); if (value.equals(resId)) { list.add(node); } } } return list; } /** * Returns an attribute for a given node. * @param node the node to query * @param name the name of an attribute * @param attributes the option {@link NamedNodeMap} object to use to read the attributes from. */ private static String getAttribute(Node node, String name, NamedNodeMap attributes) { if (attributes == null) { attributes = node.getAttributes(); } if (attributes != null) { Node attribute = attributes.getNamedItemNS(SdkConstants.NS_RESOURCES, name); if (attribute != null) { return attribute.getNodeValue(); } } return null; } }