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      */
ViewHierarchy(LayoutCanvas canvas)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      */
dispose()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      */
setSession(RenderSession session, Set<UiElementNode> explodedNodes, boolean layoutlib5)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 
createMergeInfo(RenderSession session)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      */
hasMergeRoot()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      */
updateNodeProxies(CanvasViewInfo vi)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      */
addInvisibleParents(CanvasViewInfo vi, Set<UiElementNode> invisibleNodes)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      */
getSession()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     */
isValid()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      */
isEmpty()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      */
hasInvisibleParents()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      */
hasExplodedParents()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      */
findWithin( LayoutPoint topLeft, LayoutPoint bottomRight)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      */
addWithin( CanvasViewInfo canvasViewInfo, Rectangle canvasRectangle, List<CanvasViewInfo> infos)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
findViewInfoFor(@ullable Node node)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      */
findViewInfoAt(LayoutPoint p)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      */
findViewInfoAt_Recursive(LayoutPoint p, CanvasViewInfo canvasViewInfo)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      */
findAltViewInfoAt(LayoutPoint p)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      */
findAltViewInfoAt_Recursive( LayoutPoint p, CanvasViewInfo parent, List<CanvasViewInfo> outList)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      */
findViewInfoFor(INode node)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      */
findViewInfoFor(UiElementNode viewKey)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
findViewInfoFor(@ullable NodeProxy proxy)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      */
findAllViewInfos(boolean includeRoot)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 
findAllViewInfos(List<CanvasViewInfo> result, CanvasViewInfo canvasViewInfo, boolean includeRoot)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      */
getRoot()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      */
getInvisibleViews()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      */
getInvisibleNodes()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      */
getIncludedBounds()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
getDefaultProperties(@onNull Object viewObject)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      */
dump(RenderSession session, ViewInfo info, int depth)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