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