Fix several gallery bugs.

1. Make sure the handler in ImageService use main looper
2. Remember the visible range in slot view to prevent reput items
3. Improve the fling formula, so that v0 can match the fling speed.

Change-Id: I48a19327c971c30cdbc8d2fb43c4bacb45ef5664
diff --git a/new3d/src/com/android/gallery3d/data/ImageService.java b/new3d/src/com/android/gallery3d/data/ImageService.java
index f8b3930..1d91047 100644
--- a/new3d/src/com/android/gallery3d/data/ImageService.java
+++ b/new3d/src/com/android/gallery3d/data/ImageService.java
@@ -19,6 +19,7 @@
 import android.content.ContentResolver;
 import android.graphics.Bitmap;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.Message;
 import android.util.Log;
 
@@ -49,7 +50,7 @@
     public ImageService(ContentResolver contentResolver) {
         mContentResolver = contentResolver;
 
-        mHandler = new Handler() {
+        mHandler = new Handler(Looper.getMainLooper()) {
             @Override
             public void handleMessage(Message m) {
                 if (m.what != DECODE_TIMEOUT) return;
diff --git a/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java b/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java
index 2aaf0b4..23ed738 100644
--- a/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java
+++ b/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java
@@ -331,6 +331,8 @@
                 putSlot(i);
             }
         }
+        mVisibleEnd = end;
+        mVisibleStart = start;
     }
 
     public void onLayoutChanged(int width, int height) {
diff --git a/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java b/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java
index 41d6f5c..5a32cc3 100644
--- a/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java
+++ b/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -18,82 +18,101 @@
 
 import com.android.gallery3d.util.Utils;
 
-import android.content.Context;
-import android.hardware.SensorManager;
-import android.view.ViewConfiguration;
-import android.view.animation.Interpolator;
-
 public class ScrollerHelper {
     private static final long START_ANIMATION = -1;
+    private static final long NO_ANIMATION = -2;
 
-    private final float mDeceleration;
+    private static final int ANIM_KIND_FLING = 1;
+    private static final int ANIM_KIND_SCROLL = 2;
+    private static final int DECELERATED_FACTOR = 4;
 
-    private boolean mFinished;
-    private int mVelocity;
-    private int mDirection;
+    private long mStartTime = NO_ANIMATION;
+
     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
-                * Utils.dpToPixel(context, 160)        // pixels per inch
-                * ViewConfiguration.getScrollFriction();
-    }
+    private int mAnimationKind;
+
+    // The fling duration when velocity is 1 pixel / second
+    private float FLING_DURATION_PARAM = 200f; // 200ms
 
     /**
      * 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;
+    public boolean advanceAnimation(long currentTimeMillis) {
+        if (mStartTime == NO_ANIMATION) return false;
         if (mStartTime == START_ANIMATION) mStartTime = currentTimeMillis;
 
         int timePassed = (int)(currentTimeMillis - mStartTime);
         if (timePassed < mDuration) {
-            if (mInterpolator != null) {
-                timePassed = (int) (mDuration * mInterpolator.getInterpolation(
-                        (float) timePassed / mDuration) + 0.5f);
+            float progress = (float) timePassed / mDuration;
+            float f = 1 - progress;
+            if (mAnimationKind == ANIM_KIND_SCROLL) {
+                f = 1 - f;  // linear
+            } else if (mAnimationKind == ANIM_KIND_FLING) {
+                f = 1 - f * f * f * f;  // x ^ DECELERATED_FACTOR
             }
-            float t = timePassed / 1000.0f;
-            int distance = (int) ((
-                    mVelocity * t) - (mDeceleration * t * t / 2.0f) + 0.5f);
-            mPosition = Utils.clamp(mStart + mDirection * distance, mMin, mMax);
+            mPosition = Math.round(mStart + (mFinal - mStart) * f);
+            Log.v("Fling", String.format("mStart = %s, mFinal = %s, mPosition = %s, f = %s, progress = %s",
+                    mStart, mFinal, mPosition, f, progress));
+            if (mPosition == mFinal) {
+                mStartTime = NO_ANIMATION;
+                return false;
+            }
+            return true;
         } else {
             mPosition = mFinal;
-            mFinished = true;
+            mStartTime = NO_ANIMATION;
+            return false;
         }
-        return true;
     }
 
     public void forceFinished() {
-        mFinished = true;
+        mStartTime = NO_ANIMATION;
+        mFinal = mPosition;
     }
 
-    public int getCurrentPosition() {
+    public int getPosition() {
         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);
+    public void setPosition(int position) {
+        mPosition = position;
+    }
+
+    public void fling(float velocity, int min, int max) {
+        /*
+         * The position formula: x = s + (e - s) * (1 - (1 - t / T) ^ d)
+         *     velocity formula: v = d * (e - s) * (1 - t / T) ^ (d - 1) / T
+         * Thus,
+         *     v0 = (e - s) / T * d => (e - s) = v0 * T / d
+         */
         mStartTime = START_ANIMATION;
-        mStart = start;
-        mMin = min;
-        mMax = max;
-        double totalDistance = (double) mDirection
-                * (velocity * velocity) / (2 * mDeceleration);
-        mFinal = Utils.clamp(start + (int) (totalDistance + 0.5), min, max);
+        mAnimationKind = ANIM_KIND_FLING;
+        mStart = mPosition;
+        double x = Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1));
+        mDuration = (int) Math.round(FLING_DURATION_PARAM * x);
+        int distance = Math.round(
+                velocity * mDuration / DECELERATED_FACTOR / 1000);
+        mFinal = Utils.clamp(mStart + distance, min, max);
+
+        // If the left space is not enough, get duration base on the left space
+        if (mFinal - mStart != distance && distance != 0) {
+            mDuration *= Math.pow(
+                    (double) (mFinal - mStart) / distance, 1.0 / (DECELERATED_FACTOR));
+        }
+    }
+
+    public void startScroll(int distance, int min, int max) {
+        mStartTime = START_ANIMATION;
+        mAnimationKind = ANIM_KIND_SCROLL;
+        mStart = mPosition;
+        mFinal = Utils.clamp(mFinal + distance, min, max);
+        mDuration = 0;
     }
 }
