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