• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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