aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
blob: d247e28d75ae2b6d3067160b83fede1bf5b20c01 (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
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
/*
 * Copyright (C) 2010 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.gle2;

import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.VIEW_MERGE;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.api.INode;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.utils.Pair;

import org.eclipse.swt.graphics.Rectangle;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import java.util.Set;

/**
 * The view hierarchy class manages a set of view info objects and performs find
 * operations on this set.
 */
public class ViewHierarchy {
    private static final boolean DUMP_INFO = false;

    private LayoutCanvas mCanvas;

    /**
     * Constructs a new {@link ViewHierarchy} tied to the given
     * {@link LayoutCanvas}.
     *
     * @param canvas The {@link LayoutCanvas} to create a {@link ViewHierarchy}
     *            for.
     */
    public ViewHierarchy(LayoutCanvas canvas) {
        mCanvas = canvas;
    }

    /**
     * The CanvasViewInfo root created by the last call to {@link #setSession}
     * with a valid layout.
     * <p/>
     * This <em>can</em> be null to indicate we're dealing with an empty document with
     * no root node. Null here does not mean the result was invalid, merely that the XML
     * had no content to display -- we need to treat an empty document as valid so that
     * we can drop new items in it.
     */
    private CanvasViewInfo mLastValidViewInfoRoot;

    /**
     * True when the last {@link #setSession} provided a valid {@link LayoutScene}.
     * <p/>
     * When false this means the canvas is displaying an out-dated result image & bounds and some
     * features should be disabled accordingly such a drag'n'drop.
     * <p/>
     * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
     * valid since it is an acceptable drop target.
     */
    private boolean mIsResultValid;

    /**
     * A list of invisible parents (see {@link CanvasViewInfo#isInvisible()} for
     * details) in the current view hierarchy.
     */
    private final List<CanvasViewInfo> mInvisibleParents = new ArrayList<CanvasViewInfo>();

    /**
     * A read-only view of {@link #mInvisibleParents}; note that this is NOT a copy so it
     * reflects updates to the underlying {@link #mInvisibleParents} list.
     */
    private final List<CanvasViewInfo> mInvisibleParentsReadOnly =
        Collections.unmodifiableList(mInvisibleParents);

    /**
     * Flag which records whether or not we have any exploded parent nodes in this
     * view hierarchy. This is used to track whether or not we need to recompute the
     * layout when we exit show-all-invisible-parents mode (see
     * {@link LayoutCanvas#showInvisibleViews}).
     */
    private boolean mExplodedParents;

    /**
     * Bounds of included views in the current view hierarchy when rendered in other context
     */
    private List<Rectangle> mIncludedBounds;

    /** The render session for the current view hierarchy */
    private RenderSession mSession;

    /** Map from nodes to canvas view infos */
    private Map<UiViewElementNode, CanvasViewInfo> mNodeToView = Collections.emptyMap();

    /** Map from DOM nodes to canvas view infos */
    private Map<Node, CanvasViewInfo> mDomNodeToView = Collections.emptyMap();

    /**
     * Disposes the view hierarchy content.
     */
    public void dispose() {
        if (mSession != null) {
            mSession.dispose();
            mSession = null;
        }
    }


    /**
     * Sets the result of the layout rendering. The result object indicates if the layout
     * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
     *
     * Implementation detail: the bridge's computeLayout() method already returns a newly
     * allocated ILayourResult. That means we can keep this result and hold on to it
     * when it is valid.
     *
     * @param session The new session, either valid or not.
     * @param explodedNodes The set of individual nodes the layout computer was asked to
     *            explode. Note that these are independent of the explode-all mode where
     *            all views are exploded; this is used only for the mode (
     *            {@link LayoutCanvas#showInvisibleViews}) where individual invisible
     *            nodes are padded during certain interactions.
     */
    /* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes,
            boolean layoutlib5) {
        // replace the previous scene, so the previous scene must be disposed.
        if (mSession != null) {
            mSession.dispose();
        }

        mSession = session;
        mIsResultValid = (session != null && session.getResult().isSuccess());
        mExplodedParents = false;
        mNodeToView = new HashMap<UiViewElementNode, CanvasViewInfo>(50);
        if (mIsResultValid && session != null) {
            List<ViewInfo> rootList = session.getRootViews();

            Pair<CanvasViewInfo,List<Rectangle>> infos = null;

            if (rootList == null || rootList.size() == 0) {
                // Special case: Look to see if this is really an empty <merge> view,
                // which shows up without any ViewInfos in the merge. In that case we
                // want to manufacture an empty view, such that we can target the view
                // via drag & drop, etc.
                if (hasMergeRoot()) {
                    ViewInfo mergeRoot = createMergeInfo(session);
                    infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
                } else {
                    infos = null;
                }
            } else {
                if (rootList.size() > 1 && hasMergeRoot()) {
                    ViewInfo mergeRoot = createMergeInfo(session);
                    mergeRoot.setChildren(rootList);
                    infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
                } else {
                    ViewInfo root = rootList.get(0);

                    if (root != null) {
                        infos = CanvasViewInfo.create(root, layoutlib5);
                        if (DUMP_INFO) {
                            dump(session, root, 0);
                        }
                    } else {
                        infos = null;
                    }
                }
            }
            if (infos != null) {
                mLastValidViewInfoRoot = infos.getFirst();
                mIncludedBounds = infos.getSecond();

                if (mLastValidViewInfoRoot.getUiViewNode() == null &&
                        mLastValidViewInfoRoot.getChildren().isEmpty()) {
                    GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
                    if (editor.getIncludedWithin() != null) {
                        // Somehow, this view was supposed to be rendered within another
                        // view, yet this view was rendered as part of the other view.
                        // In that case, abort attempting to show included in; clear the
                        // include context and trigger a standalone re-render.
                        editor.showIn(null);
                        return;
                    }
                }

            } else {
                mLastValidViewInfoRoot = null;
                mIncludedBounds = null;
            }

            updateNodeProxies(mLastValidViewInfoRoot);

            // Update the data structures related to tracking invisible and exploded nodes.
            // We need to find the {@link CanvasViewInfo} objects that correspond to
            // the passed in {@link UiElementNode} keys that were re-rendered, and mark
            // them as exploded and store them in a list for rendering.
            mExplodedParents = false;
            mInvisibleParents.clear();
            addInvisibleParents(mLastValidViewInfoRoot, explodedNodes);

            mDomNodeToView = new HashMap<Node, CanvasViewInfo>(mNodeToView.size());
            for (Map.Entry<UiViewElementNode, CanvasViewInfo> entry : mNodeToView.entrySet()) {
                mDomNodeToView.put(entry.getKey().getXmlNode(), entry.getValue());
            }

            // Update the selection
            mCanvas.getSelectionManager().sync();
        } else {
            mIncludedBounds = null;
            mInvisibleParents.clear();
            mDomNodeToView = Collections.emptyMap();
        }
    }

    private ViewInfo createMergeInfo(RenderSession session) {
        BufferedImage image = session.getImage();
        ControlPoint imageSize = ControlPoint.create(mCanvas,
                mCanvas.getHorizontalTransform().getMargin() + image.getWidth(),
                mCanvas.getVerticalTransform().getMargin() + image.getHeight());
        LayoutPoint layoutSize = imageSize.toLayout();
        UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
        List<UiElementNode> children = model.getUiChildren();
        return new ViewInfo(VIEW_MERGE, children.get(0), 0, 0, layoutSize.x, layoutSize.y);
    }

    /**
     * Returns true if this view hierarchy corresponds to an editor that has a {@code
     * <merge>} tag at the root
     *
     * @return true if there is a {@code <merge>} at the root of this editor's document
     */
    private boolean hasMergeRoot() {
        UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
        if (model != null) {
            List<UiElementNode> children = model.getUiChildren();
            if (children != null && children.size() > 0
                    && VIEW_MERGE.equals(children.get(0).getDescriptor().getXmlName())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Creates or updates the node proxy for this canvas view info.
     * <p/>
     * Since proxies are reused, this will update the bounds of an existing proxy when the
     * canvas is refreshed and a view changes position or size.
     * <p/>
     * This is a recursive call that updates the whole hierarchy starting at the given
     * view info.
     */
    private void updateNodeProxies(CanvasViewInfo vi) {
        if (vi == null) {
            return;
        }

        UiViewElementNode key = vi.getUiViewNode();

        if (key != null) {
            mCanvas.getNodeFactory().create(vi);
            mNodeToView.put(key, vi);
        }

        for (CanvasViewInfo child : vi.getChildren()) {
            updateNodeProxies(child);
        }
    }

    /**
     * Make a pass over the view hierarchy and look for two things:
     * <ol>
     * <li>Invisible parents. These are nodes that can hold children and have empty
     * bounds. These are then added to the {@link #mInvisibleParents} list.
     * <li>Exploded nodes. These are nodes that were previously marked as invisible, and
     * subsequently rendered by a recomputed layout. They now no longer have empty bounds,
     * but should be specially marked via {@link CanvasViewInfo#setExploded} such that we
     * for example in selection operations can determine if we need to recompute the
     * layout.
     * </ol>
     *
     * @param vi
     * @param invisibleNodes
     */
    private void addInvisibleParents(CanvasViewInfo vi, Set<UiElementNode> invisibleNodes) {
        if (vi == null) {
            return;
        }

        if (vi.isInvisible()) {
            mInvisibleParents.add(vi);
        } else if (invisibleNodes != null) {
            UiViewElementNode key = vi.getUiViewNode();

            if (key != null && invisibleNodes.contains(key)) {
                vi.setExploded(true);
                mExplodedParents = true;
                mInvisibleParents.add(vi);
            }
        }

        for (CanvasViewInfo child : vi.getChildren()) {
            addInvisibleParents(child, invisibleNodes);
        }
    }

    /**
     * Returns the current {@link RenderSession}.
     * @return the session or null if none have been set.
     */
    public RenderSession getSession() {
        return mSession;
    }

    /**
     * Returns true when the last {@link #setSession} provided a valid
     * {@link RenderSession}.
     * <p/>
     * When false this means the canvas is displaying an out-dated result image & bounds and some
     * features should be disabled accordingly such a drag'n'drop.
     * <p/>
     * Note that an empty document (with a null {@link #getRoot()}) is considered
     * valid since it is an acceptable drop target.
     * @return True when this {@link ViewHierarchy} contains a valid hierarchy of views.
    */
    public boolean isValid() {
        return mIsResultValid;
    }

    /**
     * Returns true if the last valid content of the canvas represents an empty document.
     * @return True if the last valid content of the canvas represents an empty document.
     */
    public boolean isEmpty() {
        return mLastValidViewInfoRoot == null;
    }

    /**
     * Returns true if we have parents in this hierarchy that are invisible (e.g. because
     * they have no children and zero layout bounds).
     *
     * @return True if we have invisible parents.
     */
    public boolean hasInvisibleParents() {
        return mInvisibleParents.size() > 0;
    }

    /**
     * Returns true if we have views that were exploded during rendering
     * @return True if we have exploded parents
     */
    public boolean hasExplodedParents() {
        return mExplodedParents;
    }

    /** Locates and return any views that overlap the given selection rectangle.
     * @param topLeft The top left corner of the selection rectangle.
     * @param bottomRight The bottom right corner of the selection rectangle.
     * @return A collection of {@link CanvasViewInfo} objects that overlap the
     *   rectangle.
     */
    public Collection<CanvasViewInfo> findWithin(
            LayoutPoint topLeft,
            LayoutPoint bottomRight) {
        Rectangle selectionRectangle = new Rectangle(topLeft.x, topLeft.y, bottomRight.x
                - topLeft.x, bottomRight.y - topLeft.y);
        List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
        addWithin(mLastValidViewInfoRoot, selectionRectangle, infos);
        return infos;
    }

    /**
     * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
     * <p/>
     * Tries to find the inner most child matching the given x,y coordinates in the view
     * info sub-tree. This uses the potentially-expanded selection bounds.
     *
     * Returns null if not found.
     */
    private void addWithin(
            CanvasViewInfo canvasViewInfo,
            Rectangle canvasRectangle,
            List<CanvasViewInfo> infos) {
        if (canvasViewInfo == null) {
            return;
        }
        Rectangle r = canvasViewInfo.getSelectionRect();
        if (canvasRectangle.intersects(r)) {

            // try to find a matching child first
            for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
                addWithin(child, canvasRectangle, infos);
            }

            if (canvasViewInfo != mLastValidViewInfoRoot) {
                infos.add(canvasViewInfo);
            }
        }
    }

    /**
     * Locates and returns the {@link CanvasViewInfo} corresponding to the given
     * node, or null if it cannot be found.
     *
     * @param node The node we want to find a corresponding
     *            {@link CanvasViewInfo} for.
     * @return The {@link CanvasViewInfo} corresponding to the given node, or
     *         null if no match was found.
     */
    @Nullable
    public CanvasViewInfo findViewInfoFor(@Nullable Node node) {
        CanvasViewInfo vi = mDomNodeToView.get(node);

        if (vi == null) {
            if (node == null) {
                return null;
            } else if (node.getNodeType() == Node.TEXT_NODE) {
                return mDomNodeToView.get(node.getParentNode());
            } else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
                return mDomNodeToView.get(((Attr) node).getOwnerElement());
            } else if (node.getNodeType() == Node.DOCUMENT_NODE) {
                return mDomNodeToView.get(((Document) node).getDocumentElement());
            }
        }

        return vi;
    }

    /**
     * Tries to find the inner most child matching the given x,y coordinates in
     * the view info sub-tree, starting at the last know view info root. This
     * uses the potentially-expanded selection bounds.
     * <p/>
     * Returns null if not found or if there's no view info root.
     *
     * @param p The point at which to look for the deepest match in the view
     *            hierarchy
     * @return A {@link CanvasViewInfo} that intersects the given point, or null
     *         if nothing was found.
     */
    public CanvasViewInfo findViewInfoAt(LayoutPoint p) {
        if (mLastValidViewInfoRoot == null) {
            return null;
        }

        return findViewInfoAt_Recursive(p, mLastValidViewInfoRoot);
    }

    /**
     * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
     * <p/>
     * Tries to find the inner most child matching the given x,y coordinates in the view
     * info sub-tree. This uses the potentially-expanded selection bounds.
     *
     * Returns null if not found.
     */
    private CanvasViewInfo findViewInfoAt_Recursive(LayoutPoint p, CanvasViewInfo canvasViewInfo) {
        if (canvasViewInfo == null) {
            return null;
        }
        Rectangle r = canvasViewInfo.getSelectionRect();
        if (r.contains(p.x, p.y)) {

            // try to find a matching child first
            // Iterate in REVERSE z order such that siblings on top
            // are checked before earlier siblings (this matters in layouts like
            // FrameLayout and in <merge> contexts where the views are sitting on top
            // of each other and we want to select the same view as the one drawn
            // on top of the others
            List<CanvasViewInfo> children = canvasViewInfo.getChildren();
            assert children instanceof RandomAccess;
            for (int i = children.size() - 1; i >= 0; i--) {
                CanvasViewInfo child = children.get(i);
                CanvasViewInfo v = findViewInfoAt_Recursive(p, child);
                if (v != null) {
                    return v;
                }
            }

            // if no children matched, this is the view that we're looking for
            return canvasViewInfo;
        }

        return null;
    }

    /**
     * Returns a list of all the possible alternatives for a given view at the given
     * position. This is used to build and manage the "alternate" selection that cycles
     * around the parents or children of the currently selected element.
     */
    /* package */ List<CanvasViewInfo> findAltViewInfoAt(LayoutPoint p) {
        if (mLastValidViewInfoRoot != null) {
            return findAltViewInfoAt_Recursive(p, mLastValidViewInfoRoot, null);
        }

        return null;
    }

    /**
     * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
     * Please don't use directly.
     */
    private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
            LayoutPoint p, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
        Rectangle r;

        if (outList == null) {
            outList = new ArrayList<CanvasViewInfo>();

            if (parent != null) {
                // add the parent root only once
                r = parent.getSelectionRect();
                if (r.contains(p.x, p.y)) {
                    outList.add(parent);
                }
            }
        }

        if (parent != null && !parent.getChildren().isEmpty()) {
            // then add all children that match the position
            for (CanvasViewInfo child : parent.getChildren()) {
                r = child.getSelectionRect();
                if (r.contains(p.x, p.y)) {
                    outList.add(child);
                }
            }

            // finally recurse in the children
            for (CanvasViewInfo child : parent.getChildren()) {
                r = child.getSelectionRect();
                if (r.contains(p.x, p.y)) {
                    findAltViewInfoAt_Recursive(p, child, outList);
                }
            }
        }

        return outList;
    }

    /**
     * Locates and returns the {@link CanvasViewInfo} corresponding to the given
     * node, or null if it cannot be found.
     *
     * @param node The node we want to find a corresponding
     *            {@link CanvasViewInfo} for.
     * @return The {@link CanvasViewInfo} corresponding to the given node, or
     *         null if no match was found.
     */
    public CanvasViewInfo findViewInfoFor(INode node) {
        return findViewInfoFor((NodeProxy) node);
    }

    /**
     * Tries to find a child with the same view key in the view info sub-tree.
     * Returns null if not found.
     *
     * @param viewKey The view key that a matching {@link CanvasViewInfo} should
     *            have as its key.
     * @return A {@link CanvasViewInfo} matching the given key, or null if not
     *         found.
     */
    public CanvasViewInfo findViewInfoFor(UiElementNode viewKey) {
        return mNodeToView.get(viewKey);
    }

    /**
     * Tries to find a child with the given node proxy as the view key.
     * Returns null if not found.
     *
     * @param proxy The view key that a matching {@link CanvasViewInfo} should
     *            have as its key.
     * @return A {@link CanvasViewInfo} matching the given key, or null if not
     *         found.
     */
    @Nullable
    public CanvasViewInfo findViewInfoFor(@Nullable NodeProxy proxy) {
        if (proxy == null) {
            return null;
        }
        return mNodeToView.get(proxy.getNode());
    }

    /**
     * Returns a list of ALL ViewInfos (possibly excluding the root, depending
     * on the parameter for that).
     *
     * @param includeRoot If true, include the root in the list, otherwise
     *            exclude it (but include all its children)
     * @return A list of canvas view infos.
     */
    public List<CanvasViewInfo> findAllViewInfos(boolean includeRoot) {
        List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
        if (mIsResultValid && mLastValidViewInfoRoot != null) {
            findAllViewInfos(infos, mLastValidViewInfoRoot, includeRoot);
        }

        return infos;
    }

    private void findAllViewInfos(List<CanvasViewInfo> result, CanvasViewInfo canvasViewInfo,
            boolean includeRoot) {
        if (canvasViewInfo != null) {
            if (includeRoot || !canvasViewInfo.isRoot()) {
                result.add(canvasViewInfo);
            }
            for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
                findAllViewInfos(result, child, true);
            }
        }
    }

    /**
     * Returns the root of the view hierarchy, if any (could be null, for example
     * on rendering failure).
     *
     * @return The current view hierarchy, or null
     */
    public CanvasViewInfo getRoot() {
        return mLastValidViewInfoRoot;
    }

    /**
     * Returns a collection of views that have zero bounds and that correspond to empty
     * parents. Note that the views may not actually have zero bounds; in particular, if
     * they are exploded ({@link CanvasViewInfo#isExploded()}, then they will have the
     * bounds of a shown invisible node. Therefore, this method returns the views that
     * would be invisible in a real rendering of the scene.
     *
     * @return A collection of empty parent views.
     */
    public List<CanvasViewInfo> getInvisibleViews() {
        return mInvisibleParentsReadOnly;
    }

    /**
     * Returns the invisible nodes (the {@link UiElementNode} objects corresponding
     * to the {@link CanvasViewInfo} objects returned from {@link #getInvisibleViews()}.
     * We are pulling out the nodes since they preserve their identity across layout
     * rendering, and in particular we return it as a set such that the layout renderer
     * can perform quick identity checks when looking up attribute values during the
     * rendering process.
     *
     * @return A set of the invisible nodes.
     */
    public Set<UiElementNode> getInvisibleNodes() {
        if (mInvisibleParents.size() == 0) {
            return Collections.emptySet();
        }

        Set<UiElementNode> nodes = new HashSet<UiElementNode>(mInvisibleParents.size());
        for (CanvasViewInfo info : mInvisibleParents) {
            UiViewElementNode node = info.getUiViewNode();
            if (node != null) {
                nodes.add(node);
            }
        }

        return nodes;
    }

    /**
     * Returns the list of bounds for included views in the current view hierarchy. Can be null
     * when there are no included views.
     *
     * @return a list of included view bounds, or null
     */
    public List<Rectangle> getIncludedBounds() {
        return mIncludedBounds;
    }

    /**
     * Returns a map of the default properties for the given view object in this session
     *
     * @param viewObject the object to look up the properties map for
     * @return the map of properties, or null if not found
     */
    @Nullable
    public Map<String, String> getDefaultProperties(@NonNull Object viewObject) {
        if (mSession != null) {
            return mSession.getDefaultProperties(viewObject);
        }

        return null;
    }

    /**
     * Dumps a {@link ViewInfo} hierarchy to stdout
     *
     * @param session the corresponding session, if any
     * @param info the {@link ViewInfo} object to dump
     * @param depth the depth to indent it to
     */
    public static void dump(RenderSession session, ViewInfo info, int depth) {
        if (DUMP_INFO) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < depth; i++) {
                sb.append("    "); //$NON-NLS-1$
            }
            sb.append(info.getClassName());
            sb.append(" ["); //$NON-NLS-1$
            sb.append(info.getLeft());
            sb.append(","); //$NON-NLS-1$
            sb.append(info.getTop());
            sb.append(","); //$NON-NLS-1$
            sb.append(info.getRight());
            sb.append(","); //$NON-NLS-1$
            sb.append(info.getBottom());
            sb.append("]"); //$NON-NLS-1$
            Object cookie = info.getCookie();
            if (cookie instanceof UiViewElementNode) {
                sb.append(" "); //$NON-NLS-1$
                UiViewElementNode node = (UiViewElementNode) cookie;
                sb.append("<"); //$NON-NLS-1$
                sb.append(node.getDescriptor().getXmlName());
                sb.append(">"); //$NON-NLS-1$

                String id = node.getAttributeValue(ATTR_ID);
                if (id != null && !id.isEmpty()) {
                    sb.append(" ");
                    sb.append(id);
                }
            } else if (cookie != null) {
                sb.append(" " + cookie); //$NON-NLS-1$
            }
            /* Display defaults?
            if (info.getViewObject() != null) {
                Map<String, String> defaults = session.getDefaultProperties(info.getCookie());
                sb.append(" - defaults: "); //$NON-NLS-1$
                sb.append(defaults);
                sb.append('\n');
            }
            */

            System.out.println(sb.toString());

            for (ViewInfo child : info.getChildren()) {
                dump(session, child, depth + 1);
            }
        }
    }
}