1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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.gallery3d.ui;
18 
19 import android.graphics.Rect;
20 import android.os.Handler;
21 import android.view.GestureDetector;
22 import android.view.MotionEvent;
23 import android.view.animation.DecelerateInterpolator;
24 
25 import com.android.gallery3d.anim.Animation;
26 import com.android.gallery3d.app.AbstractGalleryActivity;
27 import com.android.gallery3d.common.Utils;
28 import com.android.gallery3d.glrenderer.GLCanvas;
29 
30 public class SlotView extends GLView {
31     @SuppressWarnings("unused")
32     private static final String TAG = "SlotView";
33 
34     private static final boolean WIDE = true;
35     private static final int INDEX_NONE = -1;
36 
37     public static final int RENDER_MORE_PASS = 1;
38     public static final int RENDER_MORE_FRAME = 2;
39 
40     public interface Listener {
onDown(int index)41         public void onDown(int index);
onUp(boolean followedByLongPress)42         public void onUp(boolean followedByLongPress);
onSingleTapUp(int index)43         public void onSingleTapUp(int index);
onLongTap(int index)44         public void onLongTap(int index);
onScrollPositionChanged(int position, int total)45         public void onScrollPositionChanged(int position, int total);
46     }
47 
48     public static class SimpleListener implements Listener {
onDown(int index)49         @Override public void onDown(int index) {}
onUp(boolean followedByLongPress)50         @Override public void onUp(boolean followedByLongPress) {}
onSingleTapUp(int index)51         @Override public void onSingleTapUp(int index) {}
onLongTap(int index)52         @Override public void onLongTap(int index) {}
onScrollPositionChanged(int position, int total)53         @Override public void onScrollPositionChanged(int position, int total) {}
54     }
55 
56     public static interface SlotRenderer {
prepareDrawing()57         public void prepareDrawing();
onVisibleRangeChanged(int visibleStart, int visibleEnd)58         public void onVisibleRangeChanged(int visibleStart, int visibleEnd);
onSlotSizeChanged(int width, int height)59         public void onSlotSizeChanged(int width, int height);
renderSlot(GLCanvas canvas, int index, int pass, int width, int height)60         public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height);
61     }
62 
63     private final GestureDetector mGestureDetector;
64     private final ScrollerHelper mScroller;
65     private final Paper mPaper = new Paper();
66 
67     private Listener mListener;
68     private UserInteractionListener mUIListener;
69 
70     private boolean mMoreAnimation = false;
71     private SlotAnimation mAnimation = null;
72     private final Layout mLayout = new Layout();
73     private int mStartIndex = INDEX_NONE;
74 
75     // whether the down action happened while the view is scrolling.
76     private boolean mDownInScrolling;
77     private int mOverscrollEffect = OVERSCROLL_3D;
78     private final Handler mHandler;
79 
80     private SlotRenderer mRenderer;
81 
82     private int[] mRequestRenderSlots = new int[16];
83 
84     public static final int OVERSCROLL_3D = 0;
85     public static final int OVERSCROLL_SYSTEM = 1;
86     public static final int OVERSCROLL_NONE = 2;
87 
88     // to prevent allocating memory
89     private final Rect mTempRect = new Rect();
90 
SlotView(AbstractGalleryActivity activity, Spec spec)91     public SlotView(AbstractGalleryActivity activity, Spec spec) {
92         mGestureDetector = new GestureDetector(activity, new MyGestureListener());
93         mScroller = new ScrollerHelper(activity);
94         mHandler = new SynchronizedHandler(activity.getGLRoot());
95         setSlotSpec(spec);
96     }
97 
setSlotRenderer(SlotRenderer slotDrawer)98     public void setSlotRenderer(SlotRenderer slotDrawer) {
99         mRenderer = slotDrawer;
100         if (mRenderer != null) {
101             mRenderer.onSlotSizeChanged(mLayout.mSlotWidth, mLayout.mSlotHeight);
102             mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd());
103         }
104     }
105 
setCenterIndex(int index)106     public void setCenterIndex(int index) {
107         int slotCount = mLayout.mSlotCount;
108         if (index < 0 || index >= slotCount) {
109             return;
110         }
111         Rect rect = mLayout.getSlotRect(index, mTempRect);
112         int position = WIDE
113                 ? (rect.left + rect.right - getWidth()) / 2
114                 : (rect.top + rect.bottom - getHeight()) / 2;
115         setScrollPosition(position);
116     }
117 
makeSlotVisible(int index)118     public void makeSlotVisible(int index) {
119         Rect rect = mLayout.getSlotRect(index, mTempRect);
120         int visibleBegin = WIDE ? mScrollX : mScrollY;
121         int visibleLength = WIDE ? getWidth() : getHeight();
122         int visibleEnd = visibleBegin + visibleLength;
123         int slotBegin = WIDE ? rect.left : rect.top;
124         int slotEnd = WIDE ? rect.right : rect.bottom;
125 
126         int position = visibleBegin;
127         if (visibleLength < slotEnd - slotBegin) {
128             position = visibleBegin;
129         } else if (slotBegin < visibleBegin) {
130             position = slotBegin;
131         } else if (slotEnd > visibleEnd) {
132             position = slotEnd - visibleLength;
133         }
134 
135         setScrollPosition(position);
136     }
137 
setScrollPosition(int position)138     public void setScrollPosition(int position) {
139         position = Utils.clamp(position, 0, mLayout.getScrollLimit());
140         mScroller.setPosition(position);
141         updateScrollPosition(position, false);
142     }
143 
setSlotSpec(Spec spec)144     public void setSlotSpec(Spec spec) {
145         mLayout.setSlotSpec(spec);
146     }
147 
148     @Override
addComponent(GLView view)149     public void addComponent(GLView view) {
150         throw new UnsupportedOperationException();
151     }
152 
153     @Override
onLayout(boolean changeSize, int l, int t, int r, int b)154     protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
155         if (!changeSize) return;
156 
157         // Make sure we are still at a resonable scroll position after the size
158         // is changed (like orientation change). We choose to keep the center
159         // visible slot still visible. This is arbitrary but reasonable.
160         int visibleIndex =
161                 (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
162         mLayout.setSize(r - l, b - t);
163         makeSlotVisible(visibleIndex);
164         if (mOverscrollEffect == OVERSCROLL_3D) {
165             mPaper.setSize(r - l, b - t);
166         }
167     }
168 
startScatteringAnimation(RelativePosition position)169     public void startScatteringAnimation(RelativePosition position) {
170         mAnimation = new ScatteringAnimation(position);
171         mAnimation.start();
172         if (mLayout.mSlotCount != 0) invalidate();
173     }
174 
startRisingAnimation()175     public void startRisingAnimation() {
176         mAnimation = new RisingAnimation();
177         mAnimation.start();
178         if (mLayout.mSlotCount != 0) invalidate();
179     }
180 
updateScrollPosition(int position, boolean force)181     private void updateScrollPosition(int position, boolean force) {
182         if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return;
183         if (WIDE) {
184             mScrollX = position;
185         } else {
186             mScrollY = position;
187         }
188         mLayout.setScrollPosition(position);
189         onScrollPositionChanged(position);
190     }
191 
onScrollPositionChanged(int newPosition)192     protected void onScrollPositionChanged(int newPosition) {
193         int limit = mLayout.getScrollLimit();
194         mListener.onScrollPositionChanged(newPosition, limit);
195     }
196 
getSlotRect(int slotIndex)197     public Rect getSlotRect(int slotIndex) {
198         return mLayout.getSlotRect(slotIndex, new Rect());
199     }
200 
201     @Override
onTouch(MotionEvent event)202     protected boolean onTouch(MotionEvent event) {
203         if (mUIListener != null) mUIListener.onUserInteraction();
204         mGestureDetector.onTouchEvent(event);
205         switch (event.getAction()) {
206             case MotionEvent.ACTION_DOWN:
207                 mDownInScrolling = !mScroller.isFinished();
208                 mScroller.forceFinished();
209                 break;
210             case MotionEvent.ACTION_UP:
211                 mPaper.onRelease();
212                 invalidate();
213                 break;
214         }
215         return true;
216     }
217 
setListener(Listener listener)218     public void setListener(Listener listener) {
219         mListener = listener;
220     }
221 
setUserInteractionListener(UserInteractionListener listener)222     public void setUserInteractionListener(UserInteractionListener listener) {
223         mUIListener = listener;
224     }
225 
setOverscrollEffect(int kind)226     public void setOverscrollEffect(int kind) {
227         mOverscrollEffect = kind;
228         mScroller.setOverfling(kind == OVERSCROLL_SYSTEM);
229     }
230 
expandIntArray(int array[], int capacity)231     private static int[] expandIntArray(int array[], int capacity) {
232         while (array.length < capacity) {
233             array = new int[array.length * 2];
234         }
235         return array;
236     }
237 
238     @Override
render(GLCanvas canvas)239     protected void render(GLCanvas canvas) {
240         super.render(canvas);
241 
242         if (mRenderer == null) return;
243         mRenderer.prepareDrawing();
244 
245         long animTime = AnimationTime.get();
246         boolean more = mScroller.advanceAnimation(animTime);
247         more |= mLayout.advanceAnimation(animTime);
248         int oldX = mScrollX;
249         updateScrollPosition(mScroller.getPosition(), false);
250 
251         boolean paperActive = false;
252         if (mOverscrollEffect == OVERSCROLL_3D) {
253             // Check if an edge is reached and notify mPaper if so.
254             int newX = mScrollX;
255             int limit = mLayout.getScrollLimit();
256             if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) {
257                 float v = mScroller.getCurrVelocity();
258                 if (newX == limit) v = -v;
259 
260                 // I don't know why, but getCurrVelocity() can return NaN.
261                 if (!Float.isNaN(v)) {
262                     mPaper.edgeReached(v);
263                 }
264             }
265             paperActive = mPaper.advanceAnimation();
266         }
267 
268         more |= paperActive;
269 
270         if (mAnimation != null) {
271             more |= mAnimation.calculate(animTime);
272         }
273 
274         canvas.translate(-mScrollX, -mScrollY);
275 
276         int requestCount = 0;
277         int requestedSlot[] = expandIntArray(mRequestRenderSlots,
278                 mLayout.mVisibleEnd - mLayout.mVisibleStart);
279 
280         for (int i = mLayout.mVisibleEnd - 1; i >= mLayout.mVisibleStart; --i) {
281             int r = renderItem(canvas, i, 0, paperActive);
282             if ((r & RENDER_MORE_FRAME) != 0) more = true;
283             if ((r & RENDER_MORE_PASS) != 0) requestedSlot[requestCount++] = i;
284         }
285 
286         for (int pass = 1; requestCount != 0; ++pass) {
287             int newCount = 0;
288             for (int i = 0; i < requestCount; ++i) {
289                 int r = renderItem(canvas,
290                         requestedSlot[i], pass, paperActive);
291                 if ((r & RENDER_MORE_FRAME) != 0) more = true;
292                 if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i;
293             }
294             requestCount = newCount;
295         }
296 
297         canvas.translate(mScrollX, mScrollY);
298 
299         if (more) invalidate();
300 
301         final UserInteractionListener listener = mUIListener;
302         if (mMoreAnimation && !more && listener != null) {
303             mHandler.post(new Runnable() {
304                 @Override
305                 public void run() {
306                     listener.onUserInteractionEnd();
307                 }
308             });
309         }
310         mMoreAnimation = more;
311     }
312 
renderItem( GLCanvas canvas, int index, int pass, boolean paperActive)313     private int renderItem(
314             GLCanvas canvas, int index, int pass, boolean paperActive) {
315         canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
316         Rect rect = mLayout.getSlotRect(index, mTempRect);
317         if (paperActive) {
318             canvas.multiplyMatrix(mPaper.getTransform(rect, mScrollX), 0);
319         } else {
320             canvas.translate(rect.left, rect.top, 0);
321         }
322         if (mAnimation != null && mAnimation.isActive()) {
323             mAnimation.apply(canvas, index, rect);
324         }
325         int result = mRenderer.renderSlot(
326                 canvas, index, pass, rect.right - rect.left, rect.bottom - rect.top);
327         canvas.restore();
328         return result;
329     }
330 
331     public static abstract class SlotAnimation extends Animation {
332         protected float mProgress = 0;
333 
SlotAnimation()334         public SlotAnimation() {
335             setInterpolator(new DecelerateInterpolator(4));
336             setDuration(1500);
337         }
338 
339         @Override
onCalculate(float progress)340         protected void onCalculate(float progress) {
341             mProgress = progress;
342         }
343 
apply(GLCanvas canvas, int slotIndex, Rect target)344         abstract public void apply(GLCanvas canvas, int slotIndex, Rect target);
345     }
346 
347     public static class RisingAnimation extends SlotAnimation {
348         private static final int RISING_DISTANCE = 128;
349 
350         @Override
apply(GLCanvas canvas, int slotIndex, Rect target)351         public void apply(GLCanvas canvas, int slotIndex, Rect target) {
352             canvas.translate(0, 0, RISING_DISTANCE * (1 - mProgress));
353         }
354     }
355 
356     public static class ScatteringAnimation extends SlotAnimation {
357         private int PHOTO_DISTANCE = 1000;
358         private RelativePosition mCenter;
359 
ScatteringAnimation(RelativePosition center)360         public ScatteringAnimation(RelativePosition center) {
361             mCenter = center;
362         }
363 
364         @Override
apply(GLCanvas canvas, int slotIndex, Rect target)365         public void apply(GLCanvas canvas, int slotIndex, Rect target) {
366             canvas.translate(
367                     (mCenter.getX() - target.centerX()) * (1 - mProgress),
368                     (mCenter.getY() - target.centerY()) * (1 - mProgress),
369                     slotIndex * PHOTO_DISTANCE * (1 - mProgress));
370             canvas.setAlpha(mProgress);
371         }
372     }
373 
374     // This Spec class is used to specify the size of each slot in the SlotView.
375     // There are two ways to do it:
376     //
377     // (1) Specify slotWidth and slotHeight: they specify the width and height
378     //     of each slot. The number of rows and the gap between slots will be
379     //     determined automatically.
380     // (2) Specify rowsLand, rowsPort, and slotGap: they specify the number
381     //     of rows in landscape/portrait mode and the gap between slots. The
382     //     width and height of each slot is determined automatically.
383     //
384     // The initial value of -1 means they are not specified.
385     public static class Spec {
386         public int slotWidth = -1;
387         public int slotHeight = -1;
388         public int slotHeightAdditional = 0;
389 
390         public int rowsLand = -1;
391         public int rowsPort = -1;
392         public int slotGap = -1;
393     }
394 
395     public class Layout {
396 
397         private int mVisibleStart;
398         private int mVisibleEnd;
399 
400         private int mSlotCount;
401         private int mSlotWidth;
402         private int mSlotHeight;
403         private int mSlotGap;
404 
405         private Spec mSpec;
406 
407         private int mWidth;
408         private int mHeight;
409 
410         private int mUnitCount;
411         private int mContentLength;
412         private int mScrollPosition;
413 
414         private IntegerAnimation mVerticalPadding = new IntegerAnimation();
415         private IntegerAnimation mHorizontalPadding = new IntegerAnimation();
416 
setSlotSpec(Spec spec)417         public void setSlotSpec(Spec spec) {
418             mSpec = spec;
419         }
420 
setSlotCount(int slotCount)421         public boolean setSlotCount(int slotCount) {
422             if (slotCount == mSlotCount) return false;
423             if (mSlotCount != 0) {
424                 mHorizontalPadding.setEnabled(true);
425                 mVerticalPadding.setEnabled(true);
426             }
427             mSlotCount = slotCount;
428             int hPadding = mHorizontalPadding.getTarget();
429             int vPadding = mVerticalPadding.getTarget();
430             initLayoutParameters();
431             return vPadding != mVerticalPadding.getTarget()
432                     || hPadding != mHorizontalPadding.getTarget();
433         }
434 
getSlotRect(int index, Rect rect)435         public Rect getSlotRect(int index, Rect rect) {
436             int col, row;
437             if (WIDE) {
438                 col = index / mUnitCount;
439                 row = index - col * mUnitCount;
440             } else {
441                 row = index / mUnitCount;
442                 col = index - row * mUnitCount;
443             }
444 
445             int x = mHorizontalPadding.get() + col * (mSlotWidth + mSlotGap);
446             int y = mVerticalPadding.get() + row * (mSlotHeight + mSlotGap);
447             rect.set(x, y, x + mSlotWidth, y + mSlotHeight);
448             return rect;
449         }
450 
getSlotWidth()451         public int getSlotWidth() {
452             return mSlotWidth;
453         }
454 
getSlotHeight()455         public int getSlotHeight() {
456             return mSlotHeight;
457         }
458 
459         // Calculate
460         // (1) mUnitCount: the number of slots we can fit into one column (or row).
461         // (2) mContentLength: the width (or height) we need to display all the
462         //     columns (rows).
463         // (3) padding[]: the vertical and horizontal padding we need in order
464         //     to put the slots towards to the center of the display.
465         //
466         // The "major" direction is the direction the user can scroll. The other
467         // direction is the "minor" direction.
468         //
469         // The comments inside this method are the description when the major
470         // directon is horizontal (X), and the minor directon is vertical (Y).
initLayoutParameters( int majorLength, int minorLength, int majorUnitSize, int minorUnitSize, int[] padding)471         private void initLayoutParameters(
472                 int majorLength, int minorLength,  /* The view width and height */
473                 int majorUnitSize, int minorUnitSize,  /* The slot width and height */
474                 int[] padding) {
475             int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap);
476             if (unitCount == 0) unitCount = 1;
477             mUnitCount = unitCount;
478 
479             // We put extra padding above and below the column.
480             int availableUnits = Math.min(mUnitCount, mSlotCount);
481             int usedMinorLength = availableUnits * minorUnitSize +
482                     (availableUnits - 1) * mSlotGap;
483             padding[0] = (minorLength - usedMinorLength) / 2;
484 
485             // Then calculate how many columns we need for all slots.
486             int count = ((mSlotCount + mUnitCount - 1) / mUnitCount);
487             mContentLength = count * majorUnitSize + (count - 1) * mSlotGap;
488 
489             // If the content length is less then the screen width, put
490             // extra padding in left and right.
491             padding[1] = Math.max(0, (majorLength - mContentLength) / 2);
492         }
493 
initLayoutParameters()494         private void initLayoutParameters() {
495             // Initialize mSlotWidth and mSlotHeight from mSpec
496             if (mSpec.slotWidth != -1) {
497                 mSlotGap = 0;
498                 mSlotWidth = mSpec.slotWidth;
499                 mSlotHeight = mSpec.slotHeight;
500             } else {
501                 int rows = (mWidth > mHeight) ? mSpec.rowsLand : mSpec.rowsPort;
502                 mSlotGap = mSpec.slotGap;
503                 mSlotHeight = Math.max(1, (mHeight - (rows - 1) * mSlotGap) / rows);
504                 mSlotWidth = mSlotHeight - mSpec.slotHeightAdditional;
505             }
506 
507             if (mRenderer != null) {
508                 mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight);
509             }
510 
511             int[] padding = new int[2];
512             if (WIDE) {
513                 initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
514                 mVerticalPadding.startAnimateTo(padding[0]);
515                 mHorizontalPadding.startAnimateTo(padding[1]);
516             } else {
517                 initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding);
518                 mVerticalPadding.startAnimateTo(padding[1]);
519                 mHorizontalPadding.startAnimateTo(padding[0]);
520             }
521             updateVisibleSlotRange();
522         }
523 
setSize(int width, int height)524         public void setSize(int width, int height) {
525             mWidth = width;
526             mHeight = height;
527             initLayoutParameters();
528         }
529 
updateVisibleSlotRange()530         private void updateVisibleSlotRange() {
531             int position = mScrollPosition;
532 
533             if (WIDE) {
534                 int startCol = position / (mSlotWidth + mSlotGap);
535                 int start = Math.max(0, mUnitCount * startCol);
536                 int endCol = (position + mWidth + mSlotWidth + mSlotGap - 1) /
537                         (mSlotWidth + mSlotGap);
538                 int end = Math.min(mSlotCount, mUnitCount * endCol);
539                 setVisibleRange(start, end);
540             } else {
541                 int startRow = position / (mSlotHeight + mSlotGap);
542                 int start = Math.max(0, mUnitCount * startRow);
543                 int endRow = (position + mHeight + mSlotHeight + mSlotGap - 1) /
544                         (mSlotHeight + mSlotGap);
545                 int end = Math.min(mSlotCount, mUnitCount * endRow);
546                 setVisibleRange(start, end);
547             }
548         }
549 
setScrollPosition(int position)550         public void setScrollPosition(int position) {
551             if (mScrollPosition == position) return;
552             mScrollPosition = position;
553             updateVisibleSlotRange();
554         }
555 
setVisibleRange(int start, int end)556         private void setVisibleRange(int start, int end) {
557             if (start == mVisibleStart && end == mVisibleEnd) return;
558             if (start < end) {
559                 mVisibleStart = start;
560                 mVisibleEnd = end;
561             } else {
562                 mVisibleStart = mVisibleEnd = 0;
563             }
564             if (mRenderer != null) {
565                 mRenderer.onVisibleRangeChanged(mVisibleStart, mVisibleEnd);
566             }
567         }
568 
getVisibleStart()569         public int getVisibleStart() {
570             return mVisibleStart;
571         }
572 
getVisibleEnd()573         public int getVisibleEnd() {
574             return mVisibleEnd;
575         }
576 
getSlotIndexByPosition(float x, float y)577         public int getSlotIndexByPosition(float x, float y) {
578             int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0);
579             int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition);
580 
581             absoluteX -= mHorizontalPadding.get();
582             absoluteY -= mVerticalPadding.get();
583 
584             if (absoluteX < 0 || absoluteY < 0) {
585                 return INDEX_NONE;
586             }
587 
588             int columnIdx = absoluteX / (mSlotWidth + mSlotGap);
589             int rowIdx = absoluteY / (mSlotHeight + mSlotGap);
590 
591             if (!WIDE && columnIdx >= mUnitCount) {
592                 return INDEX_NONE;
593             }
594 
595             if (WIDE && rowIdx >= mUnitCount) {
596                 return INDEX_NONE;
597             }
598 
599             if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) {
600                 return INDEX_NONE;
601             }
602 
603             if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) {
604                 return INDEX_NONE;
605             }
606 
607             int index = WIDE
608                     ? (columnIdx * mUnitCount + rowIdx)
609                     : (rowIdx * mUnitCount + columnIdx);
610 
611             return index >= mSlotCount ? INDEX_NONE : index;
612         }
613 
getScrollLimit()614         public int getScrollLimit() {
615             int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight;
616             return limit <= 0 ? 0 : limit;
617         }
618 
advanceAnimation(long animTime)619         public boolean advanceAnimation(long animTime) {
620             // use '|' to make sure both sides will be executed
621             return mVerticalPadding.calculate(animTime) | mHorizontalPadding.calculate(animTime);
622         }
623     }
624 
625     private class MyGestureListener implements GestureDetector.OnGestureListener {
626         private boolean isDown;
627 
628         // We call the listener's onDown() when our onShowPress() is called and
629         // call the listener's onUp() when we receive any further event.
630         @Override
onShowPress(MotionEvent e)631         public void onShowPress(MotionEvent e) {
632             GLRoot root = getGLRoot();
633             root.lockRenderThread();
634             try {
635                 if (isDown) return;
636                 int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
637                 if (index != INDEX_NONE) {
638                     isDown = true;
639                     mListener.onDown(index);
640                 }
641             } finally {
642                 root.unlockRenderThread();
643             }
644         }
645 
cancelDown(boolean byLongPress)646         private void cancelDown(boolean byLongPress) {
647             if (!isDown) return;
648             isDown = false;
649             mListener.onUp(byLongPress);
650         }
651 
652         @Override
onDown(MotionEvent e)653         public boolean onDown(MotionEvent e) {
654             return false;
655         }
656 
657         @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)658         public boolean onFling(MotionEvent e1,
659                 MotionEvent e2, float velocityX, float velocityY) {
660             cancelDown(false);
661             int scrollLimit = mLayout.getScrollLimit();
662             if (scrollLimit == 0) return false;
663             float velocity = WIDE ? velocityX : velocityY;
664             mScroller.fling((int) -velocity, 0, scrollLimit);
665             if (mUIListener != null) mUIListener.onUserInteractionBegin();
666             invalidate();
667             return true;
668         }
669 
670         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)671         public boolean onScroll(MotionEvent e1,
672                 MotionEvent e2, float distanceX, float distanceY) {
673             cancelDown(false);
674             float distance = WIDE ? distanceX : distanceY;
675             int overDistance = mScroller.startScroll(
676                     Math.round(distance), 0, mLayout.getScrollLimit());
677             if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) {
678                 mPaper.overScroll(overDistance);
679             }
680             invalidate();
681             return true;
682         }
683 
684         @Override
onSingleTapUp(MotionEvent e)685         public boolean onSingleTapUp(MotionEvent e) {
686             cancelDown(false);
687             if (mDownInScrolling) return true;
688             int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
689             if (index != INDEX_NONE) mListener.onSingleTapUp(index);
690             return true;
691         }
692 
693         @Override
onLongPress(MotionEvent e)694         public void onLongPress(MotionEvent e) {
695             cancelDown(true);
696             if (mDownInScrolling) return;
697             lockRendering();
698             try {
699                 int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
700                 if (index != INDEX_NONE) mListener.onLongTap(index);
701             } finally {
702                 unlockRendering();
703             }
704         }
705     }
706 
setStartIndex(int index)707     public void setStartIndex(int index) {
708         mStartIndex = index;
709     }
710 
711     // Return true if the layout parameters have been changed
setSlotCount(int slotCount)712     public boolean setSlotCount(int slotCount) {
713         boolean changed = mLayout.setSlotCount(slotCount);
714 
715         // mStartIndex is applied the first time setSlotCount is called.
716         if (mStartIndex != INDEX_NONE) {
717             setCenterIndex(mStartIndex);
718             mStartIndex = INDEX_NONE;
719         }
720         // Reset the scroll position to avoid scrolling over the updated limit.
721         setScrollPosition(WIDE ? mScrollX : mScrollY);
722         return changed;
723     }
724 
getVisibleStart()725     public int getVisibleStart() {
726         return mLayout.getVisibleStart();
727     }
728 
getVisibleEnd()729     public int getVisibleEnd() {
730         return mLayout.getVisibleEnd();
731     }
732 
getScrollX()733     public int getScrollX() {
734         return mScrollX;
735     }
736 
getScrollY()737     public int getScrollY() {
738         return mScrollY;
739     }
740 
getSlotRect(int slotIndex, GLView rootPane)741     public Rect getSlotRect(int slotIndex, GLView rootPane) {
742         // Get slot rectangle relative to this root pane.
743         Rect offset = new Rect();
744         rootPane.getBoundsOf(this, offset);
745         Rect r = getSlotRect(slotIndex);
746         r.offset(offset.left - getScrollX(),
747                 offset.top - getScrollY());
748         return r;
749     }
750 
751     private static class IntegerAnimation extends Animation {
752         private int mTarget;
753         private int mCurrent = 0;
754         private int mFrom = 0;
755         private boolean mEnabled = false;
756 
setEnabled(boolean enabled)757         public void setEnabled(boolean enabled) {
758             mEnabled = enabled;
759         }
760 
startAnimateTo(int target)761         public void startAnimateTo(int target) {
762             if (!mEnabled) {
763                 mTarget = mCurrent = target;
764                 return;
765             }
766             if (target == mTarget) return;
767 
768             mFrom = mCurrent;
769             mTarget = target;
770             setDuration(180);
771             start();
772         }
773 
get()774         public int get() {
775             return mCurrent;
776         }
777 
getTarget()778         public int getTarget() {
779             return mTarget;
780         }
781 
782         @Override
onCalculate(float progress)783         protected void onCalculate(float progress) {
784             mCurrent = Math.round(mFrom + progress * (mTarget - mFrom));
785             if (progress == 1f) mEnabled = false;
786         }
787     }
788 }
789