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