Home | History | Annotate | Download | only in layout
      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.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
     20 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
     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;
     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.Set;
     38 import java.util.Map.Entry;
     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