1 /* 2 * Copyright (C) 2009 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; 18 19 import com.android.layoutlib.api.ILayoutResult; 20 import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; 21 22 import org.eclipse.swt.SWT; 23 import org.eclipse.swt.events.MouseEvent; 24 import org.eclipse.swt.events.MouseListener; 25 import org.eclipse.swt.events.MouseMoveListener; 26 import org.eclipse.swt.events.PaintEvent; 27 import org.eclipse.swt.events.PaintListener; 28 import org.eclipse.swt.graphics.Color; 29 import org.eclipse.swt.graphics.Font; 30 import org.eclipse.swt.graphics.FontMetrics; 31 import org.eclipse.swt.graphics.GC; 32 import org.eclipse.swt.graphics.Image; 33 import org.eclipse.swt.graphics.ImageData; 34 import org.eclipse.swt.graphics.PaletteData; 35 import org.eclipse.swt.graphics.Rectangle; 36 import org.eclipse.swt.widgets.Canvas; 37 import org.eclipse.swt.widgets.Composite; 38 import org.eclipse.swt.widgets.Display; 39 40 import java.awt.image.BufferedImage; 41 import java.awt.image.DataBufferInt; 42 import java.awt.image.Raster; 43 import java.util.ArrayList; 44 import java.util.LinkedList; 45 import java.util.List; 46 import java.util.ListIterator; 47 48 /** 49 * Displays the image rendered by the {@link GraphicalEditorPart} and handles 50 * the interaction with the widgets. 51 * <p/> 52 * 53 * @since GLE2 54 * 55 * TODO list: 56 * - gray on error, keep select but disable d'n'd. 57 * - make sure it is scrollable (Canvas derives from Scrollable, so prolly just setting bounds.) 58 * - handle drop target (from palette). 59 * - handle drag'n'drop (internal, for moving/duplicating). 60 * - handle context menu (depending on selection). 61 * - selection synchronization with the outline (both ways). 62 */ 63 public class LayoutCanvas extends Canvas { 64 65 /** 66 * Margin around the rendered image. Should be enough space to display the layout 67 * width and height pseudo widgets. 68 */ 69 private static final int IMAGE_MARGIN = 5; 70 /** 71 * Minimal size of the selection, in case an empty view or layout is selected. 72 */ 73 private static final int SELECTION_MIN_SIZE = 6; 74 75 private ILayoutResult mLastValidResult; 76 private ViewInfo mLastValidViewInfoRoot; 77 78 /** Current background image. Null when there's no image. */ 79 private Image mImage; 80 81 private final LinkedList<Selection> mSelections = new LinkedList<Selection>(); 82 83 /** Selection border color. Do not dispose, it's a system color. */ 84 private Color mSelectionFgColor; 85 86 /** Selection name font. Do not dispose, it's a system font. */ 87 private Font mSelectionFont; 88 89 /** Pixel height of the font displaying the selection name. Initially set to 0 and only 90 * initialized in onPaint() when we have a GC. */ 91 private int mSelectionFontHeight; 92 93 /** Current hover view info. Null when no mouse hover. */ 94 private ViewInfo mHoverViewInfo; 95 96 /** Current mouse hover border rectangle. Null when there's no mouse hover. */ 97 private Rectangle mHoverRect; 98 99 /** Hover border color. Must be disposed, it's NOT a system color. */ 100 private Color mHoverFgColor; 101 102 private AlternateSelection mAltSelection; 103 104 /** 105 * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult} 106 * in which case it is also available in {@link #mLastValidResult}. 107 * When false this means the canvas is displaying an out-dated result image & bounds and some 108 * features should be disabled accordingly such a drag'n'drop. 109 */ 110 private boolean mIsResultValid; 111 112 LayoutCanvas(Composite parent, int style)113 public LayoutCanvas(Composite parent, int style) { 114 super(parent, style | SWT.DOUBLE_BUFFERED); 115 116 Display d = getDisplay(); 117 mSelectionFgColor = d.getSystemColor(SWT.COLOR_RED); 118 mHoverFgColor = new Color(d, 0xFF, 0x99, 0x00); // orange 119 mSelectionFont = d.getSystemFont(); 120 121 addPaintListener(new PaintListener() { 122 public void paintControl(PaintEvent e) { 123 onPaint(e); 124 } 125 }); 126 127 addMouseMoveListener(new MouseMoveListener() { 128 public void mouseMove(MouseEvent e) { 129 onMouseMove(e); 130 } 131 }); 132 133 addMouseListener(new MouseListener() { 134 public void mouseUp(MouseEvent e) { 135 onMouseUp(e); 136 } 137 138 public void mouseDown(MouseEvent e) { 139 onMouseDown(e); 140 } 141 142 public void mouseDoubleClick(MouseEvent e) { 143 onDoubleClick(e); 144 } 145 }); 146 } 147 148 @Override dispose()149 public void dispose() { 150 super.dispose(); 151 } 152 153 /** 154 * Sets the result of the layout rendering. The result object indicates if the layout 155 * rendering succeeded. If it did, it contains a bitmap and the objects rectangles. 156 * 157 * Implementation detail: the bridge's computeLayout() method already returns a newly 158 * allocated ILayourResult. That means we can keep this result and hold on to it 159 * when it is valid. 160 * 161 * @param result The new rendering result, either valid or not. 162 */ setResult(ILayoutResult result)163 public void setResult(ILayoutResult result) { 164 165 // disable any hover 166 mHoverRect = null; 167 168 mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS); 169 170 if (mIsResultValid && result != null) { 171 mLastValidResult = result; 172 mLastValidViewInfoRoot = new ViewInfo(result.getRootView()); 173 setImage(result.getImage()); 174 175 // Check if the selection is still the same (based on the object keys) 176 // and eventually recompute their bounds. 177 for (ListIterator<Selection> it = mSelections.listIterator(); it.hasNext(); ) { 178 Selection s = it.next(); 179 180 // Check the if the selected object still exists 181 Object key = s.getViewInfo().getKey(); 182 ViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot); 183 184 // Remove the previous selection -- if the selected object still exists 185 // we need to recompute its bounds in case it moved so we'll insert a new one 186 // at the same place. 187 it.remove(); 188 if (vi != null) { 189 it.add(new Selection(vi)); 190 } 191 } 192 193 // remove the current alternate selection views 194 mAltSelection = null; 195 } 196 197 redraw(); 198 } 199 200 //--- 201 202 /** 203 * Sets the image of the last *successful* rendering. 204 * Converts the AWT image into an SWT image. 205 */ setImage(BufferedImage awtImage)206 private void setImage(BufferedImage awtImage) { 207 int width = awtImage.getWidth(); 208 int height = awtImage.getHeight(); 209 210 Raster raster = awtImage.getData(new java.awt.Rectangle(width, height)); 211 int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData(); 212 213 ImageData imageData = new ImageData(width, height, 32, 214 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF)); 215 216 imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0); 217 218 mImage = new Image(getDisplay(), imageData); 219 } 220 221 /** 222 * Paints the canvas in response to paint events. 223 */ onPaint(PaintEvent e)224 private void onPaint(PaintEvent e) { 225 GC gc = e.gc; 226 227 if (mImage != null) { 228 if (!mIsResultValid) { 229 gc.setAlpha(128); 230 } 231 232 gc.drawImage(mImage, IMAGE_MARGIN, IMAGE_MARGIN); 233 234 if (!mIsResultValid) { 235 gc.setAlpha(255); 236 } 237 } 238 239 if (mHoverRect != null) { 240 gc.setForeground(mHoverFgColor); 241 gc.setLineStyle(SWT.LINE_DOT); 242 gc.drawRectangle(mHoverRect); 243 } 244 245 // initialize the selection font height once. We need the GC to do that. 246 if (mSelectionFontHeight == 0) { 247 gc.setFont(mSelectionFont); 248 FontMetrics fm = gc.getFontMetrics(); 249 mSelectionFontHeight = fm.getHeight(); 250 } 251 252 for (Selection s : mSelections) { 253 drawSelection(gc, s); 254 } 255 } 256 drawSelection(GC gc, Selection s)257 private void drawSelection(GC gc, Selection s) { 258 Rectangle r = s.getRect(); 259 260 gc.setForeground(mSelectionFgColor); 261 gc.setLineStyle(SWT.LINE_SOLID); 262 gc.drawRectangle(s.mRect); 263 264 String name = s.getName(); 265 266 if (name != null) { 267 int xs = r.x + 2; 268 int ys = r.y - mSelectionFontHeight; 269 if (ys < 0) { 270 ys = r.y + r.height; 271 } 272 gc.drawString(name, xs, ys, true /*transparent*/); 273 } 274 } 275 276 /** 277 * Hover on top of a known child. 278 */ onMouseMove(MouseEvent e)279 private void onMouseMove(MouseEvent e) { 280 if (mLastValidResult != null) { 281 ViewInfo root = mLastValidViewInfoRoot; 282 ViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN, root); 283 284 // We don't hover on the root since it's not a widget per see and it is always there. 285 if (vi == root) { 286 vi = null; 287 } 288 289 boolean needsUpdate = vi != mHoverViewInfo; 290 mHoverViewInfo = vi; 291 292 if (vi == null) { 293 mHoverRect = null; 294 } else { 295 Rectangle r = vi.getSelectionRect(); 296 mHoverRect = new Rectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, 297 r.width, r.height); 298 } 299 300 if (needsUpdate) { 301 redraw(); 302 } 303 } 304 } 305 onMouseDown(MouseEvent e)306 private void onMouseDown(MouseEvent e) { 307 // pass, not used yet. 308 } 309 310 /** 311 * Performs selection on mouse up (not mouse down). 312 * <p/> 313 * Shift key is used to toggle in multi-selection. 314 * Alt key is used to cycle selection through objects at the same level than the one 315 * pointed at (i.e. click on an object then alt-click to cycle). 316 */ onMouseUp(MouseEvent e)317 private void onMouseUp(MouseEvent e) { 318 if (mLastValidResult != null) { 319 320 boolean isShift = (e.stateMask & SWT.SHIFT) != 0; 321 boolean isAlt = (e.stateMask & SWT.ALT) != 0; 322 323 int x = e.x - IMAGE_MARGIN; 324 int y = e.y - IMAGE_MARGIN; 325 ViewInfo vi = findViewInfoAt(x, y, mLastValidViewInfoRoot); 326 327 if (isShift && !isAlt) { 328 // Case where shift is pressed: pointed object is toggled. 329 330 // reset alternate selection if any 331 mAltSelection = null; 332 333 // If nothing has been found at the cursor, assume it might be a user error 334 // and avoid clearing the existing selection. 335 336 if (vi != null) { 337 // toggle this selection on-off: remove it if already selected 338 if (deselect(vi)) { 339 redraw(); 340 return; 341 } 342 343 // otherwise add it. 344 mSelections.add(new Selection(vi)); 345 redraw(); 346 } 347 348 } else if (isAlt) { 349 // Case where alt is pressed: select or cycle the object pointed at. 350 351 // Note: if shift and alt are pressed, shift is ignored. The alternate selection 352 // mechanism does not reset the current multiple selection unless they intersect. 353 354 // We need to remember the "origin" of the alternate selection, to be 355 // able to continue cycling through it later. If there's no alternate selection, 356 // create one. If there's one but not for the same origin object, create a new 357 // one too. 358 if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) { 359 mAltSelection = new AlternateSelection(vi, findAltViewInfoAt( 360 x, y, mLastValidViewInfoRoot, null)); 361 362 // deselect them all, in case they were partially selected 363 deselectAll(mAltSelection.getAltViews()); 364 365 // select the current one 366 ViewInfo vi2 = mAltSelection.getCurrent(); 367 if (vi2 != null) { 368 mSelections.addFirst(new Selection(vi2)); 369 } 370 } else { 371 // We're trying to cycle through the current alternate selection. 372 // First remove the current object. 373 ViewInfo vi2 = mAltSelection.getCurrent(); 374 deselect(vi2); 375 376 // Now select the next one. 377 vi2 = mAltSelection.getNext(); 378 if (vi2 != null) { 379 mSelections.addFirst(new Selection(vi2)); 380 } 381 } 382 redraw(); 383 384 } else { 385 // Case where no modifier is pressed: either select or reset the selection. 386 387 // reset alternate selection if any 388 mAltSelection = null; 389 390 // reset (multi)selection if any 391 if (mSelections.size() > 0) { 392 if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) { 393 // Selection remains the same, don't touch it. 394 return; 395 } 396 mSelections.clear(); 397 } 398 399 if (vi != null) { 400 mSelections.add(new Selection(vi)); 401 } 402 redraw(); 403 } 404 } 405 } 406 407 /** Deselects a view info. Returns true if the object was actually selected. */ deselect(ViewInfo viewInfo)408 private boolean deselect(ViewInfo viewInfo) { 409 if (viewInfo == null) { 410 return false; 411 } 412 413 for (ListIterator<Selection> it = mSelections.listIterator(); it.hasNext(); ) { 414 Selection s = it.next(); 415 if (viewInfo == s.getViewInfo()) { 416 it.remove(); 417 return true; 418 } 419 } 420 421 return false; 422 } 423 424 /** Deselects multiple view infos, */ deselectAll(List<ViewInfo> viewInfos)425 private void deselectAll(List<ViewInfo> viewInfos) { 426 for (ListIterator<Selection> it = mSelections.listIterator(); it.hasNext(); ) { 427 Selection s = it.next(); 428 if (viewInfos.contains(s.getViewInfo())) { 429 it.remove(); 430 } 431 } 432 } 433 onDoubleClick(MouseEvent e)434 private void onDoubleClick(MouseEvent e) { 435 // pass, not used yet. 436 } 437 438 /** 439 * Tries to find a child with the same view key in the view info sub-tree. 440 * Returns null if not found. 441 */ findViewInfoKey(Object viewKey, ViewInfo viewInfo)442 private ViewInfo findViewInfoKey(Object viewKey, ViewInfo viewInfo) { 443 if (viewInfo.getKey() == viewKey) { 444 return viewInfo; 445 } 446 447 // try to find a matching child 448 for (ViewInfo child : viewInfo.getChildren()) { 449 ViewInfo v = findViewInfoKey(viewKey, child); 450 if (v != null) { 451 return v; 452 } 453 } 454 455 return null; 456 } 457 458 /** 459 * Tries to find the inner most child matching the given x,y coordinates in the view 460 * info sub-tree. This uses the potentially-expanded selection bounds. 461 * 462 * Returns null if not found. 463 */ findViewInfoAt(int x, int y, ViewInfo viewInfo)464 private ViewInfo findViewInfoAt(int x, int y, ViewInfo viewInfo) { 465 Rectangle r = viewInfo.getSelectionRect(); 466 if (r.contains(x, y)) { 467 468 // try to find a matching child first 469 for (ViewInfo child : viewInfo.getChildren()) { 470 ViewInfo v = findViewInfoAt(x, y, child); 471 if (v != null) { 472 return v; 473 } 474 } 475 476 // if no children matched, this is the view that we're looking for 477 return viewInfo; 478 } 479 480 return null; 481 } 482 findAltViewInfoAt( int x, int y, ViewInfo parent, ArrayList<ViewInfo> outList)483 private ArrayList<ViewInfo> findAltViewInfoAt( 484 int x, int y, ViewInfo parent, ArrayList<ViewInfo> outList) { 485 Rectangle r; 486 487 if (outList == null) { 488 outList = new ArrayList<ViewInfo>(); 489 490 // add the parent root only once 491 r = parent.getSelectionRect(); 492 if (r.contains(x, y)) { 493 outList.add(parent); 494 } 495 } 496 497 if (parent.getChildren().size() > 0) { 498 // then add all children that match the position 499 for (ViewInfo child : parent.getChildren()) { 500 r = child.getSelectionRect(); 501 if (r.contains(x, y)) { 502 outList.add(child); 503 } 504 } 505 506 // finally recurse in the children 507 for (ViewInfo child : parent.getChildren()) { 508 r = child.getSelectionRect(); 509 if (r.contains(x, y)) { 510 findAltViewInfoAt(x, y, child, outList); 511 } 512 } 513 } 514 515 return outList; 516 } 517 518 /** 519 * Maps a {@link ILayoutViewInfo} in a structure more adapted to our needs. 520 * The only large difference is that we keep both the original bounds of the view info 521 * and we pre-compute the selection bounds which are absolute to the rendered image (where 522 * as the original bounds are relative to the parent view.) 523 * <p/> 524 * Each view also know its parent, which should be handy later. 525 * <p/> 526 * We can't alter {@link ILayoutViewInfo} as it is part of the LayoutBridge and needs to 527 * have a fixed API. 528 */ 529 private static class ViewInfo { 530 private final Rectangle mRealRect; 531 private final Rectangle mSelectionRect; 532 private final String mName; 533 private final Object mKey; 534 private final ViewInfo mParent; 535 private final ArrayList<ViewInfo> mChildren = new ArrayList<ViewInfo>(); 536 537 /** 538 * Constructs a {@link ViewInfo} hierarchy based on a given {@link ILayoutViewInfo} 539 * hierarchy. This call is recursives and builds a full tree. 540 * 541 * @param viewInfo The root of the {@link ILayoutViewInfo} hierarchy. 542 */ ViewInfo(ILayoutViewInfo viewInfo)543 public ViewInfo(ILayoutViewInfo viewInfo) { 544 this(viewInfo, null /*parent*/, 0 /*parentX*/, 0 /*parentY*/); 545 } 546 ViewInfo(ILayoutViewInfo viewInfo, ViewInfo parent, int parentX, int parentY)547 private ViewInfo(ILayoutViewInfo viewInfo, ViewInfo parent, int parentX, int parentY) { 548 mParent = parent; 549 mKey = viewInfo.getViewKey(); 550 mName = viewInfo.getName(); 551 552 int x = viewInfo.getLeft(); 553 int y = viewInfo.getTop(); 554 int w = viewInfo.getRight() - x; 555 int h = viewInfo.getBottom() - y; 556 557 mRealRect = new Rectangle(x, y, w, h); 558 559 if (parent != null) { 560 x += parentX; 561 y += parentY; 562 } 563 564 if (viewInfo.getChildren() != null) { 565 for (ILayoutViewInfo child : viewInfo.getChildren()) { 566 mChildren.add(new ViewInfo(child, this, x, y)); 567 } 568 } 569 570 // adjust selection bounds for views which are too small to select 571 572 if (w < SELECTION_MIN_SIZE) { 573 int d = (SELECTION_MIN_SIZE - w) / 2; 574 x -= d; 575 w += SELECTION_MIN_SIZE - w; 576 } 577 578 if (h < SELECTION_MIN_SIZE) { 579 int d = (SELECTION_MIN_SIZE - h) / 2; 580 y -= d; 581 h += SELECTION_MIN_SIZE - h; 582 } 583 584 mSelectionRect = new Rectangle(x, y, w - 1, h - 1); 585 } 586 587 /** Returns the original {@link ILayoutResult} bounds, relative to the parent. */ getRealRect()588 public Rectangle getRealRect() { 589 return mRealRect; 590 } 591 592 /* 593 * Returns the absolute selection bounds of the view info as a rectangle. 594 * The selection bounds will always have a size greater or equal to 595 * {@link #SELECTION_MIN_SIZE}. 596 * The width/height is inclusive (i.e. width = right-left-1). 597 * This is in absolute "screen" coordinates (relative to the rendered bitmap). 598 */ getSelectionRect()599 public Rectangle getSelectionRect() { 600 return mSelectionRect; 601 } 602 603 /** 604 * Returns the view key. Could be null, although unlikely. 605 * @see ILayoutViewInfo#getViewKey() 606 */ getKey()607 public Object getKey() { 608 return mKey; 609 } 610 611 /** 612 * Returns the parent {@link ViewInfo}. 613 * It is null for the root and non-null for children. 614 */ getParent()615 public ViewInfo getParent() { 616 return mParent; 617 } 618 619 /** 620 * Returns the list of children of this {@link ViewInfo}. 621 * The list is never null. It can be empty. 622 * By contract, this.getChildren().get(0..n-1).getParent() == this. 623 */ getChildren()624 public ArrayList<ViewInfo> getChildren() { 625 return mChildren; 626 } 627 628 /** 629 * Returns the name of the {@link ViewInfo}. 630 * Could be null, although unlikely. 631 * Experience shows this is the full qualified Java name of the View. 632 * @see ILayoutViewInfo#getName() 633 */ getName()634 public String getName() { 635 return mName; 636 } 637 } 638 639 /** 640 * Represents one selection. 641 */ 642 private static class Selection { 643 /** Current selected view info. Cannot be null. */ 644 private final ViewInfo mViewInfo; 645 646 /** Current selection border rectangle. Cannot be null. */ 647 private final Rectangle mRect; 648 649 /** The name displayed over the selection, typically the widget class name. Can be null. */ 650 private final String mName; 651 652 /** 653 * Creates a new {@link Selection} object. 654 * @param viewInfo The view info being selected. Must not be null. 655 */ Selection(ViewInfo viewInfo)656 public Selection(ViewInfo viewInfo) { 657 658 assert viewInfo != null; 659 660 mViewInfo = viewInfo; 661 662 if (viewInfo == null) { 663 mRect = null; 664 } else { 665 Rectangle r = viewInfo.getSelectionRect(); 666 mRect = new Rectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, r.width, r.height); 667 } 668 669 String name = viewInfo == null ? null : viewInfo.getName(); 670 if (name != null) { 671 // The name is typically a fully-qualified class name. Let's make it a tad shorter. 672 673 if (name.startsWith("android.")) { // $NON-NLS-1$ 674 // For android classes, convert android.foo.Name to android...Name 675 int first = name.indexOf('.'); 676 int last = name.lastIndexOf('.'); 677 if (last > first) { 678 name = name.substring(0, first) + ".." + name.substring(last); // $NON-NLS-1$ 679 } 680 } else { 681 // For custom non-android classes, it's best to keep the 2 first segments of 682 // the namespace, e.g. we want to get something like com.example...MyClass 683 int first = name.indexOf('.'); 684 first = name.indexOf('.', first + 1); 685 int last = name.lastIndexOf('.'); 686 if (last > first) { 687 name = name.substring(0, first) + ".." + name.substring(last); // $NON-NLS-1$ 688 } 689 } 690 } 691 692 mName = name; 693 } 694 695 /** 696 * Returns the selected view info. Cannot be null. 697 */ getViewInfo()698 public ViewInfo getViewInfo() { 699 return mViewInfo; 700 } 701 702 /** 703 * Returns the selection border rectangle. 704 * Cannot be null. 705 */ getRect()706 public Rectangle getRect() { 707 return mRect; 708 } 709 710 /** 711 * The name displayed over the selection, typically the widget class name. 712 * Can be null. 713 */ getName()714 public String getName() { 715 return mName; 716 } 717 } 718 719 /** 720 * Information for the current alternate selection, i.e. the possible selected items 721 * that are located at the same x/y as the original view, either sibling or parents. 722 */ 723 private static class AlternateSelection { 724 private final ViewInfo mOriginatingView; 725 private final List<ViewInfo> mAltViews; 726 private int mIndex; 727 728 /** 729 * Creates a new alternate selection based on the given originating view and the 730 * given list of alternate views. Both cannot be null. 731 */ AlternateSelection(ViewInfo originatingView, List<ViewInfo> altViews)732 public AlternateSelection(ViewInfo originatingView, List<ViewInfo> altViews) { 733 assert originatingView != null; 734 assert altViews != null; 735 mOriginatingView = originatingView; 736 mAltViews = altViews; 737 mIndex = altViews.size() - 1; 738 } 739 740 /** Returns the list of alternate views. Cannot be null. */ getAltViews()741 public List<ViewInfo> getAltViews() { 742 return mAltViews; 743 } 744 745 /** Returns the originating view. Cannot be null. */ getOriginatingView()746 public ViewInfo getOriginatingView() { 747 return mOriginatingView; 748 } 749 750 /** 751 * Returns the current alternate view to select. 752 * Initially this is the top-most view. 753 */ getCurrent()754 public ViewInfo getCurrent() { 755 return mIndex >= 0 ? mAltViews.get(mIndex) : null; 756 } 757 758 /** 759 * Changes the current view to be the next one and then returns it. 760 * This loops through the alternate views. 761 */ getNext()762 public ViewInfo getNext() { 763 if (mIndex == 0) { 764 mIndex = mAltViews.size() - 1; 765 } else if (mIndex > 0) { 766 mIndex--; 767 } 768 769 return getCurrent(); 770 } 771 } 772 773 774 } 775