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