diff --git a/new3d/src/com/android/gallery3d/ui/SlotView.java b/new3d/src/com/android/gallery3d/ui/SlotView.java
index 2f407e3..2b956e0 100644
--- a/new3d/src/com/android/gallery3d/ui/SlotView.java
+++ b/new3d/src/com/android/gallery3d/ui/SlotView.java
@@ -43,7 +43,7 @@
     }
 
     private final GestureDetector mGestureDetector;
-    private final ScrollerHelper mScroller;
+    private final ScrollerHelper mScroller = new ScrollerHelper();
     private final PositionRepository mPositions;
 
     private SlotTapListener mSlotTapListener;
@@ -63,7 +63,6 @@
         mPositions = repository;
         mGestureDetector =
                 new GestureDetector(context, new MyGestureListener());
-        mScroller = new ScrollerHelper(context, new DecelerateInterpolator(1));
     }
 
     public void setSlotSize(int slotWidth, int slotHeight) {
@@ -160,8 +159,8 @@
     protected void render(GLCanvas canvas) {
         super.render(canvas);
         long currentTimeMillis = canvas.currentAnimationTimeMillis();
-        boolean more = mScroller.computeScrollOffset(currentTimeMillis);
-        setScrollPosition(mScroller.getCurrentPosition(), false);
+        boolean more = mScroller.advanceAnimation(currentTimeMillis);
+        setScrollPosition(mScroller.getPosition(), false);
         float interpolate = 1f;
         if (mAnimation != null) {
             more |= mAnimation.calculate(currentTimeMillis);
@@ -337,7 +336,7 @@
             int contentLength = mLayout.mContentLength;
             if (contentLength == 0) return false;
             velocityX = Utils.clamp(velocityX, -MAX_VELOCITY, MAX_VELOCITY);
-            mScroller.fling(mScrollX, -(int) velocityX, 0, contentLength);
+            mScroller.fling(-velocityX, 0, contentLength);
             invalidate();
             return true;
         }
@@ -346,7 +345,8 @@
         public boolean onScroll(MotionEvent e1,
                 MotionEvent e2, float distanceX, float distanceY) {
             if (mLayout.mContentLength == 0) return false;
-            setScrollPosition(mScrollX + (int) distanceX, false);
+            mScroller.startScroll(
+                    Math.round(distanceX), 0, mLayout.mContentLength);
             invalidate();
             return true;
         }