Add swipe-to-delete gesture.

Change-Id: I992e59702f9dfff17da2f4464e48c9228d42b1b3
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
index 65334d5..2b91fcb 100644
--- a/src/com/android/gallery3d/ui/PositionController.java
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -25,6 +25,7 @@
 import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.util.RangeArray;
 import com.android.gallery3d.util.RangeIntArray;
+import com.android.gallery3d.ui.PhotoView.Size;
 
 class PositionController {
     private static final String TAG = "PositionController";
@@ -35,11 +36,13 @@
     public static final int IMAGE_AT_BOTTOM_EDGE = 8;
 
     public static final int CAPTURE_ANIMATION_TIME = 700;
+    public static final int SNAPBACK_ANIMATION_TIME = 600;
 
     // Special values for animation time.
     private static final long NO_ANIMATION = -1;
     private static final long LAST_ANIMATION = -2;
 
+    private static final int ANIM_KIND_NONE = -1;
     private static final int ANIM_KIND_SCROLL = 0;
     private static final int ANIM_KIND_SCALE = 1;
     private static final int ANIM_KIND_SNAPBACK = 2;
@@ -47,17 +50,26 @@
     private static final int ANIM_KIND_ZOOM = 4;
     private static final int ANIM_KIND_OPENING = 5;
     private static final int ANIM_KIND_FLING = 6;
-    private static final int ANIM_KIND_CAPTURE = 7;
+    private static final int ANIM_KIND_FLING_X = 7;
+    private static final int ANIM_KIND_DELETE = 8;
+    private static final int ANIM_KIND_CAPTURE = 9;
 
     // Animation time in milliseconds. The order must match ANIM_KIND_* above.
+    //
+    // The values for ANIM_KIND_FLING_X does't matter because we use
+    // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's
+    // faster for Animatable.advanceAnimation() to calculate the progress
+    // (always 1).
     private static final int ANIM_TIME[] = {
         0,    // ANIM_KIND_SCROLL
         50,   // ANIM_KIND_SCALE
-        600,  // ANIM_KIND_SNAPBACK
+        SNAPBACK_ANIMATION_TIME,  // ANIM_KIND_SNAPBACK
         400,  // ANIM_KIND_SLIDE
         300,  // ANIM_KIND_ZOOM
         400,  // ANIM_KIND_OPENING
         0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
+        0,    // ANIM_KIND_FLING_X (see the comment above)
+        0,    // ANIM_KIND_DELETE (the duration is calculated dynamically)
         CAPTURE_ANIMATION_TIME,  // ANIM_KIND_CAPTURE
     };
 
@@ -86,10 +98,15 @@
     // In addition to the focused box (index == 0). We also keep information
     // about this many boxes on each side.
     private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
+    private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1];
 
     private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
     private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
 
+    // These are constants for the delete gesture.
+    private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms
+    private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms
+
     private Listener mListener;
     private volatile Rect mOpenAnimationRect;
 
@@ -164,9 +181,14 @@
     // The output of the PositionController. Available throught getPosition().
     private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
 
+    // The direction of a new picture should appear. New pictures pop from top
+    // if this value is true, or from bottom if this value is false.
+    boolean mPopFromTop;
+
     public interface Listener {
         void invalidate();
-        boolean isHolding();
+        boolean isHoldingDown();
+        boolean isHoldingDelete();
 
         // EdgeView
         void onPull(int offset, int direction);
@@ -174,6 +196,17 @@
         void onAbsorb(int velocity, int direction);
     }
 
+    static {
+        // Initialize the CENTER_OUT_INDEX array.
+        // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX
+        // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX
+        for (int i = 0; i < CENTER_OUT_INDEX.length; i++) {
+            int j = (i + 1) / 2;
+            if ((i & 1) == 0) j = -j;
+            CENTER_OUT_INDEX[i] = j;
+        }
+    }
+
     public PositionController(Context context, Listener listener) {
         mListener = listener;
         mPageScroller = new FlingScroller();
@@ -234,16 +267,16 @@
         snapAndRedraw();
     }
 
