aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ExplodedRenderingHelper.java
blob: 25fa3e991dfdf10c2f727a02d07f6bbb91a883ec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
/*
 * 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.SdkConstants;
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 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.Map.Entry;
import java.util.Set;

/**
 * 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;
    }
}