Add scroll function to SlotView.

Change-Id: Ia520fbfa285856520e2b148c50277e00f1b67ffe
diff --git a/new3d/src/com/android/gallery3d/app/Gallery.java b/new3d/src/com/android/gallery3d/app/Gallery.java
index cff25e4..07d454d 100644
--- a/new3d/src/com/android/gallery3d/app/Gallery.java
+++ b/new3d/src/com/android/gallery3d/app/Gallery.java
@@ -39,7 +39,7 @@
         setContentView(R.layout.main);
         mGLRootView = (GLRootView) findViewById(R.id.gl_root_view);
 
-        mSlotView = new SlotView();
+        mSlotView = new SlotView(this);
         mGLRootView.setContentPane(mSlotView);
         mSlotView.setModel(new SlotViewMockData(this));
     }
diff --git a/new3d/src/com/android/gallery3d/ui/DisplayItemPanel.java b/new3d/src/com/android/gallery3d/ui/DisplayItemPanel.java
index 5d4abde..2267134 100644
--- a/new3d/src/com/android/gallery3d/ui/DisplayItemPanel.java
+++ b/new3d/src/com/android/gallery3d/ui/DisplayItemPanel.java
@@ -58,6 +58,12 @@
         if (!mPrepareTransition) invalidate();
     }
 