-    public void forceImageSize(int index, int width, int height) {
-        if (width == 0 || height == 0) return;
+    public void forceImageSize(int index, Size s) {
+        if (s.width == 0 || s.height == 0) return;
         Box b = mBoxes.get(index);
-        b.mImageW = width;
-        b.mImageH = height;
+        b.mImageW = s.width;
+        b.mImageH = s.height;
         return;
     }
 
-    public void setImageSize(int index, int width, int height, Rect cFrame) {
-        if (width == 0 || height == 0) return;
+    public void setImageSize(int index, Size s, Rect cFrame) {
+        if (s.width == 0 || s.height == 0) return;
 
         boolean needUpdate = false;
         if (cFrame != null && !mConstrainedFrame.equals(cFrame)) {
@@ -251,7 +284,7 @@
             mPlatform.updateDefaultXY();
             needUpdate = true;
         }
-        needUpdate |= setBoxSize(index, width, height, false);
+        needUpdate |= setBoxSize(index, s.width, s.height, false);
 
         if (!needUpdate) return;
         updateScaleAndGapLimit();
@@ -527,37 +560,31 @@
         redraw();
     }
 
-    public void startScroll(float dx, float dy) {
+    // Only allow scrolling when we are not currently in an animation or we
+    // are in some animation with can be interrupted.
+    private boolean canScroll() {
+        Box b = mBoxes.get(0);
+        if (b.mAnimationStartTime == NO_ANIMATION) return true;
+        switch (b.mAnimationKind) {
+            case ANIM_KIND_SCROLL:
+            case ANIM_KIND_FLING:
+            case ANIM_KIND_FLING_X:
+                return true;
+        }
+        return false;
+    }
+
+    public void scrollPage(int dx, int dy) {
+        if (!canScroll()) return;
+
         Box b = mBoxes.get(0);
         Platform p = mPlatform;
 
-        // Only allow scrolling when we are not currently in an animation or we
-        // are in some animation with can be interrupted.
-        if (b.mAnimationStartTime != NO_ANIMATION) {
-            switch (b.mAnimationKind) {
-                case ANIM_KIND_SCROLL:
-                case ANIM_KIND_FLING:
-                    break;
-                default:
-                    return;
-            }
-        }
-
-        int x = p.mCurrentX + (int) (dx + 0.5f);
-        int y = b.mCurrentY + (int) (dy + 0.5f);
-
-        if (mFilmMode) {
-            scrollToFilm(x, y);
-        } else {
-            scrollToPage(x, y);
-        }
-    }
-
-    private void scrollToPage(int x, int y) {
-        Box b = mBoxes.get(0);
-
         calculateStableBound(b.mCurrentScale);
 
+        int x = p.mCurrentX + dx;
+        int y = b.mCurrentY + dy;
+
         // Vertical direction: If we have space to move in the vertical
         // direction, we show the edge effect when scrolling reaches the edge.
         if (mBoundTop != mBoundBottom) {
@@ -585,8 +612,26 @@
         startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
     }
 
-    private void scrollToFilm(int x, int y) {
+    public void scrollFilmX(int dx) {
+        if (!canScroll()) return;
+
         Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+
+        // Only allow scrolling when we are not currently in an animation or we
+        // are in some animation with can be interrupted.
+        if (b.mAnimationStartTime != NO_ANIMATION) {
+            switch (b.mAnimationKind) {
+                case ANIM_KIND_SCROLL:
+                case ANIM_KIND_FLING:
+                case ANIM_KIND_FLING_X:
+                    break;
+                default:
+                    return;
+            }
+        }
+
+        int x = p.mCurrentX + dx;
 
         // Horizontal direction: we show the edge effect when the scrolling
         // tries to go left of the first image or go right of the last image.
@@ -599,16 +644,19 @@
             x = 0;
         }
         x += mPlatform.mDefaultX;
-        startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
+        startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL);
     }
 
-    public boolean fling(float velocityX, float velocityY) {
-        int vx = (int) (velocityX + 0.5f);
-        int vy = (int) (velocityY + 0.5f);
-        return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy);
+    public void scrollFilmY(int boxIndex, int dy) {
+        if (!canScroll()) return;
+
+        Box b = mBoxes.get(boxIndex);
+        int y = b.mCurrentY + dy;
+        b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL);
+        redraw();
     }
 
-    private boolean flingPage(int velocityX, int velocityY) {
+    public boolean flingPage(int velocityX, int velocityY) {
         Box b = mBoxes.get(0);
         Platform p = mPlatform;
 
@@ -637,11 +685,12 @@
         int targetX = mPageScroller.getFinalX();
         int targetY = mPageScroller.getFinalY();
         ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
-        startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
-        return true;
+        return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
     }
 
-    private boolean flingFilm(int velocityX, int velocityY) {
+    public boolean flingFilmX(int velocityX) {
+        if (velocityX == 0) return false;
+
         Box b = mBoxes.get(0);
         Platform p = mPlatform;
 
@@ -652,17 +701,62 @@
             return false;
         }
 
-        if (velocityX == 0) return false;
-
         mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
                 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
         int targetX = mFilmScroller.getFinalX();
-        // This value doesn't matter because we use mFilmScroller.isFinished()
-        // to decide when to stop. We set this to 0 so it's faster for
-        // Animatable.advanceAnimation() to calculate the progress (always 1).
-        ANIM_TIME[ANIM_KIND_FLING] = 0;
-        startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING);
-        return true;
+        return startAnimation(
+                targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X);
+    }
+
+    // Moves the specified box out of screen. If velocityY is 0, a default
+    // velocity is used. Returns the time for the duration, or -1 if we cannot
+    // not do the animation.
+    public int flingFilmY(int boxIndex, int velocityY) {
+        Box b = mBoxes.get(boxIndex);
+
+        // Calculate targetY
+        int h = heightOf(b);
+        int targetY;
+        int FUZZY = 3;  // TODO: figure out why this is needed.
+        if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) {
+            targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY;
+        } else {
+            targetY = (mViewH + 1) / 2 + h / 2 + FUZZY;
+        }
+
+        // Calculate duration
+        int duration;
+        if (velocityY != 0) {
+            duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f
+                    / Math.abs(velocityY));
+            duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration);
+        } else {
+            duration = DEFAULT_DELETE_ANIMATION_DURATION;
+        }
+
+        // Start animation
+        ANIM_TIME[ANIM_KIND_DELETE] = duration;
+        if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) {
+            redraw();
+            return duration;
+        }
+        return -1;
+    }
+
+    // Returns the index of the box which contains the given point (x, y)
+    // Returns Integer.MAX_VALUE if there is no hit. There may be more than
+    // one box contains the given point, and we want to give priority to the
+    // one closer to the focused index (0).
+    public int hitTest(int x, int y) {
+        for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+            int j = CENTER_OUT_INDEX[i];
+            Rect r = mRects.get(j);
+            if (r.contains(x, y)) {
+                return j;
+            }
+        }
+
+        return Integer.MAX_VALUE;
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -697,12 +791,13 @@
         redraw();
     }
 
