OSDN Git Service

Add more gestures and animations in FilmStripView.
authorAngus Kong <shkong@google.com>
Fri, 5 Apr 2013 18:17:19 +0000 (11:17 -0700)
committerAngus Kong <shkong@google.com>
Mon, 8 Apr 2013 21:34:35 +0000 (14:34 -0700)
Now supports fullscreen mode and filmstrip mode.

Change-Id: I4c568e37f3306d970b3439f86ac6757b15c7cd4f

src/com/android/camera/data/CameraDataAdapter.java
src/com/android/camera/ui/FilmStripGestureRecognizer.java [new file with mode: 0644]
src/com/android/camera/ui/FilmStripView.java

index 5d10953..7a75a6d 100644 (file)
@@ -33,6 +33,7 @@ import android.widget.ImageView;
 
 import com.android.camera.Storage;
 import com.android.camera.ui.FilmStripView;
+import com.android.camera.ui.FilmStripView.ImageData;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -79,7 +80,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter {
 
     private List<LocalImageData> mImages;
 
-    private FilmStripView mFilmStripView;
+    private Listener mListener;
     private View mCameraPreviewView;
     private ColorDrawable mPlaceHolder;
 
@@ -101,7 +102,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter {
     }
 
     @Override
-    public FilmStripView.ImageData getImageData(int id) {
+    public ImageData getImageData(int id) {
         if (id >= mImages.size()) return null;
         return mImages.get(id);
     }
@@ -142,8 +143,8 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter {
     }
 
     @Override
-    public void setDataListener(FilmStripView v) {
-        mFilmStripView = v;
+    public void setListener(Listener listener) {
+        mListener = listener;
     }
 
     private LocalImageData buildCameraImageData(int width, int height, int orientation) {
@@ -152,6 +153,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter {
         d.height = height;
         d.orientation = orientation;
         d.isCameraData = true;
+        d.supportedAction = ImageData.ACTION_NONE;
         return d;
     }
 
@@ -179,6 +181,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter {
         d.orientation = c.getInt(COL_ORIENTATION);
         d.width = c.getInt(COL_WIDTH);
         d.height = c.getInt(COL_HEIGHT);
+        d.supportedAction = ImageData.ACTION_PROMOTE | ImageData.ACTION_DEMOTE;
         if (d.width <= 0 || d.height <= 0) {
             Log.v(TAG, "warning! zero dimension for "
                     + d.path + ":" + d.width + "x" + d.height);
@@ -229,6 +232,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter {
         // width and height should be adjusted according to orientation.
         public int width;
         public int height;
+        public int supportedAction;
 
         @Override
         public int getWidth() {
@@ -241,6 +245,17 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter {
         }
 
         @Override
+        public int getType() {
+            if (isCameraData) return ImageData.TYPE_CAMERA_PREVIEW;
+            return ImageData.TYPE_PHOTO;
+        }
+
+        @Override
+        public boolean isActionSupported(int action) {
+            return ((action & supportedAction) != 0);
+        }
+
+        @Override
         public String toString() {
             return "LocalImageData:" + ",data=" + path + ",mimeType=" + mimeType
                     + "," + width + "x" + height + ",orientation=" + orientation;
@@ -281,7 +296,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter {
             mImages = l;
             if (first != null) addOrReplaceCameraData(first);
             // both might be null.
-            if (changed) mFilmStripView.onDataChanged();
+            if (changed && mListener != null) mListener.onDataLoaded();
         }
     }
 
diff --git a/src/com/android/camera/ui/FilmStripGestureRecognizer.java b/src/com/android/camera/ui/FilmStripGestureRecognizer.java
new file mode 100644 (file)
index 0000000..f0e2534
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.ui;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+// This class aggregates three gesture detectors: GestureDetector,
+// ScaleGestureDetector.
+public class FilmStripGestureRecognizer {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FilmStripGestureRecognizer";
+
+    public interface Listener {
+        boolean onSingleTapUp(float x, float y);
+        boolean onDoubleTap(float x, float y);
+        boolean onScroll(float x, float y, float dx, float dy);
+        boolean onFling(float velocityX, float velocityY);
+        boolean onScaleBegin(float focusX, float focusY);
+        boolean onScale(float focusX, float focusY, float scale);
+        boolean onDown(float x, float y);
+        void onScaleEnd();
+    }
+
+    private final GestureDetector mGestureDetector;
+    private final ScaleGestureDetector mScaleDetector;
+    private final Listener mListener;
+
+    public FilmStripGestureRecognizer(Context context, Listener listener) {
+        mListener = listener;
+        mGestureDetector = new GestureDetector(context, new MyGestureListener(),
+                null, true /* ignoreMultitouch */);
+        mScaleDetector = new ScaleGestureDetector(
+                context, new MyScaleListener());
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        return mGestureDetector.onTouchEvent(event) || mScaleDetector.onTouchEvent(event);
+    }
+
+    private class MyGestureListener
+                extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            return mListener.onSingleTapUp(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            return mListener.onDoubleTap(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onScroll(
+                MotionEvent e1, MotionEvent e2, float dx, float dy) {
+            return mListener.onScroll(e2.getX(), e2.getY(), dx, dy);
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+                float velocityY) {
+            return mListener.onFling(velocityX, velocityY);
+        }
+
+        @Override
+        public boolean onDown(MotionEvent e) {
+            mListener.onDown(e.getX(), e.getY());
+            return super.onDown(e);
+        }
+    }
+
+    private class MyScaleListener
+            extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+        @Override
+        public boolean onScaleBegin(ScaleGestureDetector detector) {
+            return mListener.onScaleBegin(
+                    detector.getFocusX(), detector.getFocusY());
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            return mListener.onScale(detector.getFocusX(),
+                    detector.getFocusY(), detector.getScaleFactor());
+        }
+
+        @Override
+        public void onScaleEnd(ScaleGestureDetector detector) {
+            mListener.onScaleEnd();
+        }
+    }
+}
index 326e969..f2ffa87 100644 (file)
 
 package com.android.camera.ui;
 
+import android.animation.Animator;
+import android.animation.ValueAnimator;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.util.Log;
-import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
 import android.widget.Scroller;
 
 public class FilmStripView extends ViewGroup {
-
     private static final String TAG = "FilmStripView";
     private static final int BUFFER_SIZE = 5;
     // Horizontal padding of children.
     private static final int H_PADDING = 50;
     // Duration to go back to the first.
-    private static final int BACK_SCROLL_DURATION = 500;
-    private static final float MIN_SCALE = 0.7f;
+    private static final int DURATION_BACK_ANIM = 500;
+    private static final int DURATION_SCROLL_TO_FILMSTRIP = 350;
+    private static final int DURATION_GEOMETRY_ADJUST = 200;
+    private static final float FILM_STRIP_SCALE = 0.6f;
+    private static final float MAX_SCALE = 1f;
 
     private Context mContext;
-    private GestureDetector mGestureDetector;
+    private FilmStripGestureRecognizer mGestureRecognizer;
     private DataAdapter mDataAdapter;
     private final Rect mDrawArea = new Rect();
 
     private int mCurrentInfo;
-    private Scroller mScroller;
-    private boolean mIsScrolling;
+    private float mScale;
+    private GeometryAnimator mGeometryAnimator;
     private int mCenterPosition = -1;
     private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE];
 
     public interface ImageData {
+        public static final int TYPE_NONE = 0;
+        public static final int TYPE_CAMERA_PREVIEW = 1;
+        public static final int TYPE_PHOTO = 2;
+        public static final int TYPE_VIDEO = 3;
+        public static final int TYPE_PHOTOSPHERE = 4;
+
+        // The actions are defined bit-wise so we can use bit operations like
+        // | and &.
+        public static final int ACTION_NONE = 0;
+        public static final int ACTION_PROMOTE = 1;
+        public static final int ACTION_DEMOTE = 2;
+
+        // SIZE_FULL means disgard the width or height when deciding the view size
+        // of this ImageData, just use full screen size.
+        public static final int SIZE_FULL = -2;
+
         // The values returned by getWidth() and getHeight() will be used for layout.
         public int getWidth();
         public int getHeight();
+        public int getType();
+        public boolean isActionSupported(int action);
     }
 
     public interface DataAdapter {
+        public interface Listener {
+            public void onDataLoaded();
+            public void onDataInserted(int dataID);
+            public void onDataRemoved(int dataID);
+        }
 
         public int getTotalNumber();
         public View getView(Context context, int id);
@@ -63,22 +89,27 @@ public class FilmStripView extends ViewGroup {
         public void suggestSize(int w, int h);
 
         public void requestLoad(ContentResolver r);
-        public void setDataListener(FilmStripView v);
+        public void setListener(Listener listener);
     }
 
+    // A helper class to tract and calculate the view coordination.
     private static class ViewInfo {
         private int mDataID;
         // the position of the left of the view in the whole filmstrip.
         private int mLeftPosition;
-        private  View mView;
+        private View mView;
+        private float mOffsetY;
 
         public ViewInfo(int id, View v) {
+            v.setPivotX(0f);
+            v.setPivotY(0f);
             mDataID = id;
             mView = v;
             mLeftPosition = -1;
+            mOffsetY = 0;
         }
 
-        public int getId() {
+        public int getID() {
             return mDataID;
         }
 
@@ -90,7 +121,15 @@ public class FilmStripView extends ViewGroup {
             return mLeftPosition;
         }
 
-        public int getCenterPosition() {
+        public float getOffsetY() {
+            return mOffsetY;
+        }
+
+        public void setOffsetY(float offset) {
+            mOffsetY = offset;
+        }
+
+        public int getCenterX() {
             return mLeftPosition + mView.getWidth() / 2;
         }
 
@@ -98,15 +137,20 @@ public class FilmStripView extends ViewGroup {
             return mView;
         }
 
-        private void layoutAt(int l, int t) {
-            mView.layout(l, t, l + mView.getMeasuredWidth(), t + mView.getMeasuredHeight());
+        private void layoutAt(int left, int top) {
+            mView.layout(left, top, left + mView.getMeasuredWidth(),
+                    top + mView.getMeasuredHeight());
         }
 
-        public void layoutIn(Rect drawArea, int refCenter) {
+        public void layoutIn(Rect drawArea, int refCenter, float scale) {
             // drawArea is where to layout in.
             // refCenter is the absolute horizontal position of the center of drawArea.
-            layoutAt(drawArea.centerX() + mLeftPosition - refCenter,
-                     drawArea.centerY() - mView.getMeasuredHeight() / 2);
+            int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale);
+            int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale
+                    + mOffsetY);
+            layoutAt(left, top);
+            mView.setScaleX(scale);
+            mView.setScaleY(scale);
         }
     }
 
@@ -129,15 +173,33 @@ public class FilmStripView extends ViewGroup {
         mCurrentInfo = (BUFFER_SIZE - 1) / 2;
         setWillNotDraw(false);
         mContext = context;
-        mScroller = new Scroller(context);
-        mGestureDetector =
-                new GestureDetector(context, new MyGestureListener(),
-                        null, true /* ignoreMultitouch */);
+        mScale = 1.0f;
+        mGeometryAnimator = new GeometryAnimator(context);
+        mGestureRecognizer =
+                new FilmStripGestureRecognizer(context, new MyGestureReceiver());
+    }
+
+    public float getScale() {
+        return mScale;
+    }
+
+    public boolean isAnchoredTo(int id) {
+        if (mViewInfo[mCurrentInfo].getID() == id
+                && mViewInfo[mCurrentInfo].getCenterX() == mCenterPosition) {
+            return true;
+        }
+        return false;
+    }
+
+    public int getCurrentType() {
+        ViewInfo curr = mViewInfo[mCurrentInfo];
+        if (curr == null) return ImageData.TYPE_NONE;
+        return mDataAdapter.getImageData(curr.getID()).getType();
     }
 
     @Override
     public void onDraw(Canvas c) {
-        if (mIsScrolling) {
+        if (mGeometryAnimator.hasNewGeometry()) {
             layoutChildren();
         }
     }
@@ -146,13 +208,11 @@ public class FilmStripView extends ViewGroup {
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
-        int w = MeasureSpec.getSize(widthMeasureSpec);
-        int h = MeasureSpec.getSize(heightMeasureSpec);
-        float scale = MIN_SCALE;
-        if (mDataAdapter != null) mDataAdapter.suggestSize(w / 2, h / 2);
-
-        int boundWidth = (int) (w * scale);
-        int boundHeight = (int) (h * scale);
+        int boundWidth = MeasureSpec.getSize(widthMeasureSpec);
+        int boundHeight = MeasureSpec.getSize(heightMeasureSpec);
+        if (mDataAdapter != null) {
+            mDataAdapter.suggestSize(boundWidth / 2, boundHeight / 2);
+        }
 
         int wMode = View.MeasureSpec.EXACTLY;
         int hMode = View.MeasureSpec.EXACTLY;
@@ -161,22 +221,25 @@ public class FilmStripView extends ViewGroup {
             ViewInfo info = mViewInfo[i];
             if (mViewInfo[i] == null) continue;
 
-            int imageWidth = mDataAdapter.getImageData(info.getId()).getWidth();
-            int imageHeight = mDataAdapter.getImageData(info.getId()).getHeight();
+            int imageWidth = mDataAdapter.getImageData(info.getID()).getWidth();
+            int imageHeight = mDataAdapter.getImageData(info.getID()).getHeight();
+            if (imageWidth == ImageData.SIZE_FULL) imageWidth = boundWidth;
+            if (imageHeight == ImageData.SIZE_FULL) imageHeight = boundHeight;
 
             int scaledWidth = boundWidth;
             int scaledHeight = boundHeight;
+
             if (imageWidth * scaledHeight > scaledWidth * imageHeight) {
                 scaledHeight = imageHeight * scaledWidth / imageWidth;
             } else {
                 scaledWidth = imageWidth * scaledHeight / imageHeight;
             }
-            scaledWidth += H_PADDING * 2 * scale;
+            scaledWidth += H_PADDING * 2;
             mViewInfo[i].getView().measure(
                     View.MeasureSpec.makeMeasureSpec(scaledWidth, wMode)
                     , View.MeasureSpec.makeMeasureSpec(scaledHeight, hMode));
         }
-        setMeasuredDimension(w, h);
+        setMeasuredDimension(boundWidth, boundHeight);
     }
 
     private int findTheNearestView(int pointX) {
@@ -188,14 +251,14 @@ public class FilmStripView extends ViewGroup {
                 nearest++);
         // no existing available ViewInfo
         if (nearest == BUFFER_SIZE) return -1;
-        int min = Math.abs(pointX - mViewInfo[nearest].getCenterPosition());
+        int min = Math.abs(pointX - mViewInfo[nearest].getCenterX());
 
         for (int infoID = nearest + 1;
                 infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) {
             // not measured yet.
             if  (mViewInfo[infoID].getLeftPosition() == -1) continue;
 
-            int c = mViewInfo[infoID].getCenterPosition();
+            int c = mViewInfo[infoID].getCenterX();
             int dist = Math.abs(pointX - c);
             if (dist < min) {
                 min = dist;
@@ -205,6 +268,14 @@ public class FilmStripView extends ViewGroup {
         return nearest;
     }
 
+    private ViewInfo buildInfoFromData(int dataID) {
+        View v = mDataAdapter.getView(mContext, dataID);
+        if (v == null) return null;
+        v.setPadding(H_PADDING, 0, H_PADDING, 0);
+        addView(v);
+        return new ViewInfo(dataID, v);
+    }
+
     // We try to keep the one closest to the center of the screen at position mCurrentInfo.
     private void stepIfNeeded() {
         int nearest = findTheNearestView(mCenterPosition);
@@ -223,7 +294,8 @@ public class FilmStripView extends ViewGroup {
             }
             for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
                 mViewInfo[k] = null;
-                if (mViewInfo[k - 1] != null) getInfo(k, mViewInfo[k - 1].getId() + 1);
+                if (mViewInfo[k - 1] != null)
+                        mViewInfo[k] = buildInfoFromData(mViewInfo[k - 1].getID() + 1);
             }
         } else {
             for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
@@ -236,47 +308,46 @@ public class FilmStripView extends ViewGroup {
             }
             for (int k = -1 - adjust; k >= 0; k--) {
                 mViewInfo[k] = null;
-                if (mViewInfo[k + 1] != null) getInfo(k, mViewInfo[k + 1].getId() - 1);
+                if (mViewInfo[k + 1] != null)
+                        mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1);
             }
         }
     }
 
-    private void stopScroll() {
-        mScroller.forceFinished(true);
-        mIsScrolling = false;
-    }
-
+    // Don't go out of bound.
     private void adjustCenterPosition() {
         ViewInfo curr = mViewInfo[mCurrentInfo];
         if (curr == null) return;
 
-        if (curr.getId() == 0 && mCenterPosition < curr.getCenterPosition()) {
-            mCenterPosition = curr.getCenterPosition();
-            if (mIsScrolling) stopScroll();
+        if (curr.getID() == 0 && mCenterPosition < curr.getCenterX()) {
+            mCenterPosition = curr.getCenterX();
+            mGeometryAnimator.stopScroll();
         }
-        if (curr.getId() == mDataAdapter.getTotalNumber() - 1
-                && mCenterPosition > curr.getCenterPosition()) {
-            mCenterPosition = curr.getCenterPosition();
-            if (mIsScrolling) stopScroll();
+        if (curr.getID() == mDataAdapter.getTotalNumber() - 1
+                && mCenterPosition > curr.getCenterX()) {
+            mCenterPosition = curr.getCenterX();
+            mGeometryAnimator.stopScroll();
         }
     }
 
     private void layoutChildren() {
-        mIsScrolling = mScroller.computeScrollOffset();
-
-        if (mIsScrolling) mCenterPosition = mScroller.getCurrX();
+        if (mGeometryAnimator.hasNewGeometry()) {
+            mCenterPosition = mGeometryAnimator.getNewPosition();
+            mScale = mGeometryAnimator.getNewScale();
+        }
 
         adjustCenterPosition();
 
-        mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterPosition);
+        mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterPosition, mScale);
 
         // images on the left
         for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) {
             ViewInfo curr = mViewInfo[infoID];
             if (curr != null) {
                 ViewInfo next = mViewInfo[infoID + 1];
-                curr.setLeftPosition(next.getLeftPosition() - curr.getView().getMeasuredWidth());
-                curr.layoutIn(mDrawArea, mCenterPosition);
+                curr.setLeftPosition(
+                        next.getLeftPosition() - curr.getView().getMeasuredWidth());
+                curr.layoutIn(mDrawArea, mCenterPosition, mScale);
             }
         }
 
@@ -285,8 +356,9 @@ public class FilmStripView extends ViewGroup {
             ViewInfo curr = mViewInfo[infoID];
             if (curr != null) {
                 ViewInfo prev = mViewInfo[infoID - 1];
-                curr.setLeftPosition(prev.getLeftPosition() + prev.getView().getMeasuredWidth());
-                curr.layoutIn(mDrawArea, mCenterPosition);
+                curr.setLeftPosition(
+                        prev.getLeftPosition() + prev.getView().getMeasuredWidth());
+                curr.layoutIn(mDrawArea, mCenterPosition, mScale);
             }
         }
 
@@ -306,24 +378,43 @@ public class FilmStripView extends ViewGroup {
         layoutChildren();
     }
 
-    public void setDataAdapter(
-            DataAdapter adapter, ContentResolver resolver) {
+    public void setDataAdapter(DataAdapter adapter) {
         mDataAdapter = adapter;
         mDataAdapter.suggestSize(getMeasuredWidth(), getMeasuredHeight());
-        mDataAdapter.setDataListener(this);
-        mDataAdapter.requestLoad(resolver);
+        mDataAdapter.setListener(new DataAdapter.Listener() {
+            @Override
+            public void onDataLoaded() {
+                reload();
+            }
+
+            @Override
+            public void onDataInserted(int dataID) {
+            }
+
+            @Override
+            public void onDataRemoved(int dataID) {
+            }
+        });
     }
 
-    private void getInfo(int infoID, int dataID) {
-        View v = mDataAdapter.getView(mContext, dataID);
-        if (v == null) return;
-        v.setPadding(H_PADDING, 0, H_PADDING, 0);
-        addView(v);
-        ViewInfo info = new ViewInfo(dataID, v);
-        mViewInfo[infoID] = info;
+    public boolean isInCameraFullscreen() {
+        return (isAnchoredTo(0) && mScale == 1f
+                && getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW);
     }
 
-    public void onDataChanged() {
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (isInCameraFullscreen()) return false;
+        return true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        mGestureRecognizer.onTouchEvent(ev);
+        return true;
+    }
+
+    public void reload() {
         removeAllViews();
         int dataNumber = mDataAdapter.getTotalNumber();
         if (dataNumber == 0) return;
@@ -333,89 +424,315 @@ public class FilmStripView extends ViewGroup {
         // previous data exists.
         if (mViewInfo[mCurrentInfo] != null) {
             currentLeft = mViewInfo[mCurrentInfo].getLeftPosition();
-            currentData = mViewInfo[mCurrentInfo].getId();
+            currentData = mViewInfo[mCurrentInfo].getID();
         }
-        getInfo(mCurrentInfo, currentData);
+        mViewInfo[mCurrentInfo] = buildInfoFromData(currentData);
         mViewInfo[mCurrentInfo].setLeftPosition(currentLeft);
+        if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW
+                && currentLeft == 0) {
+            // we are in camera mode by default.
+            mGeometryAnimator.lockPosition(currentLeft);
+        }
         for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) {
             int infoID = mCurrentInfo + i;
             if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) {
-                getInfo(infoID, mViewInfo[infoID - 1].getId() + 1);
+                mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1);
             }
             infoID = mCurrentInfo - i;
             if (infoID >= 0 && mViewInfo[infoID + 1] != null) {
-                getInfo(infoID, mViewInfo[infoID + 1].getId() - 1);
+                mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1);
             }
         }
         layoutChildren();
     }
 
-    private void movePositionTo(int position) {
-        mScroller.startScroll(mCenterPosition, 0, position - mCenterPosition,
-                0, BACK_SCROLL_DURATION);
-        layoutChildren();
-    }
+    // GeometryAnimator controls all the geometry animations. It passively
+    // tells the geometry information on demand.
+    private class GeometryAnimator implements
+            ValueAnimator.AnimatorUpdateListener,
+            Animator.AnimatorListener {
+
+        private ValueAnimator mScaleAnimator;
+        private boolean mHasNewScale;
+        private float mNewScale;
+
+        private Scroller mScroller;
+        private boolean mHasNewPosition;
+        private DecelerateInterpolator mDecelerateInterpolator;
+
+        private boolean mCanStopScroll;
+        private boolean mCanStopScale;
+
+        private boolean mIsPositionLocked;
+        private int mLockedPosition;
+
+        private Runnable mPostAction;
+
+        GeometryAnimator(Context context) {
+            mScroller = new Scroller(context);
+            mHasNewPosition = false;
+            mScaleAnimator = new ValueAnimator();
+            mScaleAnimator.addUpdateListener(GeometryAnimator.this);
+            mScaleAnimator.addListener(GeometryAnimator.this);
+            mDecelerateInterpolator = new DecelerateInterpolator();
+            mCanStopScroll = true;
+            mCanStopScale = true;
+            mHasNewScale = false;
+        }
 
-    public void goToFirst() {
-        movePositionTo(0);
-    }
+        boolean hasNewGeometry() {
+            mHasNewPosition = mScroller.computeScrollOffset();
+            if (!mHasNewPosition) {
+                mCanStopScroll = true;
+            }
+            // If the position is locked, then we always return true to force
+            // the position value to use the locked value.
+            return (mHasNewPosition || mHasNewScale || mIsPositionLocked);
+        }
 
-    @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        return mGestureDetector.onTouchEvent(ev);
-    }
+        // Always call hasNewGeometry() before getting the new scale value.
+        float getNewScale() {
+            if (!mHasNewScale) return mScale;
+            mHasNewScale = false;
+            return mNewScale;
+        }
+
+        // Always call hasNewGeometry() before getting the new position value.
+        int getNewPosition() {
+            if (mIsPositionLocked) return mLockedPosition;
+            if (!mHasNewPosition) return mCenterPosition;
+            return mScroller.getCurrX();
+        }
 
-    private class MyGestureListener
-                extends GestureDetector.SimpleOnGestureListener {
+        void lockPosition(int pos) {
+            mIsPositionLocked = true;
+            mLockedPosition = pos;
+        }
 
-        @Override
-        public boolean onDoubleTap(MotionEvent e) {
-            float x = (float) e.getX();
-            float y = (float) e.getY();
-            for (int i = 0; i < BUFFER_SIZE; i++) {
-                if (mViewInfo[i] == null) continue;
-                View v = mViewInfo[i].getView();
-                if (x >= v.getLeft() && x < v.getRight()
-                        && y >= v.getTop() && y < v.getBottom()) {
-                    Log.v(TAG, "l, r, t, b " + v.getLeft() + ',' + v.getRight()
-                          + ',' + v.getTop() + ',' + v.getBottom());
-                    movePositionTo(mViewInfo[i].getCenterPosition());
-                    break;
-                }
+        void unlockPosition() {
+            if (mIsPositionLocked) {
+                // only when the position is previously locked we set the current
+                // position to make it consistent.
+                mCenterPosition = mLockedPosition;
+                mIsPositionLocked = false;
             }
+        }
+
+        void fling(int velocityX, int minX, int maxX) {
+            if (!stopScroll() || mIsPositionLocked) return;
+            mScroller.fling(mCenterPosition, 0, velocityX, 0, minX, maxX, 0, 0);
+        }
+
+        boolean stopScroll() {
+            if (!mCanStopScroll) return false;
+            mScroller.forceFinished(true);
+            mHasNewPosition = false;
             return true;
         }
 
+        boolean stopScale() {
+            if (!mCanStopScale) return false;
+            mScaleAnimator.cancel();
+            mHasNewScale = false;
+            return true;
+        }
+
+        void stop() {
+            stopScroll();
+            stopScale();
+        }
+
+        void scrollTo(int position, int duration, boolean interruptible) {
+            if (!stopScroll() || mIsPositionLocked) return;
+            mCanStopScroll = interruptible;
+            stopScroll();
+            mScroller.startScroll(mCenterPosition, 0, position - mCenterPosition,
+                    0, duration);
+        }
+
+        void scrollTo(int position, int duration) {
+            scrollTo(position, duration, true);
+        }
+
+        void scaleTo(float scale, int duration, boolean interruptible) {
+            if (!stopScale()) return;
+            mCanStopScale = interruptible;
+            mScaleAnimator.setDuration(duration);
+            mScaleAnimator.setFloatValues(mScale, scale);
+            mScaleAnimator.setInterpolator(mDecelerateInterpolator);
+            mScaleAnimator.start();
+            mHasNewScale = true;
+        }
+
+        void scaleTo(float scale, int duration) {
+            scaleTo(scale, duration, true);
+        }
+
+        void setPostAction(Runnable act) {
+            mPostAction = act;
+        }
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            mHasNewScale = true;
+            mNewScale = (Float) animation.getAnimatedValue();
+            layoutChildren();
+        }
+
+        @Override
+        public void onAnimationStart(Animator anim) {
+        }
+
+        @Override
+        public void onAnimationEnd(Animator anim) {
+            if (mPostAction != null) {
+                mPostAction.run();
+                mPostAction = null;
+            }
+            mCanStopScale = true;
+        }
+
+        @Override
+        public void onAnimationCancel(Animator anim) {
+            mPostAction = null;
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator anim) {
+        }
+    }
+
+    private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener {
+        // Indicating the current trend of scaling is up (>1) or down (<1).
+        private float mScaleTrend;
+
+        @Override
+        public boolean onSingleTapUp(float x, float y) {
+            return false;
+        }
+
+        @Override
+        public boolean onDoubleTap(float x, float y) {
+            return false;
+        }
+
         @Override
-        public boolean onDown(MotionEvent ev) {
-            if (mIsScrolling) stopScroll();
+        public boolean onDown(float x, float y) {
+            mGeometryAnimator.stop();
             return true;
         }
 
         @Override
-        public boolean onScroll(
-                MotionEvent e1, MotionEvent e2, float dx, float dy) {
-            stopScroll();
-            mCenterPosition += dx;
+        public boolean onScroll(float x, float y, float dx, float dy) {
+            int deltaX = (int) (dx / mScale);
+            if (deltaX > 0 && isInCameraFullscreen() ) {
+                mGeometryAnimator.unlockPosition();
+                mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false);
+            }
+
+            mCenterPosition += deltaX;
+
+            // Vertical part. Promote or demote.
+            int scaledDeltaY = (int) (dy / mScale);
+
+            for (int i = 0; i < BUFFER_SIZE; i++) {
+                if (mViewInfo[i] == null) continue;
+                Rect hitRect = new Rect();
+                View v = mViewInfo[i].getView();
+                v.getHitRect(hitRect);
+                if (hitRect.contains((int) x, (int) y)) {
+                    ImageData data = mDataAdapter.getImageData(mViewInfo[i].getID());
+                    if ((data.isActionSupported(ImageData.ACTION_DEMOTE) && dy > 0)
+                            || (data.isActionSupported(ImageData.ACTION_PROMOTE) && dy < 0)) {
+                        mViewInfo[i].setOffsetY(mViewInfo[i].getOffsetY() - dy);
+                    }
+                    break;
+                }
+            }
+
             layoutChildren();
             return true;
         }
 
         @Override
-        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
-                float velocityY) {
+        public boolean onFling(float velocityX, float velocityY) {
+            float scaledVelocityX = velocityX / mScale;
+            if (isInCameraFullscreen() && scaledVelocityX < 0) {
+                mGeometryAnimator.unlockPosition();
+                mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false);
+            }
             ViewInfo info = mViewInfo[mCurrentInfo];
             int w = getWidth();
             if (info == null) return true;
-            mScroller.fling(mCenterPosition, 0, (int) -velocityX, (int) velocityY,
+            mGeometryAnimator.fling((int) -scaledVelocityX,
                     // estimation of possible length on the left
-                    info.getLeftPosition() - info.getId() * w * 2,
+                    info.getLeftPosition() - info.getID() * w * 2,
                     // estimation of possible length on the right
                     info.getLeftPosition()
-                            + (mDataAdapter.getTotalNumber() - info.getId()) * w * 2,
-                    0, 0);
+                    + (mDataAdapter.getTotalNumber() - info.getID()) * w * 2);
+            layoutChildren();
+            return true;
+        }
+
+        @Override
+        public boolean onScaleBegin(float focusX, float focusY) {
+            if (isInCameraFullscreen()) return false;
+            mScaleTrend = 1f;
+            return true;
+        }
+
+        @Override
+        public boolean onScale(float focusX, float focusY, float scale) {
+            if (isInCameraFullscreen()) return false;
+
+            mScaleTrend = mScaleTrend * 0.5f + scale * 0.5f;
+            mScale *= scale;
+            if (mScale <= FILM_STRIP_SCALE) mScale = FILM_STRIP_SCALE;
+            if (mScale >= MAX_SCALE) mScale = MAX_SCALE;
             layoutChildren();
             return true;
         }
+
+        @Override
+        public void onScaleEnd() {
+            if (mScaleTrend >= 1f) {
+                if (mScale != 1f) {
+                    mGeometryAnimator.scaleTo(1f, DURATION_GEOMETRY_ADJUST, false);
+                }
+
+                if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) {
+                    if (isAnchoredTo(0)) {
+                        mGeometryAnimator.lockPosition(mViewInfo[mCurrentInfo].getCenterX());
+                    } else {
+                        mGeometryAnimator.scrollTo(
+                                mViewInfo[mCurrentInfo].getCenterX(),
+                                DURATION_GEOMETRY_ADJUST, false);
+                        mGeometryAnimator.setPostAction(mLockPositionRunnable);
+                    }
+                }
+            } else {
+                // Scale down to film strip mode.
+                if (mScale == FILM_STRIP_SCALE) {
+                    mGeometryAnimator.unlockPosition();
+                    return;
+                }
+                mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false);
+                mGeometryAnimator.setPostAction(mUnlockPositionRunnable);
+            }
+        }
+
+        private Runnable mLockPositionRunnable = new Runnable() {
+            @Override
+            public void run() {
+                mGeometryAnimator.lockPosition(mViewInfo[mCurrentInfo].getCenterX());
+            }
+        };
+
+        private Runnable mUnlockPositionRunnable = new Runnable() {
+            @Override
+            public void run() {
+                mGeometryAnimator.unlockPosition();
+            }
+        };
     }
 }