aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java771
1 files changed, 771 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
new file mode 100644
index 000000000..d247e28d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
@@ -0,0 +1,771 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.VIEW_MERGE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.utils.Pair;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.RandomAccess;
+import java.util.Set;
+
+/**
+ * The view hierarchy class manages a set of view info objects and performs find
+ * operations on this set.
+ */
+public class ViewHierarchy {
+ private static final boolean DUMP_INFO = false;
+
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs a new {@link ViewHierarchy} tied to the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} to create a {@link ViewHierarchy}
+ * for.
+ */
+ public ViewHierarchy(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ /**
+ * The CanvasViewInfo root created by the last call to {@link #setSession}
+ * with a valid layout.
+ * <p/>
+ * This <em>can</em> be null to indicate we're dealing with an empty document with
+ * no root node. Null here does not mean the result was invalid, merely that the XML
+ * had no content to display -- we need to treat an empty document as valid so that
+ * we can drop new items in it.
+ */
+ private CanvasViewInfo mLastValidViewInfoRoot;
+
+ /**
+ * True when the last {@link #setSession} provided a valid {@link LayoutScene}.
+ * <p/>
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
+ * valid since it is an acceptable drop target.
+ */
+ private boolean mIsResultValid;
+
+ /**
+ * A list of invisible parents (see {@link CanvasViewInfo#isInvisible()} for
+ * details) in the current view hierarchy.
+ */
+ private final List<CanvasViewInfo> mInvisibleParents = new ArrayList<CanvasViewInfo>();
+
+ /**
+ * A read-only view of {@link #mInvisibleParents}; note that this is NOT a copy so it
+ * reflects updates to the underlying {@link #mInvisibleParents} list.
+ */
+ private final List<CanvasViewInfo> mInvisibleParentsReadOnly =
+ Collections.unmodifiableList(mInvisibleParents);
+
+ /**
+ * Flag which records whether or not we have any exploded parent nodes in this
+ * view hierarchy. This is used to track whether or not we need to recompute the
+ * layout when we exit show-all-invisible-parents mode (see
+ * {@link LayoutCanvas#showInvisibleViews}).
+ */
+ private boolean mExplodedParents;
+
+ /**
+ * Bounds of included views in the current view hierarchy when rendered in other context
+ */
+ private List<Rectangle> mIncludedBounds;
+
+ /** The render session for the current view hierarchy */
+ private RenderSession mSession;
+
+ /** Map from nodes to canvas view infos */
+ private Map<UiViewElementNode, CanvasViewInfo> mNodeToView = Collections.emptyMap();
+
+ /** Map from DOM nodes to canvas view infos */
+ private Map<Node, CanvasViewInfo> mDomNodeToView = Collections.emptyMap();
+
+ /**
+ * Disposes the view hierarchy content.
+ */
+ public void dispose() {
+ if (mSession != null) {
+ mSession.dispose();
+ mSession = null;
+ }
+ }
+
+
+ /**
+ * Sets the result of the layout rendering. The result object indicates if the layout
+ * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
+ *
+ * Implementation detail: the bridge's computeLayout() method already returns a newly
+ * allocated ILayourResult. That means we can keep this result and hold on to it
+ * when it is valid.
+ *
+ * @param session The new session, either valid or not.
+ * @param explodedNodes The set of individual nodes the layout computer was asked to
+ * explode. Note that these are independent of the explode-all mode where
+ * all views are exploded; this is used only for the mode (
+ * {@link LayoutCanvas#showInvisibleViews}) where individual invisible
+ * nodes are padded during certain interactions.
+ */
+ /* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes,
+ boolean layoutlib5) {
+ // replace the previous scene, so the previous scene must be disposed.
+ if (mSession != null) {
+ mSession.dispose();
+ }
+
+ mSession = session;
+ mIsResultValid = (session != null && session.getResult().isSuccess());
+ mExplodedParents = false;
+ mNodeToView = new HashMap<UiViewElementNode, CanvasViewInfo>(50);
+ if (mIsResultValid && session != null) {
+ List<ViewInfo> rootList = session.getRootViews();
+
+ Pair<CanvasViewInfo,List<Rectangle>> infos = null;
+
+ if (rootList == null || rootList.size() == 0) {
+ // Special case: Look to see if this is really an empty <merge> view,
+ // which shows up without any ViewInfos in the merge. In that case we
+ // want to manufacture an empty view, such that we can target the view
+ // via drag & drop, etc.
+ if (hasMergeRoot()) {
+ ViewInfo mergeRoot = createMergeInfo(session);
+ infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
+ } else {
+ infos = null;
+ }
+ } else {
+ if (rootList.size() > 1 && hasMergeRoot()) {
+ ViewInfo mergeRoot = createMergeInfo(session);
+ mergeRoot.setChildren(rootList);
+ infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
+ } else {
+ ViewInfo root = rootList.get(0);
+
+ if (root != null) {
+ infos = CanvasViewInfo.create(root, layoutlib5);
+ if (DUMP_INFO) {
+ dump(session, root, 0);
+ }
+ } else {
+ infos = null;
+ }
+ }
+ }
+ if (infos != null) {
+ mLastValidViewInfoRoot = infos.getFirst();
+ mIncludedBounds = infos.getSecond();
+
+ if (mLastValidViewInfoRoot.getUiViewNode() == null &&
+ mLastValidViewInfoRoot.getChildren().isEmpty()) {
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (editor.getIncludedWithin() != null) {
+ // Somehow, this view was supposed to be rendered within another
+ // view, yet this view was rendered as part of the other view.
+ // In that case, abort attempting to show included in; clear the
+ // include context and trigger a standalone re-render.
+ editor.showIn(null);
+ return;
+ }
+ }
+
+ } else {
+ mLastValidViewInfoRoot = null;
+ mIncludedBounds = null;
+ }
+
+ updateNodeProxies(mLastValidViewInfoRoot);
+
+ // Update the data structures related to tracking invisible and exploded nodes.
+ // We need to find the {@link CanvasViewInfo} objects that correspond to
+ // the passed in {@link UiElementNode} keys that were re-rendered, and mark
+ // them as exploded and store them in a list for rendering.
+ mExplodedParents = false;
+ mInvisibleParents.clear();
+ addInvisibleParents(mLastValidViewInfoRoot, explodedNodes);
+
+ mDomNodeToView = new HashMap<Node, CanvasViewInfo>(mNodeToView.size());
+ for (Map.Entry<UiViewElementNode, CanvasViewInfo> entry : mNodeToView.entrySet()) {
+ mDomNodeToView.put(entry.getKey().getXmlNode(), entry.getValue());
+ }
+
+ // Update the selection
+ mCanvas.getSelectionManager().sync();
+ } else {
+ mIncludedBounds = null;
+ mInvisibleParents.clear();
+ mDomNodeToView = Collections.emptyMap();
+ }
+ }
+
+ private ViewInfo createMergeInfo(RenderSession session) {
+ BufferedImage image = session.getImage();
+ ControlPoint imageSize = ControlPoint.create(mCanvas,
+ mCanvas.getHorizontalTransform().getMargin() + image.getWidth(),
+ mCanvas.getVerticalTransform().getMargin() + image.getHeight());
+ LayoutPoint layoutSize = imageSize.toLayout();
+ UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
+ List<UiElementNode> children = model.getUiChildren();
+ return new ViewInfo(VIEW_MERGE, children.get(0), 0, 0, layoutSize.x, layoutSize.y);
+ }
+
+ /**
+ * Returns true if this view hierarchy corresponds to an editor that has a {@code
+ * <merge>} tag at the root
+ *
+ * @return true if there is a {@code <merge>} at the root of this editor's document
+ */
+ private boolean hasMergeRoot() {
+ UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
+ if (model != null) {
+ List<UiElementNode> children = model.getUiChildren();
+ if (children != null && children.size() > 0
+ && VIEW_MERGE.equals(children.get(0).getDescriptor().getXmlName())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates or updates the node proxy for this canvas view info.
+ * <p/>
+ * Since proxies are reused, this will update the bounds of an existing proxy when the
+ * canvas is refreshed and a view changes position or size.
+ * <p/>
+ * This is a recursive call that updates the whole hierarchy starting at the given
+ * view info.
+ */
+ private void updateNodeProxies(CanvasViewInfo vi) {
+ if (vi == null) {
+ return;
+ }
+
+ UiViewElementNode key = vi.getUiViewNode();
+
+ if (key != null) {
+ mCanvas.getNodeFactory().create(vi);
+ mNodeToView.put(key, vi);
+ }
+
+ for (CanvasViewInfo child : vi.getChildren()) {
+ updateNodeProxies(child);
+ }
+ }
+
+ /**
+ * Make a pass over the view hierarchy and look for two things:
+ * <ol>
+ * <li>Invisible parents. These are nodes that can hold children and have empty
+ * bounds. These are then added to the {@link #mInvisibleParents} list.
+ * <li>Exploded nodes. These are nodes that were previously marked as invisible, and
+ * subsequently rendered by a recomputed layout. They now no longer have empty bounds,
+ * but should be specially marked via {@link CanvasViewInfo#setExploded} such that we
+ * for example in selection operations can determine if we need to recompute the
+ * layout.
+ * </ol>
+ *
+ * @param vi
+ * @param invisibleNodes
+ */
+ private void addInvisibleParents(CanvasViewInfo vi, Set<UiElementNode> invisibleNodes) {
+ if (vi == null) {
+ return;
+ }
+
+ if (vi.isInvisible()) {
+ mInvisibleParents.add(vi);
+ } else if (invisibleNodes != null) {
+ UiViewElementNode key = vi.getUiViewNode();
+
+ if (key != null && invisibleNodes.contains(key)) {
+ vi.setExploded(true);
+ mExplodedParents = true;
+ mInvisibleParents.add(vi);
+ }
+ }
+
+ for (CanvasViewInfo child : vi.getChildren()) {
+ addInvisibleParents(child, invisibleNodes);
+ }
+ }
+
+ /**
+ * Returns the current {@link RenderSession}.
+ * @return the session or null if none have been set.
+ */
+ public RenderSession getSession() {
+ return mSession;
+ }
+
+ /**
+ * Returns true when the last {@link #setSession} provided a valid
+ * {@link RenderSession}.
+ * <p/>
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * Note that an empty document (with a null {@link #getRoot()}) is considered
+ * valid since it is an acceptable drop target.
+ * @return True when this {@link ViewHierarchy} contains a valid hierarchy of views.
+ */
+ public boolean isValid() {
+ return mIsResultValid;
+ }
+
+ /**
+ * Returns true if the last valid content of the canvas represents an empty document.
+ * @return True if the last valid content of the canvas represents an empty document.
+ */
+ public boolean isEmpty() {
+ return mLastValidViewInfoRoot == null;
+ }
+
+ /**
+ * Returns true if we have parents in this hierarchy that are invisible (e.g. because
+ * they have no children and zero layout bounds).
+ *
+ * @return True if we have invisible parents.
+ */
+ public boolean hasInvisibleParents() {
+ return mInvisibleParents.size() > 0;
+ }
+
+ /**
+ * Returns true if we have views that were exploded during rendering
+ * @return True if we have exploded parents
+ */
+ public boolean hasExplodedParents() {
+ return mExplodedParents;
+ }
+
+ /** Locates and return any views that overlap the given selection rectangle.
+ * @param topLeft The top left corner of the selection rectangle.
+ * @param bottomRight The bottom right corner of the selection rectangle.
+ * @return A collection of {@link CanvasViewInfo} objects that overlap the
+ * rectangle.
+ */
+ public Collection<CanvasViewInfo> findWithin(
+ LayoutPoint topLeft,
+ LayoutPoint bottomRight) {
+ Rectangle selectionRectangle = new Rectangle(topLeft.x, topLeft.y, bottomRight.x
+ - topLeft.x, bottomRight.y - topLeft.y);
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ addWithin(mLastValidViewInfoRoot, selectionRectangle, infos);
+ return infos;
+ }
+
+ /**
+ * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+ * <p/>
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree. This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found.
+ */
+ private void addWithin(
+ CanvasViewInfo canvasViewInfo,
+ Rectangle canvasRectangle,
+ List<CanvasViewInfo> infos) {
+ if (canvasViewInfo == null) {
+ return;
+ }
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ if (canvasRectangle.intersects(r)) {
+
+ // try to find a matching child first
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ addWithin(child, canvasRectangle, infos);
+ }
+
+ if (canvasViewInfo != mLastValidViewInfoRoot) {
+ infos.add(canvasViewInfo);
+ }
+ }
+ }
+
+ /**
+ * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+ * node, or null if it cannot be found.
+ *
+ * @param node The node we want to find a corresponding
+ * {@link CanvasViewInfo} for.
+ * @return The {@link CanvasViewInfo} corresponding to the given node, or
+ * null if no match was found.
+ */
+ @Nullable
+ public CanvasViewInfo findViewInfoFor(@Nullable Node node) {
+ CanvasViewInfo vi = mDomNodeToView.get(node);
+
+ if (vi == null) {
+ if (node == null) {
+ return null;
+ } else if (node.getNodeType() == Node.TEXT_NODE) {
+ return mDomNodeToView.get(node.getParentNode());
+ } else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
+ return mDomNodeToView.get(((Attr) node).getOwnerElement());
+ } else if (node.getNodeType() == Node.DOCUMENT_NODE) {
+ return mDomNodeToView.get(((Document) node).getDocumentElement());
+ }
+ }
+
+ return vi;
+ }
+
+ /**
+ * Tries to find the inner most child matching the given x,y coordinates in
+ * the view info sub-tree, starting at the last know view info root. This
+ * uses the potentially-expanded selection bounds.
+ * <p/>
+ * Returns null if not found or if there's no view info root.
+ *
+ * @param p The point at which to look for the deepest match in the view
+ * hierarchy
+ * @return A {@link CanvasViewInfo} that intersects the given point, or null
+ * if nothing was found.
+ */
+ public CanvasViewInfo findViewInfoAt(LayoutPoint p) {
+ if (mLastValidViewInfoRoot == null) {
+ return null;
+ }
+
+ return findViewInfoAt_Recursive(p, mLastValidViewInfoRoot);
+ }
+
+ /**
+ * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+ * <p/>
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree. This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found.
+ */
+ private CanvasViewInfo findViewInfoAt_Recursive(LayoutPoint p, CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return null;
+ }
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+
+ // try to find a matching child first
+ // Iterate in REVERSE z order such that siblings on top
+ // are checked before earlier siblings (this matters in layouts like
+ // FrameLayout and in <merge> contexts where the views are sitting on top
+ // of each other and we want to select the same view as the one drawn
+ // on top of the others
+ List<CanvasViewInfo> children = canvasViewInfo.getChildren();
+ assert children instanceof RandomAccess;
+ for (int i = children.size() - 1; i >= 0; i--) {
+ CanvasViewInfo child = children.get(i);
+ CanvasViewInfo v = findViewInfoAt_Recursive(p, child);
+ if (v != null) {
+ return v;
+ }
+ }
+
+ // if no children matched, this is the view that we're looking for
+ return canvasViewInfo;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a list of all the possible alternatives for a given view at the given
+ * position. This is used to build and manage the "alternate" selection that cycles
+ * around the parents or children of the currently selected element.
+ */
+ /* package */ List<CanvasViewInfo> findAltViewInfoAt(LayoutPoint p) {
+ if (mLastValidViewInfoRoot != null) {
+ return findAltViewInfoAt_Recursive(p, mLastValidViewInfoRoot, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
+ * Please don't use directly.
+ */
+ private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
+ LayoutPoint p, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
+ Rectangle r;
+
+ if (outList == null) {
+ outList = new ArrayList<CanvasViewInfo>();
+
+ if (parent != null) {
+ // add the parent root only once
+ r = parent.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ outList.add(parent);
+ }
+ }
+ }
+
+ if (parent != null && !parent.getChildren().isEmpty()) {
+ // then add all children that match the position
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ outList.add(child);
+ }
+ }
+
+ // finally recurse in the children
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ findAltViewInfoAt_Recursive(p, child, outList);
+ }
+ }
+ }
+
+ return outList;
+ }
+
+ /**
+ * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+ * node, or null if it cannot be found.
+ *
+ * @param node The node we want to find a corresponding
+ * {@link CanvasViewInfo} for.
+ * @return The {@link CanvasViewInfo} corresponding to the given node, or
+ * null if no match was found.
+ */
+ public CanvasViewInfo findViewInfoFor(INode node) {
+ return findViewInfoFor((NodeProxy) node);
+ }
+
+ /**
+ * Tries to find a child with the same view key in the view info sub-tree.
+ * Returns null if not found.
+ *
+ * @param viewKey The view key that a matching {@link CanvasViewInfo} should
+ * have as its key.
+ * @return A {@link CanvasViewInfo} matching the given key, or null if not
+ * found.
+ */
+ public CanvasViewInfo findViewInfoFor(UiElementNode viewKey) {
+ return mNodeToView.get(viewKey);
+ }
+
+ /**
+ * Tries to find a child with the given node proxy as the view key.
+ * Returns null if not found.
+ *
+ * @param proxy The view key that a matching {@link CanvasViewInfo} should
+ * have as its key.
+ * @return A {@link CanvasViewInfo} matching the given key, or null if not
+ * found.
+ */
+ @Nullable
+ public CanvasViewInfo findViewInfoFor(@Nullable NodeProxy proxy) {
+ if (proxy == null) {
+ return null;
+ }
+ return mNodeToView.get(proxy.getNode());
+ }
+
+ /**
+ * Returns a list of ALL ViewInfos (possibly excluding the root, depending
+ * on the parameter for that).
+ *
+ * @param includeRoot If true, include the root in the list, otherwise
+ * exclude it (but include all its children)
+ * @return A list of canvas view infos.
+ */
+ public List<CanvasViewInfo> findAllViewInfos(boolean includeRoot) {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ if (mIsResultValid && mLastValidViewInfoRoot != null) {
+ findAllViewInfos(infos, mLastValidViewInfoRoot, includeRoot);
+ }
+
+ return infos;
+ }
+
+ private void findAllViewInfos(List<CanvasViewInfo> result, CanvasViewInfo canvasViewInfo,
+ boolean includeRoot) {
+ if (canvasViewInfo != null) {
+ if (includeRoot || !canvasViewInfo.isRoot()) {
+ result.add(canvasViewInfo);
+ }
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ findAllViewInfos(result, child, true);
+ }
+ }
+ }
+
+ /**
+ * Returns the root of the view hierarchy, if any (could be null, for example
+ * on rendering failure).
+ *
+ * @return The current view hierarchy, or null
+ */
+ public CanvasViewInfo getRoot() {
+ return mLastValidViewInfoRoot;
+ }
+
+ /**
+ * Returns a collection of views that have zero bounds and that correspond to empty
+ * parents. Note that the views may not actually have zero bounds; in particular, if
+ * they are exploded ({@link CanvasViewInfo#isExploded()}, then they will have the
+ * bounds of a shown invisible node. Therefore, this method returns the views that
+ * would be invisible in a real rendering of the scene.
+ *
+ * @return A collection of empty parent views.
+ */
+ public List<CanvasViewInfo> getInvisibleViews() {
+ return mInvisibleParentsReadOnly;
+ }
+
+ /**
+ * Returns the invisible nodes (the {@link UiElementNode} objects corresponding
+ * to the {@link CanvasViewInfo} objects returned from {@link #getInvisibleViews()}.
+ * We are pulling out the nodes since they preserve their identity across layout
+ * rendering, and in particular we return it as a set such that the layout renderer
+ * can perform quick identity checks when looking up attribute values during the
+ * rendering process.
+ *
+ * @return A set of the invisible nodes.
+ */
+ public Set<UiElementNode> getInvisibleNodes() {
+ if (mInvisibleParents.size() == 0) {
+ return Collections.emptySet();
+ }
+
+ Set<UiElementNode> nodes = new HashSet<UiElementNode>(mInvisibleParents.size());
+ for (CanvasViewInfo info : mInvisibleParents) {
+ UiViewElementNode node = info.getUiViewNode();
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+
+ return nodes;
+ }
+
+ /**
+ * Returns the list of bounds for included views in the current view hierarchy. Can be null
+ * when there are no included views.
+ *
+ * @return a list of included view bounds, or null
+ */
+ public List<Rectangle> getIncludedBounds() {
+ return mIncludedBounds;
+ }
+
+ /**
+ * Returns a map of the default properties for the given view object in this session
+ *
+ * @param viewObject the object to look up the properties map for
+ * @return the map of properties, or null if not found
+ */
+ @Nullable
+ public Map<String, String> getDefaultProperties(@NonNull Object viewObject) {
+ if (mSession != null) {
+ return mSession.getDefaultProperties(viewObject);
+ }
+
+ return null;
+ }
+
+ /**
+ * Dumps a {@link ViewInfo} hierarchy to stdout
+ *
+ * @param session the corresponding session, if any
+ * @param info the {@link ViewInfo} object to dump
+ * @param depth the depth to indent it to
+ */
+ public static void dump(RenderSession session, ViewInfo info, int depth) {
+ if (DUMP_INFO) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < depth; i++) {
+ sb.append(" "); //$NON-NLS-1$
+ }
+ sb.append(info.getClassName());
+ sb.append(" ["); //$NON-NLS-1$
+ sb.append(info.getLeft());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getTop());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getRight());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getBottom());
+ sb.append("]"); //$NON-NLS-1$
+ Object cookie = info.getCookie();
+ if (cookie instanceof UiViewElementNode) {
+ sb.append(" "); //$NON-NLS-1$
+ UiViewElementNode node = (UiViewElementNode) cookie;
+ sb.append("<"); //$NON-NLS-1$
+ sb.append(node.getDescriptor().getXmlName());
+ sb.append(">"); //$NON-NLS-1$
+
+ String id = node.getAttributeValue(ATTR_ID);
+ if (id != null && !id.isEmpty()) {
+ sb.append(" ");
+ sb.append(id);
+ }
+ } else if (cookie != null) {
+ sb.append(" " + cookie); //$NON-NLS-1$
+ }
+ /* Display defaults?
+ if (info.getViewObject() != null) {
+ Map<String, String> defaults = session.getDefaultProperties(info.getCookie());
+ sb.append(" - defaults: "); //$NON-NLS-1$
+ sb.append(defaults);
+ sb.append('\n');
+ }
+ */
+
+ System.out.println(sb.toString());
+
+ for (ViewInfo child : info.getChildren()) {
+ dump(session, child, depth + 1);
+ }
+ }
+ }
+}