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