Home | History | Annotate | Download | only in ui
      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 {
     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