• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.Context;
20 import android.graphics.Rect;
21 import android.os.Handler;
22 import android.view.GestureDetector;
23 import android.view.MotionEvent;
24 import android.view.animation.DecelerateInterpolator;
25 
26 import com.android.gallery3d.anim.Animation;
27 import com.android.gallery3d.app.GalleryActivity;
28 import com.android.gallery3d.common.Utils;
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(GalleryActivity activity, Spec spec)91     public SlotView(GalleryActivity activity, Spec spec) {
92         mGestureDetector = new GestureDetector(
93                 (Context) activity, new MyGestureListener());
94         mScroller = new ScrollerHelper((Context) activity);
95         mHandler = new SynchronizedHandler(activity.getGLRoot());
96         setSlotSpec(spec);
97     }
98 
setSlotRenderer(SlotRenderer slotDrawer)99     public void setSlotRenderer(SlotRenderer slotDrawer) {
100         mRenderer = slotDrawer;
101         if (mRenderer != null) {
102             mRenderer.onSlotSizeChanged(mLayout.mSlotWidth, mLayout.mSlotHeight);
103             mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd());
104         }
105     }
106 
setCenterIndex(int index)107     public void setCenterIndex(int index) {
108         int slotCount = mLayout.mSlotCount;
109         if (index < 0 || index >= slotCount) {
110             return;
111         }
112         Rect rect = mLayout.getSlotRect(index, mTempRect);
113         int position = WIDE
114                 ? (rect.left + rect.right - getWidth()) / 2
115                 : (rect.top + rect.bottom - getHeight()) / 2;
116         setScrollPosition(position);
117     }
118 
makeSlotVisible(int index)119     public void makeSlotVisible(int index) {
120         Rect rect = mLayout.getSlotRect(index, mTempRect);
121         int visibleBegin = WIDE ? mScrollX : mScrollY;
122         int visibleLength = WIDE ? getWidth() : getHeight();
123         int visibleEnd = visibleBegin + visibleLength;
124         int slotBegin = WIDE ? rect.left : rect.top;
125         int slotEnd = WIDE ? rect.right : rect.bottom;
126 
127         int position = visibleBegin;
128         if (visibleLength < slotEnd - slotBegin) {
129             position = visibleBegin;
130         } else if (slotBegin < visibleBegin) {
131             position = slotBegin;
132         } else if (slotEnd > visibleEnd) {
133             position = slotEnd - visibleLength;
134         }
135 
136         setScrollPosition(position);
137     }
138 
setScrollPosition(int position)139     public void setScrollPosition(int position) {
140         position = Utils.clamp(position, 0, mLayout.getScrollLimit());
141         mScroller.setPosition(position);
142         updateScrollPosition(position, false);
143     }
144 
setSlotSpec(Spec spec)145     public void setSlotSpec(Spec spec) {
146         mLayout.setSlotSpec(spec);
147     }
148 
149     @Override
addComponent(GLView view)150     public void addComponent(GLView view) {
151         throw new UnsupportedOperationException();
152     }
153 
154     @Override
onLayout(boolean changeSize, int l, int t, int r, int b)155     protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
156         if (!changeSize) return;
157 
158         // Make sure we are still at a resonable scroll position after the size
159         // is changed (like orientation change). We choose to keep the center
160         // visible slot still visible. This is arbitrary but reasonable.
161         int visibleIndex =
162                 (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
163         mLayout.setSize(r - l, b - t);
164         makeSlotVisible(visibleIndex);
165         if (mOverscrollEffect == OVERSCROLL_3D) {
166             mPaper.setSize(r - l, b - t);
167         }
168     }
169 
startScatteringAnimation(RelativePosition position)170     public void startScatteringAnimation(RelativePosition position) {
171         mAnimation = new ScatteringAnimation(position);
172         mAnimation.start();
173         if (mLayout.mSlotCount != 0) invalidate();
174     }
175 
startRisingAnimation()176     public void startRisingAnimation() {
177         mAnimation = new RisingAnimation();
178         mAnimation.start();
179         if (mLayout.mSlotCount != 0) invalidate();
180     }
181 
updateScrollPosition(int position, boolean force)182     private void updateScrollPosition(int position, boolean force) {
183         if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return;
184         if (WIDE) {
185             mScrollX = position;
186         } else {
187             mScrollY = position;
188         }
189         mLayout.setScrollPosition(position);
190         onScrollPositionChanged(position);
191     }
192 
onScrollPositionChanged(int newPosition)193     protected void onScrollPositionChanged(int newPosition) {
194         int limit = mLayout.getScrollLimit();
195         mListener.onScrollPositionChanged(newPosition, limit);
196     }
197 
getSlotRect(int slotIndex)198     public Rect getSlotRect(int slotIndex) {
199         return mLayout.getSlotRect(slotIndex, new Rect());
200     }
201 
202     @Override
onTouch(MotionEvent event)203     protected boolean onTouch(MotionEvent event) {
204         if (mUIListener != null) mUIListener.onUserInteraction();
205         mGestureDetector.onTouchEvent(event);
206         switch (event.getAction()) {
207             case MotionEvent.ACTION_DOWN:
208                 mDownInScrolling = !mScroller.isFinished();
209                 mScroller.forceFinished();
210                 break;
211             case MotionEvent.ACTION_UP:
212                 mPaper.onRelease();
213                 invalidate();
214                 break;
215         }
216         return true;
217     }
218 
setListener(Listener listener)219     public void setListener(Listener listener) {
220         mListener = listener;
221     }
222 
setUserInteractionListener(UserInteractionListener listener)223     public void setUserInteractionListener(UserInteractionListener listener) {
224         mUIListener = listener;
225     }
226 
setOverscrollEffect(int kind)227     public void setOverscrollEffect(int kind) {
228         mOverscrollEffect = kind;
229         mScroller.setOverfling(kind == OVERSCROLL_SYSTEM);
230     }
231 
expandIntArray(int array[], int capacity)232     private static int[] expandIntArray(int array[], int capacity) {
233         while (array.length < capacity) {
234             array = new int[array.length * 2];
235         }
236         return array;
237     }
238 
239     @Override
render(GLCanvas canvas)240     protected void render(GLCanvas canvas) {
241         super.render(canvas);
242 
243         if (mRenderer == null) return;
244         mRenderer.prepareDrawing();
245 
246         long animTime = AnimationTime.get();
247         boolean more = mScroller.advanceAnimation(animTime);
248         more |= mLayout.advanceAnimation(animTime);
249         int oldX = mScrollX;
250         updateScrollPosition(mScroller.getPosition(), false);
251 
252         boolean paperActive = false;
253         if (mOverscrollEffect == OVERSCROLL_3D) {
254             // Check if an edge is reached and notify mPaper if so.
255             int newX = mScrollX;
256             int limit = mLayout.getScrollLimit();
257             if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) {
258                 float v = mScroller.getCurrVelocity();
259                 if (newX == limit) v = -v;
260 
261                 // I don't know why, but getCurrVelocity() can return NaN.
262                 if (!Float.isNaN(v)) {
263                     mPaper.edgeReached(v);
264                 }
265             }
266             paperActive = mPaper.advanceAnimation();
267         }
268 
269         more |= paperActive;
270 
271         if (mAnimation != null) {
272             more |= mAnimation.calculate(animTime);
273         }
274 
275         canvas.translate(-mScrollX, -mScrollY);
276 
277         int requestCount = 0;
278         int requestedSlot[] = expandIntArray(mRequestRenderSlots,
279                 mLayout.mVisibleEnd - mLayout.mVisibleStart);
280 
281         for (int i = mLayout.mVisibleEnd - 1; i >= mLayout.mVisibleStart; --i) {
282             int r = renderItem(canvas, i, 0, paperActive);
283             if ((r & RENDER_MORE_FRAME) != 0) more = true;
284             if ((r & RENDER_MORE_PASS) != 0) requestedSlot[requestCount++] = i;
285         }
286 
287         for (int pass = 1; requestCount != 0; ++pass) {
288             int newCount = 0;
289             for (int i = 0; i < requestCount; ++i) {
290                 int r = renderItem(canvas,
291                         requestedSlot[i], pass, paperActive);
292                 if ((r & RENDER_MORE_FRAME) != 0) more = true;
293                 if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i;
294             }
295             requestCount = newCount;
296         }
297 
298         canvas.translate(mScrollX, mScrollY);
299 
300         if (more) invalidate();
301 
302         final UserInteractionListener listener = mUIListener;
303         if (mMoreAnimation && !more && listener != null) {
304             mHandler.post(new Runnable() {
305                 @Override
306                 public void run() {
307                     listener.onUserInteractionEnd();
308                 }
309             });
310         }
311         mMoreAnimation = more;
312     }
313 
renderItem( GLCanvas canvas, int index, int pass, boolean paperActive)314     private int renderItem(
315             GLCanvas canvas, int index, int pass, boolean paperActive) {
316         canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
317         Rect rect = mLayout.getSlotRect(index, mTempRect);
318         if (paperActive) {
319             canvas.multiplyMatrix(mPaper.getTransform(rect, mScrollX), 0);
320         } else {
321             canvas.translate(rect.left, rect.top, 0);
322         }
323         if (mAnimation != null && mAnimation.isActive()) {
324             mAnimation.apply(canvas, index, rect);
325         }
326         int result = mRenderer.renderSlot(
327                 canvas, index, pass, rect.right - rect.left, rect.bottom - rect.top);
328         canvas.restore();
329         return result;
330     }
331 
332     public static abstract class SlotAnimation extends Animation {
333         protected float mProgress = 0;
334 
SlotAnimation()335         public SlotAnimation() {
336             setInterpolator(new DecelerateInterpolator(4));
337             setDuration(1500);
338         }
339 
340         @Override
onCalculate(float progress)341         protected void onCalculate(float progress) {
342             mProgress = progress;
343         }
344 
apply(GLCanvas canvas, int slotIndex, Rect target)345         abstract public void apply(GLCanvas canvas, int slotIndex, Rect target);
346     }
347 
348     public static class RisingAnimation extends SlotAnimation {
349         private static final int RISING_DISTANCE = 128;
350 
351         @Override
apply(GLCanvas canvas, int slotIndex, Rect target)352         public void apply(GLCanvas canvas, int slotIndex, Rect target) {
353             canvas.translate(0, 0, RISING_DISTANCE * (1 - mProgress));
354         }
355     }
356 
357     public static class ScatteringAnimation extends SlotAnimation {
358         private int PHOTO_DISTANCE = 1000;
359         private RelativePosition mCenter;
360 
ScatteringAnimation(RelativePosition center)361         public ScatteringAnimation(RelativePosition center) {
362             mCenter = center;
363         }
364 
365         @Override
apply(GLCanvas canvas, int slotIndex, Rect target)366         public void apply(GLCanvas canvas, int slotIndex, Rect target) {
367             canvas.translate(
368                     (mCenter.getX() - target.centerX()) * (1 - mProgress),
369                     (mCenter.getY() - target.centerY()) * (1 - mProgress),
370                     slotIndex * PHOTO_DISTANCE * (1 - mProgress));
371             canvas.setAlpha(mProgress);
372         }
373     }
374 
375     // This Spec class is used to specify the size of each slot in the SlotView.
376     // There are two ways to do it:
377     //
378     // (1) Specify slotWidth and slotHeight: they specify the width and height
379     //     of each slot. The number of rows and the gap between slots will be
380     //     determined automatically.
381     // (2) Specify rowsLand, rowsPort, and slotGap: they specify the number
382     //     of rows in landscape/portrait mode and the gap between slots. The
383     //     width and height of each slot is determined automatically.
384     //
385     // The initial value of -1 means they are not specified.
386     public static class Spec {
387         public int slotWidth = -1;
388         public int slotHeight = -1;
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;
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 
741     private static class IntegerAnimation extends Animation {
742         private int mTarget;
743         private int mCurrent = 0;
744         private int mFrom = 0;
745         private boolean mEnabled = false;
746 
setEnabled(boolean enabled)747         public void setEnabled(boolean enabled) {
748             mEnabled = enabled;
749         }
750 
startAnimateTo(int target)751         public void startAnimateTo(int target) {
752             if (!mEnabled) {
753                 mTarget = mCurrent = target;
754                 return;
755             }
756             if (target == mTarget) return;
757 
758             mFrom = mCurrent;
759             mTarget = target;
760             setDuration(180);
761             start();
762         }
763 
get()764         public int get() {
765             return mCurrent;
766         }
767 
getTarget()768         public int getTarget() {
769             return mTarget;
770         }
771 
772         @Override
onCalculate(float progress)773         protected void onCalculate(float progress) {
774             mCurrent = Math.round(mFrom + progress * (mTarget - mFrom));
775             if (progress == 1f) mEnabled = false;
776         }
777     }
778 }
779