Home | History | Annotate | Download | only in gle2
      1 /*
      2  * Copyright (C) 2010 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.gle2;
     18 
     19 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE;
     20 
     21 import com.android.ide.common.api.INode;
     22 import com.android.ide.common.rendering.api.RenderSession;
     23 import com.android.ide.common.rendering.api.ViewInfo;
     24 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
     25 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     26 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
     27 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     28 import com.android.util.Pair;
     29 
     30 import org.eclipse.swt.graphics.Rectangle;
     31 import org.w3c.dom.Node;
     32 
     33 import java.awt.image.BufferedImage;
     34 import java.util.ArrayList;
     35 import java.util.Collection;
     36 import java.util.Collections;
     37 import java.util.HashMap;
     38 import java.util.HashSet;
     39 import java.util.List;
     40 import java.util.Map;
     41 import java.util.RandomAccess;
     42 import java.util.Set;
     43 
     44 /**
     45  * The view hierarchy class manages a set of view info objects and performs find
     46  * operations on this set.
     47  */
     48 public class ViewHierarchy {
     49     private static final boolean DUMP_INFO = false;
     50 
     51     private LayoutCanvas mCanvas;
     52 
     53     /**
     54      * Constructs a new {@link ViewHierarchy} tied to the given
     55      * {@link LayoutCanvas}.
     56      *
     57      * @param canvas The {@link LayoutCanvas} to create a {@link ViewHierarchy}
     58      *            for.
     59      */
     60     public ViewHierarchy(LayoutCanvas canvas) {
     61         this.mCanvas = canvas;
     62     }
     63 
     64     /**
     65      * The CanvasViewInfo root created by the last call to {@link #setSession}
     66      * with a valid layout.
     67      * <p/>
     68      * This <em>can</em> be null to indicate we're dealing with an empty document with
     69      * no root node. Null here does not mean the result was invalid, merely that the XML
     70      * had no content to display -- we need to treat an empty document as valid so that
     71      * we can drop new items in it.
     72      */
     73     private CanvasViewInfo mLastValidViewInfoRoot;
     74 
     75     /**
     76      * True when the last {@link #setSession} provided a valid {@link LayoutScene}.
     77      * <p/>
     78      * When false this means the canvas is displaying an out-dated result image & bounds and some
     79      * features should be disabled accordingly such a drag'n'drop.
     80      * <p/>
     81      * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
     82      * valid since it is an acceptable drop target.
     83      */
     84     private boolean mIsResultValid;
     85 
     86     /**
     87      * A list of invisible parents (see {@link CanvasViewInfo#isInvisible()} for
     88      * details) in the current view hierarchy.
     89      */
     90     private final List<CanvasViewInfo> mInvisibleParents = new ArrayList<CanvasViewInfo>();
     91 
     92     /**
     93      * A read-only view of {@link #mInvisibleParents}; note that this is NOT a copy so it
     94      * reflects updates to the underlying {@link #mInvisibleParents} list.
     95      */
     96     private final List<CanvasViewInfo> mInvisibleParentsReadOnly =
     97         Collections.unmodifiableList(mInvisibleParents);
     98 
     99     /**
    100      * Flag which records whether or not we have any exploded parent nodes in this
    101      * view hierarchy. This is used to track whether or not we need to recompute the
    102      * layout when we exit show-all-invisible-parents mode (see
    103      * {@link LayoutCanvas#showInvisibleViews}).
    104      */
    105     private boolean mExplodedParents;
    106 
    107     /**
    108      * Bounds of included views in the current view hierarchy when rendered in other context
    109      */
    110     private List<Rectangle> mIncludedBounds;
    111 
    112     /** The render session for the current view hierarchy */
    113     private RenderSession mSession;
    114 
    115     /** Map from nodes to canvas view infos */
    116     private Map<UiViewElementNode, CanvasViewInfo> mNodeToView = Collections.emptyMap();
    117 
    118     /**
    119      * Disposes the view hierarchy content.
    120      */
    121     public void dispose() {
    122         if (mSession != null) {
    123             mSession.dispose();
    124             mSession = null;
    125         }
    126     }
    127 
    128 
    129     /**
    130      * Sets the result of the layout rendering. The result object indicates if the layout
    131      * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
    132      *
    133      * Implementation detail: the bridge's computeLayout() method already returns a newly
    134      * allocated ILayourResult. That means we can keep this result and hold on to it
    135      * when it is valid.
    136      *
    137      * @param session The new session, either valid or not.
    138      * @param explodedNodes The set of individual nodes the layout computer was asked to
    139      *            explode. Note that these are independent of the explode-all mode where
    140      *            all views are exploded; this is used only for the mode (
    141      *            {@link LayoutCanvas#showInvisibleViews}) where individual invisible
    142      *            nodes are padded during certain interactions.
    143      */
    144     /* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes,
    145             boolean layoutlib5) {
    146         // replace the previous scene, so the previous scene must be disposed.
    147         if (mSession != null) {
    148             mSession.dispose();
    149         }
    150 
    151         mSession = session;
    152         mIsResultValid = (session != null && session.getResult().isSuccess());
    153         mExplodedParents = false;
    154         mNodeToView = new HashMap<UiViewElementNode, CanvasViewInfo>(50);
    155         if (mIsResultValid && session != null) {
    156             List<ViewInfo> rootList = session.getRootViews();
    157 
    158             Pair<CanvasViewInfo,List<Rectangle>> infos = null;
    159 
    160             if (rootList == null || rootList.size() == 0) {
    161                 // Special case: Look to see if this is really an empty <merge> view,
    162                 // which shows up without any ViewInfos in the merge. In that case we
    163                 // want to manufacture an empty view, such that we can target the view
    164                 // via drag & drop, etc.
    165                 if (hasMergeRoot()) {
    166                     ViewInfo mergeRoot = createMergeInfo(session);
    167                     infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
    168                 } else {
    169                     infos = null;
    170                 }
    171             } else {
    172                 if (rootList.size() > 1 && hasMergeRoot()) {
    173                     ViewInfo mergeRoot = createMergeInfo(session);
    174                     mergeRoot.setChildren(rootList);
    175                     infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
    176                 } else {
    177                     ViewInfo root = rootList.get(0);
    178 
    179                     if (root != null) {
    180                         infos = CanvasViewInfo.create(root, layoutlib5);
    181                         if (DUMP_INFO) {
    182                             dump(root, 0);
    183                         }
    184                     } else {
    185                         infos = null;
    186                     }
    187                 }
    188             }
    189             if (infos != null) {
    190                 mLastValidViewInfoRoot = infos.getFirst();
    191                 mIncludedBounds = infos.getSecond();
    192 
    193                 if (mLastValidViewInfoRoot.getUiViewNode() == null &&
    194                         mLastValidViewInfoRoot.getChildren().isEmpty()) {
    195                     GraphicalEditorPart editor = mCanvas.getLayoutEditor().getGraphicalEditor();
    196                     if (editor.getIncludedWithin() != null) {
    197                         // Somehow, this view was supposed to be rendered within another
    198                         // view, yet this view was rendered as part of the other view.
    199                         // In that case, abort attempting to show included in; clear the
    200                         // include context and trigger a standalone re-render.
    201                         editor.showIn(null);
    202                         return;
    203                     }
    204                 }
    205 
    206             } else {
    207                 mLastValidViewInfoRoot = null;
    208                 mIncludedBounds = null;
    209             }
    210 
    211             updateNodeProxies(mLastValidViewInfoRoot);
    212 
    213             // Update the data structures related to tracking invisible and exploded nodes.
    214             // We need to find the {@link CanvasViewInfo} objects that correspond to
    215             // the passed in {@link UiElementNode} keys that were re-rendered, and mark
    216             // them as exploded and store them in a list for rendering.
    217             mExplodedParents = false;
    218             mInvisibleParents.clear();
    219             addInvisibleParents(mLastValidViewInfoRoot, explodedNodes);
    220 
    221             // Update the selection
    222             mCanvas.getSelectionManager().sync();
    223         } else {
    224             mIncludedBounds = null;
    225             mInvisibleParents.clear();
    226         }
    227     }
    228 
    229     private ViewInfo createMergeInfo(RenderSession session) {
    230         BufferedImage image = session.getImage();
    231         ControlPoint imageSize = ControlPoint.create(mCanvas,
    232                 mCanvas.getHorizontalTransform().getMargin() + image.getWidth(),
    233                 mCanvas.getVerticalTransform().getMargin() + image.getHeight());
    234         LayoutPoint layoutSize = imageSize.toLayout();
    235         UiDocumentNode model = mCanvas.getLayoutEditor().getUiRootNode();
    236         List<UiElementNode> children = model.getUiChildren();
    237         return new ViewInfo(VIEW_MERGE, children.get(0), 0, 0, layoutSize.x, layoutSize.y);
    238     }
    239 
    240     /**
    241      * Returns true if this view hierarchy corresponds to an editor that has a {@code
    242      * <merge>} tag at the root
    243      *
    244      * @return true if there is a {@code <merge>} at the root of this editor's document
    245      */
    246     private boolean hasMergeRoot() {
    247         UiDocumentNode model = mCanvas.getLayoutEditor().getUiRootNode();
    248         if (model != null) {
    249             List<UiElementNode> children = model.getUiChildren();
    250             if (children != null && children.size() > 0
    251                     && VIEW_MERGE.equals(children.get(0).getDescriptor().getXmlName())) {
    252                 return true;
    253             }
    254         }
    255 
    256         return false;
    257     }
    258 
    259     /**
    260      * Creates or updates the node proxy for this canvas view info.
    261      * <p/>
    262      * Since proxies are reused, this will update the bounds of an existing proxy when the
    263      * canvas is refreshed and a view changes position or size.
    264      * <p/>
    265      * This is a recursive call that updates the whole hierarchy starting at the given
    266      * view info.
    267      */
    268     private void updateNodeProxies(CanvasViewInfo vi) {
    269         if (vi == null) {
    270             return;
    271         }
    272 
    273         UiViewElementNode key = vi.getUiViewNode();
    274 
    275         if (key != null) {
    276             mCanvas.getNodeFactory().create(vi);
    277             mNodeToView.put(key, vi);
    278         }
    279 
    280         for (CanvasViewInfo child : vi.getChildren()) {
    281             updateNodeProxies(child);
    282         }
    283     }
    284 
    285     /**
    286      * Make a pass over the view hierarchy and look for two things:
    287      * <ol>
    288      * <li>Invisible parents. These are nodes that can hold children and have empty
    289      * bounds. These are then added to the {@link #mInvisibleParents} list.
    290      * <li>Exploded nodes. These are nodes that were previously marked as invisible, and
    291      * subsequently rendered by a recomputed layout. They now no longer have empty bounds,
    292      * but should be specially marked via {@link CanvasViewInfo#setExploded} such that we
    293      * for example in selection operations can determine if we need to recompute the
    294      * layout.
    295      * </ol>
    296      *
    297      * @param vi
    298      * @param invisibleNodes
    299      */
    300     private void addInvisibleParents(CanvasViewInfo vi, Set<UiElementNode> invisibleNodes) {
    301         if (vi == null) {
    302             return;
    303         }
    304 
    305         if (vi.isInvisible()) {
    306             mInvisibleParents.add(vi);
    307         } else if (invisibleNodes != null) {
    308             UiViewElementNode key = vi.getUiViewNode();
    309 
    310             if (key != null && invisibleNodes.contains(key)) {
    311                 vi.setExploded(true);
    312                 mExplodedParents = true;
    313                 mInvisibleParents.add(vi);
    314             }
    315         }
    316 
    317         for (CanvasViewInfo child : vi.getChildren()) {
    318             addInvisibleParents(child, invisibleNodes);
    319         }
    320     }
    321 
    322     /**
    323      * Returns the current {@link RenderSession}.
    324      * @return the session or null if none have been set.
    325      */
    326     public RenderSession getSession() {
    327         return mSession;
    328     }
    329 
    330     /**
    331      * Returns true when the last {@link #setSession} provided a valid
    332      * {@link RenderSession}.
    333      * <p/>
    334      * When false this means the canvas is displaying an out-dated result image & bounds and some
    335      * features should be disabled accordingly such a drag'n'drop.
    336      * <p/>
    337      * Note that an empty document (with a null {@link #getRoot()}) is considered
    338      * valid since it is an acceptable drop target.
    339      * @return True when this {@link ViewHierarchy} contains a valid hierarchy of views.
    340     */
    341     public boolean isValid() {
    342         return mIsResultValid;
    343     }
    344 
    345     /**
    346      * Returns true if the last valid content of the canvas represents an empty document.
    347      * @return True if the last valid content of the canvas represents an empty document.
    348      */
    349     public boolean isEmpty() {
    350         return mLastValidViewInfoRoot == null;
    351     }
    352 
    353     /**
    354      * Returns true if we have parents in this hierarchy that are invisible (e.g. because
    355      * they have no children and zero layout bounds).
    356      *
    357      * @return True if we have invisible parents.
    358      */
    359     public boolean hasInvisibleParents() {
    360         return mInvisibleParents.size() > 0;
    361     }
    362 
    363     /**
    364      * Returns true if we have views that were exploded during rendering
    365      * @return True if we have exploded parents
    366      */
    367     public boolean hasExplodedParents() {
    368         return mExplodedParents;
    369     }
    370 
    371     /** Locates and return any views that overlap the given selection rectangle.
    372      * @param topLeft The top left corner of the selection rectangle.
    373      * @param bottomRight The bottom right corner of the selection rectangle.
    374      * @return A collection of {@link CanvasViewInfo} objects that overlap the
    375      *   rectangle.
    376      */
    377     public Collection<CanvasViewInfo> findWithin(
    378             LayoutPoint topLeft,
    379             LayoutPoint bottomRight) {
    380         Rectangle selectionRectangle = new Rectangle(topLeft.x, topLeft.y, bottomRight.x
    381                 - topLeft.x, bottomRight.y - topLeft.y);
    382         List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
    383         addWithin(mLastValidViewInfoRoot, selectionRectangle, infos);
    384         return infos;
    385     }
    386 
    387     /**
    388      * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
    389      * <p/>
    390      * Tries to find the inner most child matching the given x,y coordinates in the view
    391      * info sub-tree. This uses the potentially-expanded selection bounds.
    392      *
    393      * Returns null if not found.
    394      */
    395     private void addWithin(
    396             CanvasViewInfo canvasViewInfo,
    397             Rectangle canvasRectangle,
    398             List<CanvasViewInfo> infos) {
    399         if (canvasViewInfo == null) {
    400             return;
    401         }
    402         Rectangle r = canvasViewInfo.getSelectionRect();
    403         if (canvasRectangle.intersects(r)) {
    404 
    405             // try to find a matching child first
    406             for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
    407                 addWithin(child, canvasRectangle, infos);
    408             }
    409 
    410             if (canvasViewInfo != mLastValidViewInfoRoot) {
    411                 infos.add(canvasViewInfo);
    412             }
    413         }
    414     }
    415 
    416     /**
    417      * Locates and returns the {@link CanvasViewInfo} corresponding to the given
    418      * node, or null if it cannot be found.
    419      *
    420      * @param node The node we want to find a corresponding
    421      *            {@link CanvasViewInfo} for.
    422      * @return The {@link CanvasViewInfo} corresponding to the given node, or
    423      *         null if no match was found.
    424      */
    425     public CanvasViewInfo findViewInfoFor(Node node) {
    426         if (mLastValidViewInfoRoot != null) {
    427             return findViewInfoForNode(node, mLastValidViewInfoRoot);
    428         }
    429         return null;
    430     }
    431 
    432     /**
    433      * Tries to find a child with the same view XML node in the view info sub-tree.
    434      * Returns null if not found.
    435      */
    436     private CanvasViewInfo findViewInfoForNode(Node xmlNode, CanvasViewInfo canvasViewInfo) {
    437         if (canvasViewInfo == null) {
    438             return null;
    439         }
    440         if (canvasViewInfo.getXmlNode() == xmlNode) {
    441             return canvasViewInfo;
    442         }
    443 
    444         // Try to find a matching child
    445         for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
    446             CanvasViewInfo v = findViewInfoForNode(xmlNode, child);
    447             if (v != null) {
    448                 return v;
    449             }
    450         }
    451 
    452         return null;
    453     }
    454 
    455 
    456     /**
    457      * Tries to find the inner most child matching the given x,y coordinates in
    458      * the view info sub-tree, starting at the last know view info root. This
    459      * uses the potentially-expanded selection bounds.
    460      * <p/>
    461      * Returns null if not found or if there's no view info root.
    462      *
    463      * @param p The point at which to look for the deepest match in the view
    464      *            hierarchy
    465      * @return A {@link CanvasViewInfo} that intersects the given point, or null
    466      *         if nothing was found.
    467      */
    468     public CanvasViewInfo findViewInfoAt(LayoutPoint p) {
    469         if (mLastValidViewInfoRoot == null) {
    470             return null;
    471         }
    472 
    473         return findViewInfoAt_Recursive(p, mLastValidViewInfoRoot);
    474     }
    475 
    476     /**
    477      * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
    478      * <p/>
    479      * Tries to find the inner most child matching the given x,y coordinates in the view
    480      * info sub-tree. This uses the potentially-expanded selection bounds.
    481      *
    482      * Returns null if not found.
    483      */
    484     private CanvasViewInfo findViewInfoAt_Recursive(LayoutPoint p, CanvasViewInfo canvasViewInfo) {
    485         if (canvasViewInfo == null) {
    486             return null;
    487         }
    488         Rectangle r = canvasViewInfo.getSelectionRect();
    489         if (r.contains(p.x, p.y)) {
    490 
    491             // try to find a matching child first
    492             // Iterate in REVERSE z order such that siblings on top
    493             // are checked before earlier siblings (this matters in layouts like
    494             // FrameLayout and in <merge> contexts where the views are sitting on top
    495             // of each other and we want to select the same view as the one drawn
    496             // on top of the others
    497             List<CanvasViewInfo> children = canvasViewInfo.getChildren();
    498             assert children instanceof RandomAccess;
    499             for (int i = children.size() - 1; i >= 0; i--) {
    500                 CanvasViewInfo child = children.get(i);
    501                 CanvasViewInfo v = findViewInfoAt_Recursive(p, child);
    502                 if (v != null) {
    503                     return v;
    504                 }
    505             }
    506 
    507             // if no children matched, this is the view that we're looking for
    508             return canvasViewInfo;
    509         }
    510 
    511         return null;
    512     }
    513 
    514     /**
    515      * Returns a list of all the possible alternatives for a given view at the given
    516      * position. This is used to build and manage the "alternate" selection that cycles
    517      * around the parents or children of the currently selected element.
    518      */
    519     /* package */ List<CanvasViewInfo> findAltViewInfoAt(LayoutPoint p) {
    520         if (mLastValidViewInfoRoot != null) {
    521             return findAltViewInfoAt_Recursive(p, mLastValidViewInfoRoot, null);
    522         }
    523 
    524         return null;
    525     }
    526 
    527     /**
    528      * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
    529      * Please don't use directly.
    530      */
    531     private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
    532             LayoutPoint p, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
    533         Rectangle r;
    534 
    535         if (outList == null) {
    536             outList = new ArrayList<CanvasViewInfo>();
    537 
    538             if (parent != null) {
    539                 // add the parent root only once
    540                 r = parent.getSelectionRect();
    541                 if (r.contains(p.x, p.y)) {
    542                     outList.add(parent);
    543                 }
    544             }
    545         }
    546 
    547         if (parent != null && !parent.getChildren().isEmpty()) {
    548             // then add all children that match the position
    549             for (CanvasViewInfo child : parent.getChildren()) {
    550                 r = child.getSelectionRect();
    551                 if (r.contains(p.x, p.y)) {
    552                     outList.add(child);
    553                 }
    554             }
    555 
    556             // finally recurse in the children
    557             for (CanvasViewInfo child : parent.getChildren()) {
    558                 r = child.getSelectionRect();
    559                 if (r.contains(p.x, p.y)) {
    560                     findAltViewInfoAt_Recursive(p, child, outList);
    561                 }
    562             }
    563         }
    564 
    565         return outList;
    566     }
    567 
    568     /**
    569      * Locates and returns the {@link CanvasViewInfo} corresponding to the given
    570      * node, or null if it cannot be found.
    571      *
    572      * @param node The node we want to find a corresponding
    573      *            {@link CanvasViewInfo} for.
    574      * @return The {@link CanvasViewInfo} corresponding to the given node, or
    575      *         null if no match was found.
    576      */
    577     public CanvasViewInfo findViewInfoFor(INode node) {
    578         return findViewInfoFor((NodeProxy) node);
    579     }
    580 
    581     /**
    582      * Tries to find a child with the same view key in the view info sub-tree.
    583      * Returns null if not found.
    584      *
    585      * @param viewKey The view key that a matching {@link CanvasViewInfo} should
    586      *            have as its key.
    587      * @return A {@link CanvasViewInfo} matching the given key, or null if not
    588      *         found.
    589      */
    590     public CanvasViewInfo findViewInfoFor(UiElementNode viewKey) {
    591         return mNodeToView.get(viewKey);
    592     }
    593 
    594     /**
    595      * Tries to find a child with the given node proxy as the view key.
    596      * Returns null if not found.
    597      *
    598      * @param proxy The view key that a matching {@link CanvasViewInfo} should
    599      *            have as its key.
    600      * @return A {@link CanvasViewInfo} matching the given key, or null if not
    601      *         found.
    602      */
    603     public CanvasViewInfo findViewInfoFor(NodeProxy proxy) {
    604         return mNodeToView.get(proxy.getNode());
    605     }
    606 
    607     /**
    608      * Returns a list of ALL ViewInfos (possibly excluding the root, depending
    609      * on the parameter for that).
    610      *
    611      * @param includeRoot If true, include the root in the list, otherwise
    612      *            exclude it (but include all its children)
    613      * @return A list of canvas view infos.
    614      */
    615     public List<CanvasViewInfo> findAllViewInfos(boolean includeRoot) {
    616         List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
    617         if (mIsResultValid && mLastValidViewInfoRoot != null) {
    618             findAllViewInfos(infos, mLastValidViewInfoRoot, includeRoot);
    619         }
    620 
    621         return infos;
    622     }
    623 
    624     private void findAllViewInfos(List<CanvasViewInfo> result, CanvasViewInfo canvasViewInfo,
    625             boolean includeRoot) {
    626         if (canvasViewInfo != null) {
    627             if (includeRoot || !canvasViewInfo.isRoot()) {
    628                 result.add(canvasViewInfo);
    629             }
    630             for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
    631                 findAllViewInfos(result, child, true);
    632             }
    633         }
    634     }
    635 
    636     /**
    637      * Returns the root of the view hierarchy, if any (could be null, for example
    638      * on rendering failure).
    639      *
    640      * @return The current view hierarchy, or null
    641      */
    642     public CanvasViewInfo getRoot() {
    643         return mLastValidViewInfoRoot;
    644     }
    645 
    646     /**
    647      * Returns a collection of views that have zero bounds and that correspond to empty
    648      * parents. Note that the views may not actually have zero bounds; in particular, if
    649      * they are exploded ({@link CanvasViewInfo#isExploded()}, then they will have the
    650      * bounds of a shown invisible node. Therefore, this method returns the views that
    651      * would be invisible in a real rendering of the scene.
    652      *
    653      * @return A collection of empty parent views.
    654      */
    655     public List<CanvasViewInfo> getInvisibleViews() {
    656         return mInvisibleParentsReadOnly;
    657     }
    658 
    659     /**
    660      * Returns the invisible nodes (the {@link UiElementNode} objects corresponding
    661      * to the {@link CanvasViewInfo} objects returned from {@link #getInvisibleViews()}.
    662      * We are pulling out the nodes since they preserve their identity across layout
    663      * rendering, and in particular we return it as a set such that the layout renderer
    664      * can perform quick identity checks when looking up attribute values during the
    665      * rendering process.
    666      *
    667      * @return A set of the invisible nodes.
    668      */
    669     public Set<UiElementNode> getInvisibleNodes() {
    670         if (mInvisibleParents.size() == 0) {
    671             return Collections.emptySet();
    672         }
    673 
    674         Set<UiElementNode> nodes = new HashSet<UiElementNode>(mInvisibleParents.size());
    675         for (CanvasViewInfo info : mInvisibleParents) {
    676             UiViewElementNode node = info.getUiViewNode();
    677             if (node != null) {
    678                 nodes.add(node);
    679             }
    680         }
    681 
    682         return nodes;
    683     }
    684 
    685     /**
    686      * Returns the list of bounds for included views in the current view hierarchy. Can be null
    687      * when there are no included views.
    688      *
    689      * @return a list of included view bounds, or null
    690      */
    691     public List<Rectangle> getIncludedBounds() {
    692         return mIncludedBounds;
    693     }
    694 
    695     /**
    696      * Dumps a {@link ViewInfo} hierarchy to stdout
    697      *
    698      * @param info the {@link ViewInfo} object to dump
    699      * @param depth the depth to indent it to
    700      */
    701     public static void dump(ViewInfo info, int depth) {
    702         if (DUMP_INFO) {
    703             StringBuilder sb = new StringBuilder();
    704             for (int i = 0; i < depth; i++) {
    705                 sb.append("    "); //$NON-NLS-1$
    706             }
    707             sb.append(info.getClassName());
    708             sb.append(" ["); //$NON-NLS-1$
    709             sb.append(info.getLeft());
    710             sb.append(","); //$NON-NLS-1$
    711             sb.append(info.getTop());
    712             sb.append(","); //$NON-NLS-1$
    713             sb.append(info.getRight());
    714             sb.append(","); //$NON-NLS-1$
    715             sb.append(info.getBottom());
    716             sb.append("]"); //$NON-NLS-1$
    717             Object cookie = info.getCookie();
    718             if (cookie instanceof UiViewElementNode) {
    719                 sb.append(" "); //$NON-NLS-1$
    720                 UiViewElementNode node = (UiViewElementNode) cookie;
    721                 sb.append("<"); //$NON-NLS-1$
    722                 sb.append(node.getDescriptor().getXmlName());
    723                 sb.append(">"); //$NON-NLS-1$
    724             } else if (cookie != null) {
    725                 sb.append(" " + cookie); //$NON-NLS-1$
    726             }
    727 
    728             System.out.println(sb.toString());
    729 
    730             for (ViewInfo child : info.getChildren()) {
    731                 dump(child, depth + 1);
    732             }
    733         }
    734     }
    735 }
    736