-    private void startAnimation(int targetX, int targetY, float targetScale,
+    private boolean startAnimation(int targetX, int targetY, float targetScale,
             int kind) {
         boolean changed = false;
         changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind);
         changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
         if (changed) redraw();
+        return changed;
     }
 
     public void advanceAnimation() {
@@ -752,15 +847,11 @@
     // Convert the information in mPlatform and mBoxes to mRects, so the user
     // can get the position of each box by getPosition().
     //
-    // Note the loop index goes from inside-out because each box's X coordinate
+    // Note we go from center-out because each box's X coordinate
     // is relative to its anchor box (except the focused box).
     private void layoutAndSetPosition() {
-        // layout box 0 (focused box)
-        convertBoxToRect(0);
-        for (int i = 1; i <= BOX_MAX; i++) {
-            // layout box i and -i
-            convertBoxToRect(i);
-            convertBoxToRect(-i);
+        for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+            convertBoxToRect(CENTER_OUT_INDEX[i]);
         }
         //dumpState();
     }
@@ -770,10 +861,8 @@
             Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
         }
 
-        dumpRect(0);
-        for (int i = 1; i <= BOX_MAX; i++) {
-            dumpRect(i);
-            dumpRect(-i);
+        for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+            dumpRect(CENTER_OUT_INDEX[i]);
         }
 
         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
@@ -854,6 +943,25 @@
         b.mCurrentY = 0;
         b.mCurrentScale = b.mScaleMin;
         b.mAnimationStartTime = NO_ANIMATION;
+        b.mAnimationKind = ANIM_KIND_NONE;
+    }
+
+    // Initialize a box to a given size.
+    private void initBox(int index, Size size) {
+        if (size.width == 0 || size.height == 0) {
+            initBox(index);
+            return;
+        }
+        Box b = mBoxes.get(index);
+        b.mImageW = size.width;
+        b.mImageH = size.height;
+        b.mUseViewSize = false;
+        b.mScaleMin = getMinimalScale(b);
+        b.mScaleMax = getMaximalScale(b);
+        b.mCurrentY = 0;
+        b.mCurrentScale = b.mScaleMin;
+        b.mAnimationStartTime = NO_ANIMATION;
+        b.mAnimationKind = ANIM_KIND_NONE;
     }
 
     // Initialize a gap. This can only be called after the boxes around the gap
@@ -904,7 +1012,7 @@
     // focused box. constrained indicates whether the focused box should be put
     // into the constrained frame.
     public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext,
-            boolean constrained) {
+            boolean constrained, Size[] sizes) {
         //debugMoveBox(fromIndex);
         mHasPrev = hasPrev;
         mHasNext = hasNext;
@@ -957,7 +1065,7 @@
                 k++;
             }
             mBoxes.put(i, mTempBoxes.get(k++));
