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