+    public void removeDisplayItem(DisplayItem item) {
+        if (item.mPanel != this) throw new IllegalArgumentException();
+        mItems.remove(item);
+        item.mPanel = null;
+    }
+
     public void prepareTransition() {
         mPrepareTransition = true;
         for (DisplayItem item : mItems) {
@@ -85,9 +91,12 @@
 
     @Override
     protected void render(GLRootView view, GL11 gl) {
+        Transformation transform = view.getTransformation();
+        Matrix matrix = transform.getMatrix();
+        matrix.preTranslate(-mScrollX, 0);
         if (mAnimationStartTime == NO_ANIMATION) {
             for (DisplayItem item: mItems) {
-                renderItem(view, item);
+                renderItem(view, item, matrix);
             }
         } else {
             long now = view.currentAnimationTimeMillis();
@@ -107,11 +116,10 @@
                 invalidate();
             }
         }
+        matrix.preTranslate(mScrollX, 0);
     }
 
-    private void renderItem(GLRootView root, DisplayItem item) {
-        Transformation transform = root.getTransformation();
-        Matrix matrix = transform.getMatrix();
+    private void renderItem(GLRootView root, DisplayItem item, Matrix matrix) {
         item.mCurrent.apply(matrix);
         item.render(root);
         item.mCurrent.inverse(matrix);
diff --git a/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java b/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java
new file mode 100644
index 0000000..916a637
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -0,0 +1,81 @@
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.view.ViewConfiguration;
+import android.view.animation.Interpolator;
+
+public class ScrollerHelper {
+    private static final long ANIMATION_START = 0;
+
+    private final float mDeceleration;
+
+    private boolean mFinished;
+    private int mVelocity;
+    private int mDirection;
+    private int mDuration; // in millisecond
+    private long mStartTime;
+
+    private int mMin;
+    private int mMax;
+    private int mStart;
+    private int mFinal;
+    private int mPosition;
+    private final Interpolator mInterpolator;
+
+    public ScrollerHelper(Context context, Interpolator interpolator) {
+        mInterpolator = interpolator;
+        mDeceleration = SensorManager.GRAVITY_EARTH   // g (m/s^2)
+                * 39.37f                              // inch/meter
+                * Util.dpToPixel(context, 160)        // pixels per inch
+                * ViewConfiguration.getScrollFriction();
+    }
+
+    /**
+     * Call this when you want to know the new location.  If it returns true,
+     * the animation is not yet finished.  loc will be altered to provide the
+     * new location.
+     */
+    public boolean computeScrollOffset(long currentTimeMillis) {
+        if (mFinished) return false;
+        if (mStartTime == ANIMATION_START) mStartTime = currentTimeMillis;
+
+        int timePassed = (int)(currentTimeMillis - mStartTime);
+        if (timePassed < mDuration) {
+            if (mInterpolator != null) {
+                timePassed = (int) (mDuration * mInterpolator.getInterpolation(
+                        (float) timePassed / mDuration) + 0.5f);
+            }
+            float t = timePassed / 1000.0f;
+            int distance = (int) ((
+                    mVelocity * t) - (mDeceleration * t * t / 2.0f) + 0.5f);
+            mPosition = Util.clamp(mStart + mDirection * distance, mMin, mMax);
+        } else {
+            mPosition = mFinal;
+            mFinished = true;
+        }
+        return true;
+    }
+
+    public void forceFinished() {
+        mFinished = true;
+    }
+
+    public int getCurrentPosition() {
+        return mPosition;
+    }
+
+    public void fling(int start, int velocity, int min, int max) {
+        mFinished = false;
+        mVelocity = Math.abs(velocity);
+        mDirection = velocity >= 0 ? 1 : -1;
+        mDuration = (int) (1000 * mVelocity / mDeceleration);
+        mStartTime = ANIMATION_START;
+        mStart = start;
+        mMin = min;
+        mMax = max;
+        double totalDistance = (double) mDirection
+                * (velocity * velocity) / (2 * mDeceleration);
+        mFinal = Util.clamp(start + (int) (totalDistance + 0.5), min, max);
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/SlotView.java b/new3d/src/com/android/gallery3d/ui/SlotView.java
index 3748049..0137531 100644
--- a/new3d/src/com/android/gallery3d/ui/SlotView.java
+++ b/new3d/src/com/android/gallery3d/ui/SlotView.java
@@ -1,14 +1,23 @@
 package com.android.gallery3d.ui;
 
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.animation.DecelerateInterpolator;
+
+import javax.microedition.khronos.opengles.GL11;
 
 public class SlotView extends GLView {
 
+    private static final String TAG = "SlotView";
+    private static final int MAX_VELOCITY = 2500;
+
     public static interface Model {
         public int size();
         public int getSlotHeight();
         public int getSlotWidth();
         public void putSlot(int index, int x, int y, DisplayItemPanel panel);
-        public void freeSlot(int index);
+        public void freeSlot(int index, DisplayItemPanel panel);
     }
 
     private Model mModel;
@@ -19,12 +28,20 @@
     private int mSlotWidth;
     private int mSlotHeight;
     private int mRowCount;
-    private int mScroll;
     private int mScrollLimit;
 
-    public SlotView() {
+    private int mVisibleStart = 0;
+    private int mVisibleEnd = 0;
+
+    private final GestureDetector mGestureDetector;
+    private final ScrollerHelper mScroller;
+
+    public SlotView(Context context) {
         mPanel = new DisplayItemPanel();
         super.addComponent(mPanel);
+        mGestureDetector =
+                new GestureDetector(context, new MyGestureListener());
+        mScroller = new ScrollerHelper(context, new DecelerateInterpolator(1));
     }
 
     @Override
@@ -46,60 +63,153 @@
 
     private void initializeLayoutParams() {
         int size = mModel.size();
-        int slotWidth = mSlotWidth = mModel.getSlotWidth();
-        int slotHeight = mSlotHeight = mModel.getSlotHeight();
+        mSlotWidth = mModel.getSlotWidth();
+        mSlotHeight = mModel.getSlotHeight();
         int totalHeight= getHeight();
         int rowCount = totalHeight / mSlotHeight;
         if (rowCount == 0) rowCount = 1;
         mRowCount = rowCount;
         int hGap = mHorizontalGap = 10;
         int vGap = mVerticalGap = (
-                totalHeight - rowCount * slotHeight) / (rowCount + 1);
-        mScrollLimit = ((size + rowCount - 1) / rowCount) * (hGap + slotWidth)
+                totalHeight - rowCount * mSlotHeight) / (rowCount + 1);
+        mScrollLimit = ((size + rowCount - 1) / rowCount) * (hGap + mSlotWidth)
                 + hGap - getWidth();
+        if (mScrollLimit < 0) mScrollLimit = 0;
     }
 
     @Override
     protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
         mPanel.layout(0, 0, r - l, b - t);
+        mVisibleStart = 0;
+        mVisibleEnd = 0;
+        mPanel.prepareTransition();
         initializeLayoutParams();
-        mScroll = Util.clamp(mScroll, 0, mScrollLimit);
-        layoutContent();
+        // The scroll limit could be changed
+        setScrollPosition(mPanel.mScrollX, true);
+        mPanel.startTransition();
+    }
+
+    private void setScrollPosition(int position, boolean force) {
+        position = Util.clamp(position, 0, mScrollLimit);
+        if (!force && position == mPanel.mScrollX) return;
+        mPanel.mScrollX = position;
+
+        int colWidth = mHorizontalGap + mSlotWidth;
+        int rowHeight = mVerticalGap + mSlotHeight;
+        int startColumn = position / colWidth;
+        int endColumn = (position + getWidth() + mSlotWidth - 1) / colWidth;
+        setVisibleRange(startColumn, endColumn);
+        invalidate();
     }
 
     public void notifyDataChanged() {
-        mScroll = 0;
+        setScrollPosition(0, false);
         notifyDataInvalidate();
     }
 
     public void notifyDataInvalidate() {
-        layoutContent();
+        invalidate();
     }
 
-    private void layoutContent() {
-        int hGap = mHorizontalGap;
-        int vGap = mVerticalGap;
-        int slotWidth = mSlotWidth;
-        int slotHeight = mSlotHeight;
+    @Override
+    protected boolean dispatchTouchEvent(MotionEvent event) {
+        // Don't pass the event to the child (MediaDisplayPanel).
+        // Handle it here
+        return onTouch(event);
+    }
+
+    @Override
+    protected boolean onTouch(MotionEvent event) {
+        mGestureDetector.onTouchEvent(event);
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mScroller.forceFinished();
+                break;
+        }
+        return true;
+    }
+
+    @Override
+    protected void render(GLRootView root, GL11 gl) {
+        super.render(root, gl);
+        if (mScroller.computeScrollOffset(root.currentAnimationTimeMillis())) {
+            setScrollPosition(mScroller.getCurrentPosition(), false);
+            invalidate();
+        }
+    }
+
+    private void freeSlotsInColumn(int columnIndex) {
+        int rowCount = mRowCount;
+        DisplayItemPanel panel = mPanel;
+        for (int i = columnIndex * rowCount,
+                n = Math.min(mModel.size(), i + rowCount); i < n; ++i) {
+            mModel.freeSlot(i, panel);
+        }
+    }
+
+    private void putSlotsInColumn(int columnIndex) {
+        int x = columnIndex * (mHorizontalGap + mSlotWidth) + mHorizontalGap;
+        int y = mVerticalGap;
+        int rowHeight = mVerticalGap + mSlotHeight;
         int rowCount = mRowCount;
 
-        int colWidth = hGap + slotWidth;
-        int rowHeight = vGap + slotHeight;
-
-        int startColumn = mScroll / colWidth;
-        int endColumn = (mScroll + getWidth() + slotWidth - 1) / colWidth;
-
-        int startIndex = startColumn * rowCount;
-        int endIndex = Math.min(endColumn * rowCount, mModel.size());
-
-        mPanel.prepareTransition();
-        for (int i = startIndex; i < endIndex; ++i) {
-            int col = i / rowCount;
-            int row = i - col * rowCount;
-            mModel.putSlot(i, col * colWidth + hGap - mScroll,
-                    row * rowHeight + vGap, mPanel);
+        DisplayItemPanel panel = mPanel;
+        for (int i = columnIndex * rowCount,
+                n = Math.min(mModel.size(), i + rowCount); i < n; ++i) {
+            mModel.putSlot(i, x, y, panel);
+            y += rowHeight;
         }
-        mPanel.startTransition();
-        invalidate();
+    }
+
+    // start: inclusive, end: exclusive
+    private void setVisibleRange(int start, int end) {
+        if (start == mVisibleStart && end == mVisibleEnd) return;
+        int rowCount = mRowCount;
+        if (start >= mVisibleEnd || end < mVisibleStart) {
+            for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+                freeSlotsInColumn(i);
+            }
+            for (int i = start; i < end; ++i) {
+                putSlotsInColumn(i);
+            }
+        } else {
+            for (int i = mVisibleStart; i < start; ++i) {
+                freeSlotsInColumn(i);
+            }
+            for (int i = end, n = mVisibleEnd; i < n; ++i) {
+                freeSlotsInColumn(i);
+            }
+            for (int i = start, n = mVisibleStart; i < n; ++i) {
+                putSlotsInColumn(i);
+            }
+            for (int i = mVisibleEnd; i < end; ++i) {
+                putSlotsInColumn(i);
+            }
+        }
+        mVisibleStart = start;
+        mVisibleEnd = end;
+    }
+
+    private class MyGestureListener
+            extends GestureDetector.SimpleOnGestureListener {
+
+        @Override
+        public boolean onFling(MotionEvent e1,
+                MotionEvent e2, float velocityX, float velocityY) {
+            if (mScrollLimit == 0) return false;
+            velocityX = Util.clamp(velocityX, -MAX_VELOCITY, MAX_VELOCITY);
+            mScroller.fling(mPanel.mScrollX, -(int) velocityX, 0, mScrollLimit);
+            invalidate();
+            return true;
+        }
+
+        @Override
+        public boolean onScroll(MotionEvent e1,
+                MotionEvent e2, float distanceX, float distanceY) {
+            if (mScrollLimit == 0) return false;
+            setScrollPosition(mPanel.mScrollX + (int) distanceX, false);
+            invalidate();
+            return true;
+        }
     }
 }
diff --git a/new3d/src/com/android/gallery3d/ui/SlotViewMockData.java b/new3d/src/com/android/gallery3d/ui/SlotViewMockData.java
index b0014aa..4993ad0 100644
--- a/new3d/src/com/android/gallery3d/ui/SlotViewMockData.java
+++ b/new3d/src/com/android/gallery3d/ui/SlotViewMockData.java
@@ -10,7 +10,7 @@
 public class SlotViewMockData implements SlotView.Model {
     private static final int LENGTH_LIMIT = 180;
     private static final double EXPECTED_AREA = LENGTH_LIMIT * LENGTH_LIMIT / 2;
-    private static final int DATA_SIZE = 15;
+    private static final int DATA_SIZE = 50;
     private static final int PILE_SIZE = 4;
 
     private final BasicTexture mPhoto[];
@@ -37,7 +37,10 @@
         }
     }
 
-    public void freeSlot(int index) {
+    public void freeSlot(int index, DisplayItemPanel panel) {
+        for (int i = index * PILE_SIZE, n = i + PILE_SIZE; i < n; ++i) {
+            panel.removeDisplayItem(mItems[i]);
+        }
     }
 
     public void putSlot(int slotIndex, int x, int y, DisplayItemPanel panel) {