1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 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 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 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. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout; 18 19 import com.android.SdkConstants; 20 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 21 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 22 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 23 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 24 import com.android.sdklib.IAndroidTarget; 25 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; 30 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; 36 import java.util.Map; 37 import java.util.Map.Entry; 38 import java.util.Set; 39 40 /** 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. 44 * 45 * TODO 46 * - find a better class name :) 47 * - move the logic for each layout to the layout rule classes? 48 * - support custom classes (by querying JDT for its super class and reverting to its behavior) 49 */ 50 public final class ExplodedRenderingHelper { 51 /** value of the padding in pixel. 52 * TODO: make a preference? 53 */ 54 public final static int PADDING_VALUE = 10; 55 56 private final int[] mPadding = new int[] { 0, 0 }; 57 private Set<String> mLayoutNames; 58 59 /** 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. 64 */ 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(); 70 71 mLayoutNames = new HashSet<String>(); 72 List<ViewElementDescriptor> layoutDescriptors = descriptors.getLayoutDescriptors(); 73 for (ViewElementDescriptor desc : layoutDescriptors) { 74 mLayoutNames.add(desc.getXmlLocalName()); 75 } 76 77 computePadding(root, mPadding); 78 } 79 80 /** 81 * (Unit tests only) 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 86 */ 87 public ExplodedRenderingHelper(Node root, Set<String> layoutNames) { 88 mLayoutNames = layoutNames; 89 90 computePadding(root, mPadding); 91 } 92 93 /** 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. 96 */ 97 public int getWidthPadding() { 98 return mPadding[0]; 99 } 100 101 /** 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. 104 */ 105 public int getHeightPadding() { 106 return mPadding[1]; 107 } 108 109 /** 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) 114 */ 115 private void computePadding(Node view, int[] padding) { 116 String localName = view.getLocalName(); 117 118 // first compute for each children 119 NodeList children = view.getChildNodes(); 120 int count = children.getLength(); 121 if (count > 0) { 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); 131 } 132 } 133 134 // since the non ELEMENT_NODE children were filtered out, count must be updated. 135 count = childrenPadding.size(); 136 137 // now combine/compare based on the parent. 138 if (count == 1) { 139 int[] p = childrenPadding.get(childrenPadding.keySet().iterator().next()); 140 padding[0] = p[0]; 141 padding[1] = p[1]; 142 } else { 143 if ("LinearLayout".equals(localName)) { //$NON-NLS-1$ 144 String orientation = getAttribute(view, "orientation", null); //$NON-NLS-1$ 145 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); 157 } else { 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()) { 161 padding[0] += p[0]; 162 padding[1] += p[1]; 163 } 164 } 165 } 166 } 167 168 // if the view itself is a layout, add its padding 169 if (mLayoutNames.contains(localName)) { 170 padding[0]++; 171 padding[1]++; 172 } 173 } 174 175 /** 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>) 183 */ 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 189 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; 194 195 int max = -1; 196 for (int[] p : paddings) { 197 resultPadding[sumIndex] += p[sumIndex]; 198 if (max == -1 || max < p[maxIndex]) { 199 max = p[maxIndex]; 200 } 201 } 202 resultPadding[maxIndex] = max; 203 } 204 205 /** 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 209 * @param padding 210 * 211 * TODO: Not used yet. Still need (lots of) work. 212 */ 213 private void combineRelativeLayout(Map<Node, int[]> childrenPadding, int[] padding) { 214 /* 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. 218 * 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) 224 */ 225 226 // list of the UiElementNode 227 Set<Node> nodeSet = childrenPadding.keySet(); 228 // map of Id -> node 229 Map<String, Node> idNodeMap = computeIdNodeMap(nodeSet); 230 231 for (Entry<Node, int[]> entry : childrenPadding.entrySet()) { 232 Node node = entry.getKey(); 233 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*/); 239 240 // then to the right 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*/); 245 246 // compute total horizontal margins 247 int[] thisPadding = childrenPadding.get(node); 248 int combinedMargin = 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; 254 } 255 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*/); 261 262 // then below 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*/); 267 268 // compute total horizontal margins 269 combinedMargin = 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; 275 } 276 } 277 } 278 279 /** 280 * Computes the biggest margin in a given direction. 281 * 282 * TODO: Not used yet. Still need (lots of) work. 283 */ 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(); 289 290 String viewId = getAttribute(node, "id", attributes); //$NON-NLS-1$ 291 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)); 297 } 298 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); 303 } 304 305 // now process each children in the same direction. 306 if (toLeftOf != null) { 307 if (list == null) { 308 list = new ArrayList<Node>(); 309 } 310 311 if (list.indexOf(toLeftOf) == -1) { 312 list.add(toLeftOf); 313 } 314 } 315 316 int[] thisPadding = childrenPadding.get(node); 317 318 if (list != null) { 319 // since there's a combination to do, we'll return a new result object 320 int[] result = null; 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])) { 325 result = tempRes; 326 } 327 } 328 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) 333 return result; 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]; 338 return realRes; 339 } 340 } 341 342 // if we reach this, there were no other views to the left of this one, so just return 343 // the view padding. 344 return includeThisPadding ? thisPadding : null; 345 } 346 347 /** 348 * Computes and returns a map of (id, node) for each node of a given {@link Set}. 349 * <p/> 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) 353 */ 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); 360 } 361 } 362 return map; 363 } 364 365 /** 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. 369 */ 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); 374 } 375 376 /** 377 * Returns a list of nodes for which a given attribute contains a reference to a given ID. 378 * 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. 383 */ 384 private ArrayList<Node> getMatchingNode(Set<Node> nodes, String resId, 385 String attribute) { 386 ArrayList<Node> list = new ArrayList<Node>(); 387 388 for (Node node : nodes) { 389 String value = getAttribute(node, attribute, null); 390 if (value != null) { 391 value = cleanUpIdReference(value); 392 if (value.equals(resId)) { 393 list.add(node); 394 } 395 } 396 } 397 398 return list; 399 } 400 401 /** 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. 406 */ 407 private static String getAttribute(Node node, String name, NamedNodeMap attributes) { 408 if (attributes == null) { 409 attributes = node.getAttributes(); 410 } 411 412 if (attributes != null) { 413 Node attribute = attributes.getNamedItemNS(SdkConstants.NS_RESOURCES, name); 414 if (attribute != null) { 415 return attribute.getNodeValue(); 416 } 417 } 418 419 return null; 420 } 421 } 422