-            initBox(i);
+            initBox(i, sizes[i + BOX_MAX]);
         }
 
         // 6. Now give the recycled box a reasonable absolute X position.
@@ -977,13 +1085,41 @@
             mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
             first = last = 0;
         }
-        // Now for those boxes between first and last, just assign the same
-        // position as the next box. (We can do better, but this should be
-        // rare). For the boxes before first or after last, we will use a new
-        // default gap size below.
-        for (int i = last - 1; i > first; i--) {
+        // Now for those boxes between first and last, assign their position to
+        // align to the previous box or the next box with known position. For
+        // the boxes before first or after last, we will use a new default gap
+        // size below.
+
+        // Align to the previous box
+        for (int i = Math.max(0, first + 1); i < last; i++) {
             if (from.get(i) != Integer.MAX_VALUE) continue;
-            mBoxes.get(i).mAbsoluteX = mBoxes.get(i + 1).mAbsoluteX;
+            Box a = mBoxes.get(i - 1);
+            Box b = mBoxes.get(i);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2
+                    + getDefaultGapSize(i);
+            if (mPopFromTop) {
+                b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+            } else {
+                b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+            }
+        }
+
+        // Align to the next box
+        for (int i = Math.min(-1, last - 1); i > first; i--) {
+            if (from.get(i) != Integer.MAX_VALUE) continue;
+            Box a = mBoxes.get(i + 1);
+            Box b = mBoxes.get(i);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2)
+                    - getDefaultGapSize(i);
+            if (mPopFromTop) {
+                b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+            } else {
+                b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+            }
         }
 
         // 7. recycle the gaps that are not used in the new array.
@@ -1107,6 +1243,19 @@
         return mFilmRatio.mCurrentRatio;
     }
 
+    public void setPopFromTop(boolean top) {
+        mPopFromTop = top;
+    }
+
+    public boolean hasDeletingBox() {
+        for(int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     ////////////////////////////////////////////////////////////////////////////
     //  Private utilities
     ////////////////////////////////////////////////////////////////////////////
@@ -1262,6 +1411,8 @@
             switch (kind) {
                 case ANIM_KIND_SCROLL:
                 case ANIM_KIND_FLING:
+                case ANIM_KIND_FLING_X:
+                case ANIM_KIND_DELETE:
                 case ANIM_KIND_CAPTURE:
                     progress = 1 - f;  // linear
                     break;
@@ -1293,7 +1444,7 @@
         public boolean startSnapback() {
             if (mAnimationStartTime != NO_ANIMATION) return false;
             if (mAnimationKind == ANIM_KIND_SCROLL
-                    && mListener.isHolding()) return false;
+                    && mListener.isHoldingDown()) return false;
             if (mInScale) return false;
 
             Box b = mBoxes.get(0);
@@ -1367,9 +1518,9 @@
         @Override
         protected boolean interpolate(float progress) {
             if (mAnimationKind == ANIM_KIND_FLING) {
-                return mFilmMode
-                        ? interpolateFlingFilm(progress)
-                        : interpolateFlingPage(progress);
+                return interpolateFlingPage(progress);
+            } else if (mAnimationKind == ANIM_KIND_FLING_X) {
+                return interpolateFlingFilm(progress);
             } else {
                 return interpolateLinear(progress);
             }
@@ -1469,7 +1620,9 @@
         public boolean startSnapback() {
             if (mAnimationStartTime != NO_ANIMATION) return false;
             if (mAnimationKind == ANIM_KIND_SCROLL
-                    && mListener.isHolding()) return false;
+                    && mListener.isHoldingDown()) return false;
+            if (mAnimationKind == ANIM_KIND_DELETE
+                    && mListener.isHoldingDelete()) return false;
             if (mInScale && this == mBoxes.get(0)) return false;
 
             int y = mCurrentY;
@@ -1508,13 +1661,6 @@
         private boolean doAnimation(int targetY, float targetScale, int kind) {
             targetScale = clampScale(targetScale);
 
-            // If the scaled height is smaller than the view height, force it to be
-            // in the center.  (We do this for height only, not width, because the
-            // user may want to scroll to the previous/next image.)
-            if (!mInScale && viewTallerThanScaledImage(targetScale)) {
-                targetY = 0;
-            }
-
             if (mCurrentY == targetY && mCurrentScale == targetScale
                     && kind != ANIM_KIND_CAPTURE) {
                 return false;
@@ -1542,7 +1688,6 @@
         @Override
         protected boolean interpolate(float progress) {
             if (mAnimationKind == ANIM_KIND_FLING) {
-                // Currently a Box can only be flung in page mode.
                 return interpolateFlingPage(progress);
             } else {
                 return interpolateLinear(progress);