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
17package com.android.gallery3d.ui;
18
19import android.graphics.Rect;
20import android.os.Handler;
21import android.view.GestureDetector;
22import android.view.MotionEvent;
23import android.view.animation.DecelerateInterpolator;
24
25import com.android.gallery3d.anim.Animation;
26import com.android.gallery3d.app.AbstractGalleryActivity;
27import com.android.gallery3d.common.Utils;
28import com.android.gallery3d.glrenderer.GLCanvas;
29
30public 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 {
41        public void onDown(int index);
42        public void onUp(boolean followedByLongPress);
43        public void onSingleTapUp(int index);
44        public void onLongTap(int index);
45        public void onScrollPositionChanged(int position, int total);
46    }
47
48    public static class SimpleListener implements Listener {
49        @Override public void onDown(int index) {}
50        @Override public void onUp(boolean followedByLongPress) {}
51        @Override public void onSingleTapUp(int index) {}
52        @Override public void onLongTap(int index) {}
53        @Override public void onScrollPositionChanged(int position, int total) {}
54    }
55
56    public static interface SlotRenderer {
57        public void prepareDrawing();
58        public void onVisibleRangeChanged(int visibleStart, int visibleEnd);
59        public void onSlotSizeChanged(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
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
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
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
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
138    public void setScrollPosition(int position) {
139        position = Utils.clamp(position, 0, mLayout.getScrollLimit());
140        mScroller.setPosition(position);
141        updateScrollPosition(position, false);
142    }
143
144    public void setSlotSpec(Spec spec) {
145        mLayout.setSlotSpec(spec);
146    }
147
148    @Override
149    public void addComponent(GLView view) {
150        throw new UnsupportedOperationException();
151    }
152
153    @Override
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
169    public void startScatteringAnimation(RelativePosition position) {
170        mAnimation = new ScatteringAnimation(position);
171        mAnimation.start();
172        if (mLayout.mSlotCount != 0) invalidate();
173    }
174
175    public void startRisingAnimation() {
176        mAnimation = new RisingAnimation();
177        mAnimation.start();
178        if (mLayout.mSlotCount != 0) invalidate();
179    }
180
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
192    protected void onScrollPositionChanged(int newPosition) {
193        int limit = mLayout.getScrollLimit();
194        mListener.onScrollPositionChanged(newPosition, limit);
195    }
196
197    public Rect getSlotRect(int slotIndex) {
198        return mLayout.getSlotRect(slotIndex, new Rect());
199    }
200
201    @Override
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
218    public void setListener(Listener listener) {
219        mListener = listener;
220    }
221
222    public void setUserInteractionListener(UserInteractionListener listener) {
223        mUIListener = listener;
224    }
225
226    public void setOverscrollEffect(int kind) {
227        mOverscrollEffect = kind;
228        mScroller.setOverfling(kind == OVERSCROLL_SYSTEM);
229    }
230
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
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
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
334        public SlotAnimation() {
335            setInterpolator(new DecelerateInterpolator(4));
336            setDuration(1500);
337        }
338
339        @Override
340        protected void onCalculate(float progress) {
341            mProgress = progress;
342        }
343
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
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
360        public ScatteringAnimation(RelativePosition center) {
361            mCenter = center;
362        }
363
364        @Override
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
417        public void setSlotSpec(Spec spec) {
418            mSpec = spec;
419        }
420
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
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
451        public int getSlotWidth() {
452            return mSlotWidth;
453        }
454
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).
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
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
524        public void setSize(int width, int height) {
525            mWidth = width;
526            mHeight = height;
527            initLayoutParameters();
528        }
529
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
550        public void setScrollPosition(int position) {
551            if (mScrollPosition == position) return;
552            mScrollPosition = position;
553            updateVisibleSlotRange();
554        }
555
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
569        public int getVisibleStart() {
570            return mVisibleStart;
571        }
572
573        public int getVisibleEnd() {
574            return mVisibleEnd;
575        }
576
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
614        public int getScrollLimit() {
615            int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight;
616            return limit <= 0 ? 0 : limit;
617        }
618
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
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
646        private void cancelDown(boolean byLongPress) {
647            if (!isDown) return;
648            isDown = false;
649            mListener.onUp(byLongPress);
650        }
651
652        @Override
653        public boolean onDown(MotionEvent e) {
654            return false;
655        }
656
657        @Override
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
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
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
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
707    public void setStartIndex(int index) {
708        mStartIndex = index;
709    }
710
711    // Return true if the layout parameters have been changed
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
725    public int getVisibleStart() {
726        return mLayout.getVisibleStart();
727    }
728
729    public int getVisibleEnd() {
730        return mLayout.getVisibleEnd();
731    }
732
733    public int getScrollX() {
734        return mScrollX;
735    }
736
737    public int getScrollY() {
738        return mScrollY;
739    }
740
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
757        public void setEnabled(boolean enabled) {
758            mEnabled = enabled;
759        }
760
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
774        public int get() {
775            return mCurrent;
776        }
777
778        public int getTarget() {
779            return mTarget;
780        }
781
782        @Override
783        protected void onCalculate(float progress) {
784            mCurrent = Math.round(mFrom + progress * (mTarget - mFrom));
785            if (progress == 1f) mEnabled = false;
786        }
787    }
788}
789