2 * Copyright (C) 2009 The Android Open Source Project
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.eclipse.org/org/documents/epl-v10.php
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.ide.eclipse.adt.internal.editors.layout;
19 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
20 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
21 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
22 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
23 import com.android.sdklib.IAndroidTarget;
24 import com.android.sdklib.SdkConstants;
26 import org.eclipse.core.resources.IProject;
27 import org.w3c.dom.NamedNodeMap;
28 import org.w3c.dom.Node;
29 import org.w3c.dom.NodeList;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.List;
38 import java.util.Map.Entry;
41 * This class computes the new screen size in "exploded rendering" mode.
42 * It goes through the whole layout tree and figures out how many embedded layouts will have
43 * extra padding and compute how that will affect the screen size.
46 * - find a better class name :)
47 * - move the logic for each layout to groovy scripts?
48 * - support custom classes (by querying JDT for its super class and reverting to its behavior)
50 public final class ExplodedRenderingHelper {
51 /** value of the padding in pixel.
52 * TODO: make a preference?
54 public final static int PADDING_VALUE = 10;
56 private final int[] mPadding = new int[] { 0, 0 };
57 private Set<String> mLayoutNames;
60 * Computes the padding. access the result through {@link #getWidthPadding()} and
61 * {@link #getHeightPadding()}.
62 * @param root the root node (ie the top layout).
63 * @param iProject the project to which the layout belong.
65 public ExplodedRenderingHelper(Node root, IProject iProject) {
66 // get the layout descriptors to get the name of all the layout classes.
67 IAndroidTarget target = Sdk.getCurrent().getTarget(iProject);
68 AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
69 LayoutDescriptors descriptors = data.getLayoutDescriptors();
71 mLayoutNames = new HashSet<String>();
72 List<ElementDescriptor> layoutDescriptors = descriptors.getLayoutDescriptors();
73 for (ElementDescriptor desc : layoutDescriptors) {
74 mLayoutNames.add(desc.getXmlLocalName());
77 computePadding(root, mPadding);
82 * Computes the padding. access the result through {@link #getWidthPadding()} and
83 * {@link #getHeightPadding()}.
84 * @param root the root node (ie the top layout).
85 * @param layoutNames the list of layout classes
87 public ExplodedRenderingHelper(Node root, Set<String> layoutNames) {
88 mLayoutNames = layoutNames;
90 computePadding(root, mPadding);
94 * Returns the number of extra padding in the X axis. This doesn't return a number of pixel
95 * or dip, but how many paddings are pushing the screen dimension out.
97 public int getWidthPadding() {
102 * Returns the number of extra padding in the Y axis. This doesn't return a number of pixel
103 * or dip, but how many paddings are pushing the screen dimension out.
105 public int getHeightPadding() {
110 * Computes the number of padding for a given view, and fills the given array of int.
111 * <p/>index 0 is X axis, index 1 is Y axis
112 * @param view the view to compute
113 * @param padding the result padding (index 0 is X axis, index 1 is Y axis)
115 private void computePadding(Node view, int[] padding) {
116 String localName = view.getLocalName();
118 // first compute for each children
119 NodeList children = view.getChildNodes();
120 int count = children.getLength();
122 // compute the padding for all the children.
123 Map<Node, int[]> childrenPadding = new HashMap<Node, int[]>(count);
124 for (int i = 0 ; i < count ; i++) {
125 Node child = children.item(i);
126 short type = child.getNodeType();
127 if (type == Node.ELEMENT_NODE) { // ignore TEXT/CDATA nodes.
128 int[] p = new int[] { 0, 0 };
129 childrenPadding.put(child, p);
130 computePadding(child, p);
134 // since the non ELEMENT_NODE children were filtered out, count must be updated.
135 count = childrenPadding.size();
137 // now combine/compare based on the parent.
139 int[] p = childrenPadding.get(childrenPadding.keySet().iterator().next());
143 if ("LinearLayout".equals(localName)) { //$NON-NLS-1$
144 String orientation = getAttribute(view, "orientation", null); //$NON-NLS-1$
146 // default value is horizontal
147 boolean horizontal = orientation == null ||
148 "horizontal".equals("vertical"); //$NON-NLS-1$ //$NON-NLS-2$
149 combineLinearLayout(childrenPadding.values(), padding, horizontal);
150 } else if ("TableLayout".equals(localName)) { //$NON-NLS-1$
151 combineLinearLayout(childrenPadding.values(), padding, false /*horizontal*/);
152 } else if ("TableRow".equals(localName)) { //$NON-NLS-1$
153 combineLinearLayout(childrenPadding.values(), padding, true /*true*/);
154 // TODO: properly support Relative Layouts.
155 // } else if ("RelativeLayout".equals(localName)) { //$NON-NLS-1$
156 // combineRelativeLayout(childrenPadding, padding);
158 // unknown layout. For now, let's consider it's better to add the children
159 // margins in both dimensions than not at all.
160 for (int[] p : childrenPadding.values()) {
168 // if the view itself is a layout, add its padding
169 if (mLayoutNames.contains(localName)) {
176 * Combines the padding of the children of a linear layout.
177 * <p/>For this layout, the padding of the children are added in the direction of
178 * the layout, while the max is taken for the other direction.
179 * @param paddings the list of the padding for the children.
180 * @param resultPadding the result padding array to fill.
181 * @param horizontal whether this layout is horizontal (<code>true</code>) or vertical
182 * (<code>false</code>)
184 private void combineLinearLayout(Collection<int[]> paddings, int[] resultPadding,
185 boolean horizontal) {
186 // The way the children are combined will depend on the direction.
187 // For instance in a vertical layout, we add the y padding as they all add to the length
188 // of the needed canvas, while we take the biggest x padding needed by the children
190 // the axis in which we take the sum of the padding of the children
191 int sumIndex = horizontal ? 0 : 1;
192 // the axis in which we take the max of the padding of the children
193 int maxIndex = horizontal ? 1 : 0;
196 for (int[] p : paddings) {
197 resultPadding[sumIndex] += p[sumIndex];
198 if (max == -1 || max < p[maxIndex]) {
202 resultPadding[maxIndex] = max;
206 * Combine the padding of children of a relative layout.
207 * @param childrenPadding a map of the children. This is guaranteed that the node object
208 * are of type ELEMENT_NODE
211 * TODO: Not used yet. Still need (lots of) work.
213 private void combineRelativeLayout(Map<Node, int[]> childrenPadding, int[] padding) {
215 * Combines the children of the layout.
216 * The way this works: for each children, for each direction, look for all the chidrens
217 * connected and compute the combined margin in that direction.
219 * There's a chance the returned value will be too much. this is due to the layout sometimes
220 * dropping views which will not be dropped here. It's ok, as it's better to have too
221 * much than not enough.
222 * We could fix this by matching those UiElementNode with their bounds as returned
223 * by the rendering (ie if bounds is 0/0 in h/w, then ignore the child)
226 // list of the UiElementNode
227 Set<Node> nodeSet = childrenPadding.keySet();
229 Map<String, Node> idNodeMap = computeIdNodeMap(nodeSet);
231 for (Entry<Node, int[]> entry : childrenPadding.entrySet()) {
232 Node node = entry.getKey();
234 // first horizontal, to the left.
235 int[] leftResult = getBiggestMarginInDirection(node, 0 /*horizontal*/,
236 "layout_toRightOf", "layout_toLeftOf", //$NON-NLS-1$ //$NON-NLS-2$
237 childrenPadding, nodeSet, idNodeMap,
238 false /*includeThisPadding*/);
241 int[] rightResult = getBiggestMarginInDirection(node, 0 /*horizontal*/,
242 "layout_toLeftOf", "layout_toRightOf", //$NON-NLS-1$ //$NON-NLS-2$
243 childrenPadding, nodeSet, idNodeMap,
244 false /*includeThisPadding*/);
246 // compute total horizontal margins
247 int[] thisPadding = childrenPadding.get(node);
249 (thisPadding != null ? thisPadding[0] : 0) +
250 (leftResult != null ? leftResult[0] : 0) +
251 (rightResult != null ? rightResult[0] : 0);
252 if (combinedMargin > padding[0]) {
253 padding[0] = combinedMargin;
256 // first vertical, above.
257 int[] topResult = getBiggestMarginInDirection(node, 1 /*horizontal*/,
258 "layout_below", "layout_above", //$NON-NLS-1$ //$NON-NLS-2$
259 childrenPadding, nodeSet, idNodeMap,
260 false /*includeThisPadding*/);
263 int[] bottomResult = getBiggestMarginInDirection(node, 1 /*horizontal*/,
264 "layout_above", "layout_below", //$NON-NLS-1$ //$NON-NLS-2$
265 childrenPadding, nodeSet, idNodeMap,
266 false /*includeThisPadding*/);
268 // compute total horizontal margins
270 (thisPadding != null ? thisPadding[1] : 0) +
271 (topResult != null ? topResult[1] : 0) +
272 (bottomResult != null ? bottomResult[1] : 0);
273 if (combinedMargin > padding[1]) {
274 padding[1] = combinedMargin;
280 * Computes the biggest margin in a given direction.
282 * TODO: Not used yet. Still need (lots of) work.
284 private int[] getBiggestMarginInDirection(Node node, int resIndex, String relativeTo,
285 String inverseRelation, Map<Node, int[]> childrenPadding,
286 Set<Node> nodeSet, Map<String, Node> idNodeMap,
287 boolean includeThisPadding) {
288 NamedNodeMap attributes = node.getAttributes();
290 String viewId = getAttribute(node, "id", attributes); //$NON-NLS-1$
292 // first get the item this one is positioned relative to.
293 String toLeftOfRef = getAttribute(node, relativeTo, attributes);
294 Node toLeftOf = null;
295 if (toLeftOfRef != null) {
296 toLeftOf = idNodeMap.get(cleanUpIdReference(toLeftOfRef));
299 ArrayList<Node> list = null;
300 if (viewId != null) {
301 // now to the left for items being placed to the left of this one.
302 list = getMatchingNode(nodeSet, cleanUpIdReference(viewId), inverseRelation);
305 // now process each children in the same direction.
306 if (toLeftOf != null) {
308 list = new ArrayList<Node>();
311 if (list.indexOf(toLeftOf) == -1) {
316 int[] thisPadding = childrenPadding.get(node);
319 // since there's a combination to do, we'll return a new result object
321 for (Node nodeOnLeft : list) {
322 int[] tempRes = getBiggestMarginInDirection(nodeOnLeft, resIndex, relativeTo,
323 inverseRelation, childrenPadding, nodeSet, idNodeMap, true);
324 if (tempRes != null && (result == null || result[resIndex] < tempRes[resIndex])) {
329 // return the combined padding
330 if (includeThisPadding == false || thisPadding[resIndex] == 0) {
331 // just return the one we got since this object adds no padding (or doesn't
332 // need to be comibined)
334 } else if (result != null) { // if result is null, the main return below is used.
335 // add the result we got with the padding from the current node
336 int[] realRes = new int [2];
337 realRes[resIndex] = thisPadding[resIndex] + result[resIndex];
342 // if we reach this, there were no other views to the left of this one, so just return
344 return includeThisPadding ? thisPadding : null;
348 * Computes and returns a map of (id, node) for each node of a given {@link Set}.
350 * Nodes with no id are ignored and not put in the map.
351 * @param nodes the nodes to fill the map with.
352 * @return a newly allocated, non-null, map of (id, node)
354 private Map<String, Node> computeIdNodeMap(Set<Node> nodes) {
355 Map<String, Node> map = new HashMap<String, Node>();
356 for (Node node : nodes) {
357 String viewId = getAttribute(node, "id", null); //$NON-NLS-1$
358 if (viewId != null) {
359 map.put(cleanUpIdReference(viewId), node);
366 * Cleans up a reference to an ID to return the ID itself only.
367 * @param reference the reference to "clean up".
368 * @return the id string only.
370 private String cleanUpIdReference(String reference) {
371 // format is @id/foo or @+id/foo or @android:id/foo, or something similar.
372 int slash = reference.indexOf('/');
373 return reference.substring(slash);
377 * Returns a list of nodes for which a given attribute contains a reference to a given ID.
379 * @param nodes the list of nodes to search through
380 * @param resId the requested ID
381 * @param attribute the name of the attribute to test.
382 * @return a newly allocated, non-null, list of nodes. Could be empty.
384 private ArrayList<Node> getMatchingNode(Set<Node> nodes, String resId,
386 ArrayList<Node> list = new ArrayList<Node>();
388 for (Node node : nodes) {
389 String value = getAttribute(node, attribute, null);
391 value = cleanUpIdReference(value);
392 if (value.equals(resId)) {
402 * Returns an attribute for a given node.
403 * @param node the node to query
404 * @param name the name of an attribute
405 * @param attributes the option {@link NamedNodeMap} object to use to read the attributes from.
407 private static String getAttribute(Node node, String name, NamedNodeMap attributes) {
408 if (attributes == null) {
409 attributes = node.getAttributes();
412 if (attributes != null) {
413 Node attribute = attributes.getNamedItemNS(SdkConstants.NS_RESOURCES, name);
414 if (attribute != null) {
415 return attribute.getNodeValue();