OSDN Git Service

Reduce view add/removes when loading photos
[android-x86/packages-apps-Camera2.git] / src / com / android / camera / widget / FilmstripView.java
index 319692d..897bd36 100644 (file)
@@ -34,7 +34,6 @@ import android.os.SystemClock;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.SparseArray;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -43,24 +42,54 @@ import android.view.animation.DecelerateInterpolator;
 import android.widget.Scroller;
 
 import com.android.camera.CameraActivity;
+import com.android.camera.data.FilmstripItem;
+import com.android.camera.data.FilmstripItem.VideoClickedCallback;
 import com.android.camera.debug.Log;
-import com.android.camera.filmstrip.DataAdapter;
 import com.android.camera.filmstrip.FilmstripController;
-import com.android.camera.filmstrip.ImageData;
+import com.android.camera.filmstrip.FilmstripDataAdapter;
 import com.android.camera.ui.FilmstripGestureRecognizer;
 import com.android.camera.ui.ZoomView;
 import com.android.camera.util.ApiHelper;
 import com.android.camera.util.CameraUtil;
 import com.android.camera2.R;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayDeque;
 import java.util.Arrays;
 import java.util.Queue;
 
 public class FilmstripView extends ViewGroup {
+    /**
+     * An action callback to be used for actions on the local media data items.
+     */
+    public static class PlayVideoIntent implements VideoClickedCallback {
+        private final WeakReference<CameraActivity> mActivity;
+
+        /**
+         * The given activity is used to start intents. It is wrapped in a weak
+         * reference to prevent leaks.
+         */
+        public PlayVideoIntent(CameraActivity activity) {
+            mActivity = new WeakReference<CameraActivity>(activity);
+        }
+
+        /**
+         * Fires an intent to play the video with the given URI and title.
+         */
+        @Override
+        public void playVideo(Uri uri, String title) {
+            CameraActivity activity = mActivity.get();
+            if (activity != null) {
+              CameraUtil.playVideo(activity, uri, title);
+            }
+        }
+    }
+
+
     private static final Log.Tag TAG = new Log.Tag("FilmstripView");
 
     private static final int BUFFER_SIZE = 5;
+    private static final int BUFFER_CENTER = (BUFFER_SIZE - 1) / 2;
     private static final int GEOMETRY_ADJUST_TIME_MS = 400;
     private static final int SNAP_IN_CENTER_TIME_MS = 600;
     private static final float FLING_COASTING_DURATION_S = 0.05f;
@@ -89,17 +118,17 @@ public class FilmstripView extends ViewGroup {
     private static final float MOUSE_SCROLL_FACTOR = 128f;
 
     private CameraActivity mActivity;
+    private VideoClickedCallback mVideoClickedCallback;
     private FilmstripGestureRecognizer mGestureRecognizer;
     private FilmstripGestureRecognizer.Listener mGestureListener;
-    private DataAdapter mDataAdapter;
+    private FilmstripDataAdapter mDataAdapter;
     private int mViewGapInPixel;
     private final Rect mDrawArea = new Rect();
 
-    private final int mCurrentItem = (BUFFER_SIZE - 1) / 2;
     private float mScale;
-    private MyController mController;
+    private FilmstripControllerImpl mController;
     private int mCenterX = -1;
-    private final ViewItem[] mViewItem = new ViewItem[BUFFER_SIZE];
+    private final ViewItem[] mViewItems = new ViewItem[BUFFER_SIZE];
 
     private FilmstripController.FilmstripListener mListener;
     private ZoomView mZoomView = null;
@@ -111,44 +140,51 @@ public class FilmstripView extends ViewGroup {
 
     // This is true if and only if the user is scrolling,
     private boolean mIsUserScrolling;
-    private int mDataIdOnUserScrolling;
+    private int mAdapterIndexUserIsScrollingOver;
     private float mOverScaleFactor = 1f;
 
     private boolean mFullScreenUIHidden = false;
-    private final SparseArray<Queue<View>> recycledViews = new SparseArray<Queue<View>>();
+    private final SparseArray<Queue<View>> recycledViews = new SparseArray<>();
 
     /**
      * A helper class to tract and calculate the view coordination.
      */
-    private class ViewItem {
-        private int mDataId;
+    private static class ViewItem {
+        private int mIndex;
         /** The position of the left of the view in the whole filmstrip. */
         private int mLeftPosition;
         private final View mView;
-        private final ImageData mData;
+        private FilmstripItem mData;
         private final RectF mViewArea;
         private boolean mMaximumBitmapRequested;
 
         private ValueAnimator mTranslationXAnimator;
         private ValueAnimator mTranslationYAnimator;
         private ValueAnimator mAlphaAnimator;
+        private final FilmstripView mFilmstrip;
 
         /**
          * Constructor.
          *
-         * @param id The id of the data from
-         *            {@link com.android.camera.filmstrip.DataAdapter}.
+         * @param index The index of the data from
+         *            {@link com.android.camera.filmstrip.FilmstripDataAdapter}.
          * @param v The {@code View} representing the data.
          */
-        public ViewItem(int id, View v, ImageData data) {
+        public ViewItem(int index, View v, FilmstripItem data, FilmstripView filmstrip) {
             v.setPivotX(0f);
             v.setPivotY(0f);
-            mDataId = id;
+            mIndex = index;
             mData = data;
             mView = v;
             mMaximumBitmapRequested = false;
             mLeftPosition = -1;
             mViewArea = new RectF();
+            mFilmstrip = filmstrip;
+        }
+
+        public void setData(FilmstripItem item) {
+            mData = item;
+            mMaximumBitmapRequested = false;
         }
 
         public boolean isMaximumBitmapRequested() {
@@ -160,19 +196,19 @@ public class FilmstripView extends ViewGroup {
         }
 
         /**
-         * Returns the data id from
-         * {@link com.android.camera.filmstrip.DataAdapter}.
+         * Returns the index from
+         * {@link com.android.camera.filmstrip.FilmstripDataAdapter}.
          */
-        public int getId() {
-            return mDataId;
+        public int getAdapterIndex() {
+            return mIndex;
         }
 
         /**
-         * Sets the data id from
-         * {@link com.android.camera.filmstrip.DataAdapter}.
+         * Sets the index used in the
+         * {@link com.android.camera.filmstrip.FilmstripDataAdapter}.
          */
-        public void setId(int id) {
-            mDataId = id;
+        public void setIndex(int index) {
+            mIndex = index;
         }
 
         /** Sets the left position of the view in the whole filmstrip. */
@@ -187,22 +223,22 @@ public class FilmstripView extends ViewGroup {
 
         /** Returns the translation of Y regarding the view scale. */
         public float getTranslationY() {
-            return mView.getTranslationY() / mScale;
+            return mView.getTranslationY() / mFilmstrip.mScale;
         }
 
         /** Returns the translation of X regarding the view scale. */
         public float getTranslationX() {
-            return mView.getTranslationX() / mScale;
+            return mView.getTranslationX() / mFilmstrip.mScale;
         }
 
         /** Sets the translation of Y regarding the view scale. */
         public void setTranslationY(float transY) {
-            mView.setTranslationY(transY * mScale);
+            mView.setTranslationY(transY * mFilmstrip.mScale);
         }
 
         /** Sets the translation of X regarding the view scale. */
         public void setTranslationX(float transX) {
-            mView.setTranslationX(transX * mScale);
+            mView.setTranslationX(transX * mFilmstrip.mScale);
         }
 
         /** Forwarding of {@link android.view.View#setAlpha(float)}. */
@@ -242,7 +278,7 @@ public class FilmstripView extends ViewGroup {
                         // translation X because the translation X of the view is
                         // touched in onLayout(). See the documentation of
                         // animateTranslationX().
-                        invalidate();
+                        mFilmstrip.invalidate();
                     }
                 });
             }
@@ -307,7 +343,7 @@ public class FilmstripView extends ViewGroup {
 
         /** Adjusts the translation of X regarding the view scale. */
         public void translateXScaledBy(float transX) {
-            setTranslationX(getTranslationX() + transX * mScale);
+            setTranslationX(getTranslationX() + transX * mFilmstrip.mScale);
         }
 
         /**
@@ -332,20 +368,19 @@ public class FilmstripView extends ViewGroup {
         }
 
         /**
-         * Notifies the {@link com.android.camera.filmstrip.DataAdapter} to
+         * Notifies the {@link com.android.camera.filmstrip.FilmstripDataAdapter} to
          * resize the view.
          */
-        public void resizeView(Context context, int w, int h) {
-            mDataAdapter.resizeView(context, mDataId, mView, w, h);
+        public void resizeView(int w, int h) {
+            mFilmstrip.mDataAdapter.resizeView(mIndex, mView, w, h);
         }
 
         /**
          * Adds the view of the data to the view hierarchy if necessary.
          */
         public void addViewToHierarchy() {
-            if (indexOfChild(mView) < 0) {
-                mData.prepare();
-                addView(mView);
+            if (mFilmstrip.indexOfChild(mView) < 0) {
+                mFilmstrip.addView(mView);
             }
 
             setVisibility(View.VISIBLE);
@@ -362,10 +397,10 @@ public class FilmstripView extends ViewGroup {
          *                          regardless of the view type.
          */
         public void removeViewFromHierarchy(boolean force) {
-            if (force || mData.getViewType() != ImageData.VIEW_TYPE_STICKY) {
-                removeView(mView);
+            if (force || !mData.getAttributes().isSticky()) {
+                mFilmstrip.removeView(mView);
                 mData.recycle(mView);
-                recycleView(mView, mDataId);
+                mFilmstrip.recycleView(mView, mIndex);
             } else {
                 setVisibility(View.INVISIBLE);
             }
@@ -376,7 +411,7 @@ public class FilmstripView extends ViewGroup {
          * {@link #bringChildToFront(android.view.View)}
          */
         public void bringViewToFront() {
-            bringChildToFront(mView);
+            mFilmstrip.bringChildToFront(mView);
         }
 
         /**
@@ -482,7 +517,7 @@ public class FilmstripView extends ViewGroup {
                         // translation X because the translation X of the view is
                         // touched in onLayout(). See the documentation of
                         // animateTranslationX().
-                        invalidate();
+                        mFilmstrip.invalidate();
                     }
                 });
             }
@@ -556,7 +591,7 @@ public class FilmstripView extends ViewGroup {
 
         @Override
         public String toString() {
-            return "DataID = " + mDataId + "\n\t left = " + mLeftPosition
+            return "AdapterIndex = " + mIndex + "\n\t left = " + mLeftPosition
                     + "\n\t viewArea = " + mViewArea
                     + "\n\t centerX = " + getCenterX()
                     + "\n\t view MeasuredSize = "
@@ -587,15 +622,16 @@ public class FilmstripView extends ViewGroup {
     private void init(CameraActivity cameraActivity) {
         setWillNotDraw(false);
         mActivity = cameraActivity;
+        mVideoClickedCallback = new PlayVideoIntent(mActivity);
         mScale = 1.0f;
-        mDataIdOnUserScrolling = 0;
-        mController = new MyController(cameraActivity);
+        mAdapterIndexUserIsScrollingOver = 0;
+        mController = new FilmstripControllerImpl();
         mViewAnimInterpolator = new DecelerateInterpolator();
         mZoomView = new ZoomView(cameraActivity);
         mZoomView.setVisibility(GONE);
         addView(mZoomView);
 
-        mGestureListener = new MyGestureReceiver();
+        mGestureListener = new FilmstripGestures();
         mGestureRecognizer =
                 new FilmstripGestureRecognizer(cameraActivity, mGestureListener);
         mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop);
@@ -640,7 +676,7 @@ public class FilmstripView extends ViewGroup {
                         case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
                             // Prevent the view group itself from being selected.
                             // Instead, select the item in the center
-                            final ViewItem currentItem = mViewItem[mCurrentItem];
+                            final ViewItem currentItem = mViewItems[BUFFER_CENTER];
                             currentItem.getView().performAccessibilityAction(action, args);
                             return true;
                         }
@@ -651,7 +687,7 @@ public class FilmstripView extends ViewGroup {
         });
     }
 
-    private void recycleView(View view, int dataId) {
+    private void recycleView(View view, int index) {
         final int viewType = (Integer) view.getTag(R.id.mediadata_tag_viewtype);
         if (viewType > 0) {
             Queue<View> recycledViewsForType = recycledViews.get(viewType);
@@ -663,14 +699,17 @@ public class FilmstripView extends ViewGroup {
         }
     }
 
-    private View getRecycledView(int dataId) {
-        final int viewType = mDataAdapter.getItemViewType(dataId);
+    private View getRecycledView(int index) {
+        final int viewType = mDataAdapter.getItemViewType(index);
         Queue<View> recycledViewsForType = recycledViews.get(viewType);
-        View result = null;
+        View view = null;
         if (recycledViewsForType != null) {
-            result = recycledViewsForType.poll();
+            view = recycledViewsForType.poll();
+        }
+        if (view != null) {
+            view.setVisibility(View.GONE);
         }
-        return result;
+        return view;
     }
 
     /**
@@ -686,7 +725,7 @@ public class FilmstripView extends ViewGroup {
      * Returns the draw area width of the current item.
      */
     public int  getCurrentItemLeft() {
-        return mViewItem[mCurrentItem].getDrawAreaLeft();
+        return mViewItems[BUFFER_CENTER].getDrawAreaLeft();
     }
 
     private void setListener(FilmstripController.FilmstripListener l) {
@@ -701,24 +740,24 @@ public class FilmstripView extends ViewGroup {
      * Called after current item or zoom level has changed.
      */
     public void zoomAtIndexChanged() {
-        if (mViewItem[mCurrentItem] == null) {
+        if (mViewItems[BUFFER_CENTER] == null) {
             return;
         }
-        int id = mViewItem[mCurrentItem].getId();
-        mListener.onZoomAtIndexChanged(id, mScale);
+        int index = mViewItems[BUFFER_CENTER].getAdapterIndex();
+        mListener.onZoomAtIndexChanged(index, mScale);
     }
 
     /**
      * Checks if the data is at the center.
      *
-     * @param id The id of the data to check.
+     * @param index The index of the item in the data adapter to check.
      * @return {@code True} if the data is currently at the center.
      */
-    private boolean isDataAtCenter(int id) {
-        if (mViewItem[mCurrentItem] == null) {
+    private boolean isItemAtIndexCentered(int index) {
+        if (mViewItems[BUFFER_CENTER] == null) {
             return false;
         }
-        if (mViewItem[mCurrentItem].getId() == id
+        if (mViewItems[BUFFER_CENTER].getAdapterIndex() == index
                 && isCurrentItemCentered()) {
             return true;
         }
@@ -726,15 +765,19 @@ public class FilmstripView extends ViewGroup {
     }
 
     private void measureViewItem(ViewItem item, int boundWidth, int boundHeight) {
-        int id = item.getId();
-        ImageData imageData = mDataAdapter.getImageData(id);
+        int index = item.getAdapterIndex();
+        FilmstripItem imageData = mDataAdapter.getFilmstripItemAt(index);
         if (imageData == null) {
-            Log.e(TAG, "trying to measure a null item");
+            Log.w(TAG, "measureViewItem() - Trying to measure a null item!");
             return;
         }
 
-        Point dim = CameraUtil.resizeToFill(imageData.getWidth(), imageData.getHeight(),
-                imageData.getRotation(), boundWidth, boundHeight);
+        Point dim = CameraUtil.resizeToFill(
+              imageData.getData().getDimensions().getWidth(),
+              imageData.getData().getDimensions().getHeight(),
+              imageData.getData().getOrientation(),
+              boundWidth,
+              boundHeight);
 
         item.measure(MeasureSpec.makeMeasureSpec(dim.x, MeasureSpec.EXACTLY),
                 MeasureSpec.makeMeasureSpec(dim.y, MeasureSpec.EXACTLY));
@@ -751,7 +794,7 @@ public class FilmstripView extends ViewGroup {
             return;
         }
 
-        for (ViewItem item : mViewItem) {
+        for (ViewItem item : mViewItems) {
             if (item != null) {
                 measureViewItem(item, boundWidth, boundHeight);
             }
@@ -762,12 +805,11 @@ public class FilmstripView extends ViewGroup {
                 MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.EXACTLY));
     }
 
-    private int findTheNearestView(int pointX) {
-
+    private int findTheNearestView(int viewX) {
         int nearest = 0;
         // Find the first non-null ViewItem.
         while (nearest < BUFFER_SIZE
-                && (mViewItem[nearest] == null || mViewItem[nearest].getLeftPosition() == -1)) {
+                && (mViewItems[nearest] == null || mViewItems[nearest].getLeftPosition() == -1)) {
             nearest++;
         }
         // No existing available ViewItem
@@ -775,32 +817,32 @@ public class FilmstripView extends ViewGroup {
             return -1;
         }
 
-        int min = Math.abs(pointX - mViewItem[nearest].getCenterX());
+        int min = Math.abs(viewX - mViewItems[nearest].getCenterX());
 
-        for (int itemID = nearest + 1; itemID < BUFFER_SIZE && mViewItem[itemID] != null; itemID++) {
+        for (int i = nearest + 1; i < BUFFER_SIZE && mViewItems[i] != null; i++) {
             // Not measured yet.
-            if (mViewItem[itemID].getLeftPosition() == -1) {
+            if (mViewItems[i].getLeftPosition() == -1) {
                 continue;
             }
 
-            int c = mViewItem[itemID].getCenterX();
-            int dist = Math.abs(pointX - c);
+            int centerX = mViewItems[i].getCenterX();
+            int dist = Math.abs(viewX - centerX);
             if (dist < min) {
                 min = dist;
-                nearest = itemID;
+                nearest = i;
             }
         }
         return nearest;
     }
 
-    private ViewItem buildItemFromData(int dataID) {
+    private ViewItem buildViewItemAt(int index) {
         if (mActivity.isDestroyed()) {
             // Loading item data is call from multiple AsyncTasks and the
-            // activity may be finished when buildItemFromData is called.
+            // activity may be finished when buildViewItemAt is called.
             Log.d(TAG, "Activity destroyed, don't load data");
             return null;
         }
-        ImageData data = mDataAdapter.getImageData(dataID);
+        FilmstripItem data = mDataAdapter.getFilmstripItemAt(index);
         if (data == null) {
             return null;
         }
@@ -813,46 +855,53 @@ public class FilmstripView extends ViewGroup {
         Log.v(TAG, "suggesting item bounds: " + width + "x" + height);
         mDataAdapter.suggestViewSizeBound(width, height);
 
-        data.prepare();
-        View recycled = getRecycledView(dataID);
-        View v = mDataAdapter.getView(mActivity, recycled, dataID);
+        View recycled = getRecycledView(index);
+        View v = mDataAdapter.getView(recycled, index,
+              mVideoClickedCallback);
         if (v == null) {
             return null;
         }
-        ViewItem item = new ViewItem(dataID, v, data);
+        ViewItem item = new ViewItem(index, v, data, this);
         item.addViewToHierarchy();
         return item;
     }
 
-    private void checkItemAtMaxSize() {
-        ViewItem item = mViewItem[mCurrentItem];
-        if (item.isMaximumBitmapRequested()) {
+    private void ensureItemAtMaxSize(int bufferIndex) {
+        ViewItem item = mViewItems[bufferIndex];
+        if (item == null || item.isMaximumBitmapRequested()) {
             return;
-        };
+        }
         item.setMaximumBitmapRequested();
         // Request full size bitmap, or max that DataAdapter will create.
-        int id = item.getId();
-        int h = mDataAdapter.getImageData(id).getHeight();
-        int w = mDataAdapter.getImageData(id).getWidth();
-        item.resizeView(mActivity, w, h);
+        int index = item.getAdapterIndex();
+        int h = mDataAdapter.getFilmstripItemAt(index).getData().getDimensions().getHeight();
+        int w = mDataAdapter.getFilmstripItemAt(index).getData().getDimensions().getWidth();
+        item.resizeView(w, h);
+    }
+
+    private void ensureBufferItemsAtMaxSize() {
+        for(int i = 0; i < BUFFER_SIZE; i++) {
+            ensureItemAtMaxSize(i);
+        }
     }
 
-    private void removeItem(int itemID) {
-        if (itemID >= mViewItem.length || mViewItem[itemID] == null) {
+    private void removeItem(int bufferIndex) {
+        if (bufferIndex >= mViewItems.length || mViewItems[bufferIndex] == null) {
             return;
         }
-        ImageData data = mDataAdapter.getImageData(mViewItem[itemID].getId());
+        FilmstripItem data = mDataAdapter.getFilmstripItemAt(
+              mViewItems[bufferIndex].getAdapterIndex());
         if (data == null) {
-            Log.e(TAG, "trying to remove a null item");
+            Log.w(TAG, "removeItem() - Trying to remove a null item!");
             return;
         }
-        mViewItem[itemID].removeViewFromHierarchy(false);
-        mViewItem[itemID] = null;
+        mViewItems[bufferIndex].removeViewFromHierarchy(false);
+        mViewItems[bufferIndex] = null;
     }
 
     /**
      * We try to keep the one closest to the center of the screen at position
-     * mCurrentItem.
+     * BUFFER_CENTER.
      */
     private void stepIfNeeded() {
         if (!inFilmstrip() && !inFullScreen()) {
@@ -860,45 +909,54 @@ public class FilmstripView extends ViewGroup {
             // not in transition.
             return;
         }
-        final int nearest = findTheNearestView(mCenterX);
-        // no change made.
-        if (nearest == -1 || nearest == mCurrentItem) {
+        final int nearestBufferIndex = findTheNearestView(mCenterX);
+        // if the nearest view is the current view, or there is no nearest
+        // view, then we do not need to adjust the view buffers.
+        if (nearestBufferIndex == -1 || nearestBufferIndex == BUFFER_CENTER) {
             return;
         }
-        int prevDataId = (mViewItem[mCurrentItem] == null ? -1 : mViewItem[mCurrentItem].getId());
-        final int adjust = nearest - mCurrentItem;
+        int prevIndex = (mViewItems[BUFFER_CENTER] == null ? -1 :
+              mViewItems[BUFFER_CENTER].getAdapterIndex());
+        final int adjust = nearestBufferIndex - BUFFER_CENTER;
         if (adjust > 0) {
+            // Remove from beginning of the buffer.
             for (int k = 0; k < adjust; k++) {
                 removeItem(k);
             }
+            // Shift items inside the buffer
             for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
-                mViewItem[k] = mViewItem[k + adjust];
+                mViewItems[k] = mViewItems[k + adjust];
             }
+            // Fill the end with new items.
             for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
-                mViewItem[k] = null;
-                if (mViewItem[k - 1] != null) {
-                    mViewItem[k] = buildItemFromData(mViewItem[k - 1].getId() + 1);
+                mViewItems[k] = null;
+                if (mViewItems[k - 1] != null) {
+                    mViewItems[k] = buildViewItemAt(mViewItems[k - 1].getAdapterIndex() + 1);
                 }
             }
             adjustChildZOrder();
         } else {
+            // Remove from the end of the buffer
             for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
                 removeItem(k);
             }
+            // Shift items inside the buffer
             for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
-                mViewItem[k] = mViewItem[k + adjust];
+                mViewItems[k] = mViewItems[k + adjust];
             }
+            // Fill the beginning with new items.
             for (int k = -1 - adjust; k >= 0; k--) {
-                mViewItem[k] = null;
-                if (mViewItem[k + 1] != null) {
-                    mViewItem[k] = buildItemFromData(mViewItem[k + 1].getId() - 1);
+                mViewItems[k] = null;
+                if (mViewItems[k + 1] != null) {
+                    mViewItems[k] = buildViewItemAt(mViewItems[k + 1].getAdapterIndex() - 1);
                 }
             }
         }
         invalidate();
         if (mListener != null) {
-            mListener.onDataFocusChanged(prevDataId, mViewItem[mCurrentItem].getId());
-            final int firstVisible = mViewItem[mCurrentItem].getId() - 2;
+            mListener.onDataFocusChanged(prevIndex, mViewItems[BUFFER_CENTER]
+                  .getAdapterIndex());
+            final int firstVisible = mViewItems[BUFFER_CENTER].getAdapterIndex() - 2;
             final int visibleItemCount = firstVisible + BUFFER_SIZE;
             final int totalItemCount = mDataAdapter.getTotalNumber();
             mListener.onScroll(firstVisible, visibleItemCount, totalItemCount);
@@ -913,45 +971,46 @@ public class FilmstripView extends ViewGroup {
      * @return Whether clamp happened.
      */
     private boolean clampCenterX() {
-        ViewItem curr = mViewItem[mCurrentItem];
-        if (curr == null) {
+        ViewItem currentItem = mViewItems[BUFFER_CENTER];
+        if (currentItem == null) {
             return false;
         }
 
         boolean stopScroll = false;
-        if (curr.getId() == 1 && mCenterX < curr.getCenterX() && mDataIdOnUserScrolling > 1 &&
-                mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY &&
+        if (currentItem.getAdapterIndex() == 1 && mCenterX < currentItem.getCenterX()
+              && mAdapterIndexUserIsScrollingOver > 1 &&
+                mDataAdapter.getFilmstripItemAt(0).getAttributes().isSticky() &&
                 mController.isScrolling()) {
             stopScroll = true;
         } else {
-            if (curr.getId() == 0 && mCenterX < curr.getCenterX()) {
+            if (currentItem.getAdapterIndex() == 0 && mCenterX < currentItem.getCenterX()) {
                 // Stop at the first ViewItem.
                 stopScroll = true;
             }
         }
-        if (curr.getId() == mDataAdapter.getTotalNumber() - 1
-                && mCenterX > curr.getCenterX()) {
+        if (currentItem.getAdapterIndex() == mDataAdapter.getTotalNumber() - 1
+                && mCenterX > currentItem.getCenterX()) {
             // Stop at the end.
             stopScroll = true;
         }
 
         if (stopScroll) {
-            mCenterX = curr.getCenterX();
+            mCenterX = currentItem.getCenterX();
         }
 
         return stopScroll;
     }
 
     /**
-     * Reorders the child views to be consistent with their data ID. This method
+     * Reorders the child views to be consistent with their index. This method
      * should be called after adding/removing views.
      */
     private void adjustChildZOrder() {
         for (int i = BUFFER_SIZE - 1; i >= 0; i--) {
-            if (mViewItem[i] == null) {
+            if (mViewItems[i] == null) {
                 continue;
             }
-            mViewItem[i].bringViewToFront();
+            mViewItems[i].bringViewToFront();
         }
         // ZoomView is a special case to always be in the front. In L set to
         // max elevation to make sure ZoomView is above other elevated views.
@@ -961,42 +1020,45 @@ public class FilmstripView extends ViewGroup {
         }
     }
 
-    @TargetApi(Build.VERSION_CODES.L)
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
     private void setMaxElevation(View v) {
         v.setElevation(Float.MAX_VALUE);
     }
 
     /**
-     * Returns the ID of the current item, or -1 if there is no data.
+     * Returns the index of the current item, or -1 if there is no data.
      */
-    private int getCurrentId() {
-        ViewItem current = mViewItem[mCurrentItem];
+    private int getCurrentItemAdapterIndex() {
+        ViewItem current = mViewItems[BUFFER_CENTER];
         if (current == null) {
             return -1;
         }
-        return current.getId();
+        return current.getAdapterIndex();
     }
 
     /**
      * Keep the current item in the center. This functions does not check if the
      * current item is null.
      */
-    private void snapInCenter() {
-        final ViewItem currItem = mViewItem[mCurrentItem];
+    private void scrollCurrentItemToCenter() {
+        final ViewItem currItem = mViewItems[BUFFER_CENTER];
         if (currItem == null) {
             return;
         }
         final int currentViewCenter = currItem.getCenterX();
         if (mController.isScrolling() || mIsUserScrolling
                 || isCurrentItemCentered()) {
+            Log.d(TAG, "[fling] mController.isScrolling() - " + mController.isScrolling());
             return;
         }
 
         int snapInTime = (int) (SNAP_IN_CENTER_TIME_MS
                 * ((float) Math.abs(mCenterX - currentViewCenter))
                 / mDrawArea.width());
+
+        Log.d(TAG, "[fling] Scroll to center.");
         mController.scrollToPosition(currentViewCenter,
-                snapInTime, false);
+              snapInTime, false);
         if (isViewTypeSticky(currItem) && !mController.isScaling() && mScale != FULL_SCREEN_SCALE) {
             // Now going to full screen camera
             mController.goToFullScreen();
@@ -1009,24 +1071,24 @@ public class FilmstripView extends ViewGroup {
      * which occupies the whole screen. The other left ones are put on the left
      * side in full scales. Does nothing if there's no next item.
      *
-     * @param currItem The item ID of the current one to be translated.
+     * @param index The index of the current one to be translated.
      * @param drawAreaWidth The width of the current draw area.
      * @param scaleFraction A {@code float} between 0 and 1. 0 if the current
      *            scale is {@code FILM_STRIP_SCALE}. 1 if the current scale is
      *            {@code FULL_SCREEN_SCALE}.
      */
     private void translateLeftViewItem(
-            int currItem, int drawAreaWidth, float scaleFraction) {
-        if (currItem < 0 || currItem > BUFFER_SIZE - 1) {
-            Log.e(TAG, "currItem id out of bound.");
+            int index, int drawAreaWidth, float scaleFraction) {
+        if (index < 0 || index > BUFFER_SIZE - 1) {
+            Log.w(TAG, "translateLeftViewItem() - Index out of bound!");
             return;
         }
 
-        final ViewItem curr = mViewItem[currItem];
-        final ViewItem next = mViewItem[currItem + 1];
+        final ViewItem curr = mViewItems[index];
+        final ViewItem next = mViewItems[index + 1];
         if (curr == null || next == null) {
-            Log.e(TAG, "Invalid view item (curr or next == null). curr = "
-                    + currItem);
+            Log.w(TAG, "translateLeftViewItem() - Invalid view item (curr or next == null). curr = "
+                    + index);
             return;
         }
 
@@ -1040,7 +1102,8 @@ public class FilmstripView extends ViewGroup {
         curr.setVisibility(VISIBLE);
 
         if (inFullScreen()) {
-            curr.setTranslationX(translate * (mCenterX - currCenterX) / (nextCenterX - currCenterX));
+            curr.setTranslationX(translate * (mCenterX - currCenterX) /
+                  (nextCenterX - currCenterX));
         } else {
             curr.setTranslationX(translate);
         }
@@ -1050,46 +1113,46 @@ public class FilmstripView extends ViewGroup {
      * Fade out the {@link ViewItem} on the right of the current one in
      * full-screen layout. Does nothing if there's no previous item.
      *
-     * @param currItemId The ID of the item to fade.
+     * @param bufferIndex The index of the item in the buffer to fade.
      */
-    private void fadeAndScaleRightViewItem(int currItemId) {
-        if (currItemId < 1 || currItemId > BUFFER_SIZE) {
-            Log.e(TAG, "currItem id out of bound.");
+    private void fadeAndScaleRightViewItem(int bufferIndex) {
+        if (bufferIndex < 1 || bufferIndex > BUFFER_SIZE) {
+            Log.w(TAG, "fadeAndScaleRightViewItem() - bufferIndex out of bound!");
             return;
         }
 
-        final ViewItem currItem = mViewItem[currItemId];
-        final ViewItem prevItem = mViewItem[currItemId - 1];
-        if (currItem == null || prevItem == null) {
-            Log.e(TAG, "Invalid view item (curr or prev == null). curr = "
-                    + currItemId);
+        final ViewItem item = mViewItems[bufferIndex];
+        final ViewItem previousItem = mViewItems[bufferIndex - 1];
+        if (item == null || previousItem == null) {
+            Log.w(TAG, "fadeAndScaleRightViewItem() - Invalid view item (curr or prev == null)."
+                  + "curr = " + bufferIndex);
             return;
         }
 
-        if (currItemId > mCurrentItem + 1) {
-            // Every item not right next to the mCurrentItem is invisible.
-            currItem.setVisibility(INVISIBLE);
+        if (bufferIndex > BUFFER_CENTER + 1) {
+            // Every item not right next to the BUFFER_CENTER is invisible.
+            item.setVisibility(INVISIBLE);
             return;
         }
-        final int prevCenterX = prevItem.getCenterX();
+        final int prevCenterX = previousItem.getCenterX();
         if (mCenterX <= prevCenterX) {
             // Shortcut. If the position is at the center of the previous one,
             // set to invisible too.
-            currItem.setVisibility(INVISIBLE);
+            item.setVisibility(INVISIBLE);
             return;
         }
-        final int currCenterX = currItem.getCenterX();
+        final int currCenterX = item.getCenterX();
         final float fadeDownFraction =
                 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
-        currItem.layoutWithTranslationX(mDrawArea, currCenterX,
-                FILM_STRIP_SCALE + (1f - FILM_STRIP_SCALE) * fadeDownFraction);
-        currItem.setAlpha(fadeDownFraction);
-        currItem.setTranslationX(0);
-        currItem.setVisibility(VISIBLE);
+        item.layoutWithTranslationX(mDrawArea, currCenterX,
+              FILM_STRIP_SCALE + (1f - FILM_STRIP_SCALE) * fadeDownFraction);
+        item.setAlpha(fadeDownFraction);
+        item.setTranslationX(0);
+        item.setVisibility(VISIBLE);
     }
 
     private void layoutViewItems(boolean layoutChanged) {
-        if (mViewItem[mCurrentItem] == null ||
+        if (mViewItems[BUFFER_CENTER] == null ||
                 mDrawArea.width() == 0 ||
                 mDrawArea.height() == 0) {
             return;
@@ -1098,8 +1161,8 @@ public class FilmstripView extends ViewGroup {
         // If the layout changed, we need to adjust the current position so
         // that if an item is centered before the change, it's still centered.
         if (layoutChanged) {
-            mViewItem[mCurrentItem].setLeftPosition(
-                    mCenterX - mViewItem[mCurrentItem].getMeasuredWidth() / 2);
+            mViewItems[BUFFER_CENTER].setLeftPosition(
+                  mCenterX - mViewItems[BUFFER_CENTER].getMeasuredWidth() / 2);
         }
 
         if (inZoomView()) {
@@ -1118,26 +1181,26 @@ public class FilmstripView extends ViewGroup {
         // first.
 
         // Left items.
-        for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
-            final ViewItem curr = mViewItem[itemID];
+        for (int i = BUFFER_CENTER - 1; i >= 0; i--) {
+            final ViewItem curr = mViewItems[i];
             if (curr == null) {
                 break;
             }
 
             // First, layout relatively to the next one.
-            final int currLeft = mViewItem[itemID + 1].getLeftPosition()
+            final int currLeft = mViewItems[i + 1].getLeftPosition()
                     - curr.getMeasuredWidth() - mViewGapInPixel;
             curr.setLeftPosition(currLeft);
         }
         // Right items.
-        for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
-            final ViewItem curr = mViewItem[itemID];
+        for (int i = BUFFER_CENTER + 1; i < BUFFER_SIZE; i++) {
+            final ViewItem curr = mViewItems[i];
             if (curr == null) {
                 break;
             }
 
             // First, layout relatively to the previous one.
-            final ViewItem prev = mViewItem[itemID - 1];
+            final ViewItem prev = mViewItems[i - 1];
             final int currLeft =
                     prev.getLeftPosition() + prev.getMeasuredWidth()
                             + mViewGapInPixel;
@@ -1147,8 +1210,8 @@ public class FilmstripView extends ViewGroup {
         // Special case for the one immediately on the right of the camera
         // preview.
         boolean immediateRight =
-                (mViewItem[mCurrentItem].getId() == 1 &&
-                mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY);
+                (mViewItems[BUFFER_CENTER].getAdapterIndex() == 1 &&
+                mDataAdapter.getFilmstripItemAt(0).getAttributes().isSticky());
 
         // Layout the current ViewItem first.
         if (immediateRight) {
@@ -1156,37 +1219,37 @@ public class FilmstripView extends ViewGroup {
             // fading. The implementation in Gallery does not push the first
             // photo to the bottom of the camera preview. Simply place the
             // photo on the right of the preview.
-            final ViewItem currItem = mViewItem[mCurrentItem];
+            final ViewItem currItem = mViewItems[BUFFER_CENTER];
             currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
             currItem.setTranslationX(0f);
             currItem.setAlpha(1f);
         } else if (scaleFraction == 1f) {
-            final ViewItem currItem = mViewItem[mCurrentItem];
+            final ViewItem currItem = mViewItems[BUFFER_CENTER];
             final int currCenterX = currItem.getCenterX();
             if (mCenterX < currCenterX) {
                 // In full-screen and mCenterX is on the left of the center,
                 // we draw the current one to "fade down".
-                fadeAndScaleRightViewItem(mCurrentItem);
+                fadeAndScaleRightViewItem(BUFFER_CENTER);
             } else if (mCenterX > currCenterX) {
                 // In full-screen and mCenterX is on the right of the center,
                 // we draw the current one translated.
-                translateLeftViewItem(mCurrentItem, fullScreenWidth, scaleFraction);
+                translateLeftViewItem(BUFFER_CENTER, fullScreenWidth, scaleFraction);
             } else {
                 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
                 currItem.setTranslationX(0f);
                 currItem.setAlpha(1f);
             }
         } else {
-            final ViewItem currItem = mViewItem[mCurrentItem];
+            final ViewItem currItem = mViewItems[BUFFER_CENTER];
             // The normal filmstrip has no translation for the current item. If
             // it has translation before, gradually set it to zero.
             currItem.setTranslationX(currItem.getTranslationX() * scaleFraction);
             currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
-            if (mViewItem[mCurrentItem - 1] == null) {
+            if (mViewItems[BUFFER_CENTER - 1] == null) {
                 currItem.setAlpha(1f);
             } else {
                 final int currCenterX = currItem.getCenterX();
-                final int prevCenterX = mViewItem[mCurrentItem - 1].getCenterX();
+                final int prevCenterX = mViewItems[BUFFER_CENTER - 1].getCenterX();
                 final float fadeDownFraction =
                         ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
                 currItem.setAlpha(
@@ -1197,23 +1260,23 @@ public class FilmstripView extends ViewGroup {
         // Layout the rest dependent on the current scale.
 
         // Items on the left
-        for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
-            final ViewItem curr = mViewItem[itemID];
+        for (int i = BUFFER_CENTER - 1; i >= 0; i--) {
+            final ViewItem curr = mViewItems[i];
             if (curr == null) {
                 break;
             }
-            translateLeftViewItem(itemID, fullScreenWidth, scaleFraction);
+            translateLeftViewItem(i, fullScreenWidth, scaleFraction);
         }
 
         // Items on the right
-        for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
-            final ViewItem curr = mViewItem[itemID];
+        for (int i = BUFFER_CENTER + 1; i < BUFFER_SIZE; i++) {
+            final ViewItem curr = mViewItems[i];
             if (curr == null) {
                 break;
             }
 
             curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
-            if (curr.getId() == 1 && isViewTypeSticky(curr)) {
+            if (curr.getAdapterIndex() == 1 && isViewTypeSticky(curr)) {
                 // Special case for the one next to the camera preview.
                 curr.setAlpha(1f);
                 continue;
@@ -1221,23 +1284,26 @@ public class FilmstripView extends ViewGroup {
 
             if (scaleFraction == 1) {
                 // It's in full-screen mode.
-                fadeAndScaleRightViewItem(itemID);
+                fadeAndScaleRightViewItem(i);
             } else {
-                if (curr.getVisibility() == INVISIBLE) {
-                    curr.setVisibility(VISIBLE);
-                }
-                if (itemID == mCurrentItem + 1) {
+                boolean setToVisible = (curr.getVisibility() == INVISIBLE);
+
+                if (i == BUFFER_CENTER + 1) {
                     curr.setAlpha(1f - scaleFraction);
                 } else {
                     if (scaleFraction == 0f) {
                         curr.setAlpha(1f);
                     } else {
-                        curr.setVisibility(INVISIBLE);
+                        setToVisible = false;
                     }
                 }
-                curr.setTranslationX(
-                        (mViewItem[mCurrentItem].getLeftPosition() - curr.getLeftPosition()) *
-                                scaleFraction);
+
+                if (setToVisible) {
+                    curr.setVisibility(VISIBLE);
+                }
+
+                curr.setTranslationX((mViewItems[BUFFER_CENTER].getLeftPosition() -
+                      curr.getLeftPosition()) * scaleFraction);
             }
         }
 
@@ -1248,8 +1314,7 @@ public class FilmstripView extends ViewGroup {
         if (item == null) {
             return false;
         }
-        return mDataAdapter.getImageData(item.getId()).getViewType() ==
-                ImageData.VIEW_TYPE_STICKY;
+        return mDataAdapter.getFilmstripItemAt(item.getAdapterIndex()).getAttributes().isSticky();
     }
 
     @Override
@@ -1285,7 +1350,7 @@ public class FilmstripView extends ViewGroup {
         if (!inZoomView()) {
             return;
         }
-        ViewItem current = mViewItem[mCurrentItem];
+        ViewItem current = mViewItems[BUFFER_CENTER];
         if (current == null) {
             return;
         }
@@ -1311,96 +1376,102 @@ public class FilmstripView extends ViewGroup {
         item.animateAlpha(1f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
     }
 
-    private void animateItemRemoval(int dataID, final ImageData data) {
+    private void animateItemRemoval(int index) {
         if (mScale > FULL_SCREEN_SCALE) {
             resetZoomView();
         }
-        int removedItemId = findItemByDataID(dataID);
+        int removeAtBufferIndex = findItemInBufferByAdapterIndex(index);
 
-        // adjust the data id to be consistent
+        // adjust the buffer to be consistent
         for (int i = 0; i < BUFFER_SIZE; i++) {
-            if (mViewItem[i] == null || mViewItem[i].getId() <= dataID) {
+            if (mViewItems[i] == null || mViewItems[i].getAdapterIndex() <= index) {
                 continue;
             }
-            mViewItem[i].setId(mViewItem[i].getId() - 1);
+            mViewItems[i].setIndex(mViewItems[i].getAdapterIndex() - 1);
         }
-        if (removedItemId == -1) {
+        if (removeAtBufferIndex == -1) {
             return;
         }
 
-        final ViewItem removedItem = mViewItem[removedItemId];
+        final ViewItem removedItem = mViewItems[removeAtBufferIndex];
         final int offsetX = removedItem.getMeasuredWidth() + mViewGapInPixel;
 
-        for (int i = removedItemId + 1; i < BUFFER_SIZE; i++) {
-            if (mViewItem[i] != null) {
-                mViewItem[i].setLeftPosition(mViewItem[i].getLeftPosition() - offsetX);
+        for (int i = removeAtBufferIndex + 1; i < BUFFER_SIZE; i++) {
+            if (mViewItems[i] != null) {
+                mViewItems[i].setLeftPosition(mViewItems[i].getLeftPosition() - offsetX);
             }
         }
 
-        if (removedItemId >= mCurrentItem
-                && mViewItem[removedItemId].getId() < mDataAdapter.getTotalNumber()) {
+        if (removeAtBufferIndex >= BUFFER_CENTER
+                && mViewItems[removeAtBufferIndex].getAdapterIndex() < mDataAdapter.getTotalNumber()) {
             // Fill the removed item by left shift when the current one or
             // anyone on the right is removed, and there's more data on the
             // right available.
-            for (int i = removedItemId; i < BUFFER_SIZE - 1; i++) {
-                mViewItem[i] = mViewItem[i + 1];
+            for (int i = removeAtBufferIndex; i < BUFFER_SIZE - 1; i++) {
+                mViewItems[i] = mViewItems[i + 1];
             }
 
             // pull data out from the DataAdapter for the last one.
             int curr = BUFFER_SIZE - 1;
             int prev = curr - 1;
-            if (mViewItem[prev] != null) {
-                mViewItem[curr] = buildItemFromData(mViewItem[prev].getId() + 1);
+            if (mViewItems[prev] != null) {
+                mViewItems[curr] = buildViewItemAt(mViewItems[prev].getAdapterIndex() + 1);
             }
 
             // The animation part.
             if (inFullScreen()) {
-                mViewItem[mCurrentItem].setVisibility(VISIBLE);
-                ViewItem nextItem = mViewItem[mCurrentItem + 1];
+                mViewItems[BUFFER_CENTER].setVisibility(VISIBLE);
+                ViewItem nextItem = mViewItems[BUFFER_CENTER + 1];
                 if (nextItem != null) {
                     nextItem.setVisibility(INVISIBLE);
                 }
             }
 
             // Translate the views to their original places.
-            for (int i = removedItemId; i < BUFFER_SIZE; i++) {
-                if (mViewItem[i] != null) {
-                    mViewItem[i].setTranslationX(offsetX);
+            for (int i = removeAtBufferIndex; i < BUFFER_SIZE; i++) {
+                if (mViewItems[i] != null) {
+                    mViewItems[i].setTranslationX(offsetX);
                 }
             }
 
             // The end of the filmstrip might have been changed.
             // The mCenterX might be out of the bound.
-            ViewItem currItem = mViewItem[mCurrentItem];
-            if (currItem.getId() == mDataAdapter.getTotalNumber() - 1
-                    && mCenterX > currItem.getCenterX()) {
-                int adjustDiff = currItem.getCenterX() - mCenterX;
-                mCenterX = currItem.getCenterX();
-                for (int i = 0; i < BUFFER_SIZE; i++) {
-                    if (mViewItem[i] != null) {
-                        mViewItem[i].translateXScaledBy(adjustDiff);
+            ViewItem currItem = mViewItems[BUFFER_CENTER];
+            if (currItem!=null) {
+                if (currItem.getAdapterIndex() == mDataAdapter.getTotalNumber() - 1
+                        && mCenterX > currItem.getCenterX()) {
+                    int adjustDiff = currItem.getCenterX() - mCenterX;
+                    mCenterX = currItem.getCenterX();
+                    for (int i = 0; i < BUFFER_SIZE; i++) {
+                        if (mViewItems[i] != null) {
+                            mViewItems[i].translateXScaledBy(adjustDiff);
+                        }
                     }
                 }
+            } else {
+                // CurrItem should NOT be NULL, but if is, at least don't crash.
+                Log.w(TAG,"Caught invalid update in removal animation.");
             }
         } else {
             // fill the removed place by right shift
             mCenterX -= offsetX;
 
-            for (int i = removedItemId; i > 0; i--) {
-                mViewItem[i] = mViewItem[i - 1];
+            for (int i = removeAtBufferIndex; i > 0; i--) {
+                mViewItems[i] = mViewItems[i - 1];
             }
 
             // pull data out from the DataAdapter for the first one.
             int curr = 0;
             int next = curr + 1;
-            if (mViewItem[next] != null) {
-                mViewItem[curr] = buildItemFromData(mViewItem[next].getId() - 1);
+            if (mViewItems[next] != null) {
+                mViewItems[curr] = buildViewItemAt(mViewItems[next].getAdapterIndex() - 1);
+
             }
 
             // Translate the views to their original places.
-            for (int i = removedItemId; i >= 0; i--) {
-                if (mViewItem[i] != null) {
-                    mViewItem[i].setTranslationX(-offsetX);
+            for (int i = removeAtBufferIndex; i >= 0; i--) {
+                if (mViewItems[i] != null) {
+                    mViewItems[i].setTranslationX(-offsetX);
                 }
             }
         }
@@ -1423,138 +1494,152 @@ public class FilmstripView extends ViewGroup {
         invalidate();
 
         // Now, slide every one back.
-        if (mViewItem[mCurrentItem] == null) {
+        if (mViewItems[BUFFER_CENTER] == null) {
             return;
         }
         for (int i = 0; i < BUFFER_SIZE; i++) {
-            if (mViewItem[i] != null
-                    && mViewItem[i].getTranslationX() != 0f) {
-                slideViewBack(mViewItem[i]);
+            if (mViewItems[i] != null
+                    && mViewItems[i].getTranslationX() != 0f) {
+                slideViewBack(mViewItems[i]);
             }
         }
-        if (isCurrentItemCentered() && isViewTypeSticky(mViewItem[mCurrentItem])) {
+        if (isCurrentItemCentered() && isViewTypeSticky(mViewItems[BUFFER_CENTER])) {
             // Special case for scrolling onto the camera preview after removal.
             mController.goToFullScreen();
         }
     }
 
     // returns -1 on failure.
-    private int findItemByDataID(int dataID) {
+    private int findItemInBufferByAdapterIndex(int index) {
         for (int i = 0; i < BUFFER_SIZE; i++) {
-            if (mViewItem[i] != null
-                    && mViewItem[i].getId() == dataID) {
+            if (mViewItems[i] != null
+                    && mViewItems[i].getAdapterIndex() == index) {
                 return i;
             }
         }
         return -1;
     }
 
-    private void updateInsertion(int dataID) {
-        int insertedItemId = findItemByDataID(dataID);
-        if (insertedItemId == -1) {
+    private void updateInsertion(int index) {
+        int bufferIndex = findItemInBufferByAdapterIndex(index);
+        if (bufferIndex == -1) {
             // Not in the current item buffers. Check if it's inserted
             // at the end.
-            if (dataID == mDataAdapter.getTotalNumber() - 1) {
-                int prev = findItemByDataID(dataID - 1);
+            if (index == mDataAdapter.getTotalNumber() - 1) {
+                int prev = findItemInBufferByAdapterIndex(index - 1);
                 if (prev >= 0 && prev < BUFFER_SIZE - 1) {
                     // The previous data is in the buffer and we still
                     // have room for the inserted data.
-                    insertedItemId = prev + 1;
+                    bufferIndex = prev + 1;
                 }
             }
         }
 
-        // adjust the data id to be consistent
+        // adjust the indexes to be consistent
         for (int i = 0; i < BUFFER_SIZE; i++) {
-            if (mViewItem[i] == null || mViewItem[i].getId() < dataID) {
+            if (mViewItems[i] == null || mViewItems[i].getAdapterIndex() < index) {
                 continue;
             }
-            mViewItem[i].setId(mViewItem[i].getId() + 1);
+            mViewItems[i].setIndex(mViewItems[i].getAdapterIndex() + 1);
         }
-        if (insertedItemId == -1) {
+        if (bufferIndex == -1) {
             return;
         }
 
-        final ImageData data = mDataAdapter.getImageData(dataID);
+        final FilmstripItem data = mDataAdapter.getFilmstripItemAt(index);
         Point dim = CameraUtil
-                .resizeToFill(data.getWidth(), data.getHeight(), data.getRotation(),
-                        getMeasuredWidth(), getMeasuredHeight());
+                .resizeToFill(
+                      data.getData().getDimensions().getWidth(),
+                      data.getData().getDimensions().getHeight(),
+                      data.getData().getOrientation(),
+                      getMeasuredWidth(),
+                      getMeasuredHeight());
         final int offsetX = dim.x + mViewGapInPixel;
-        ViewItem viewItem = buildItemFromData(dataID);
+        ViewItem viewItem = buildViewItemAt(index);
+        if (viewItem == null) {
+            Log.w(TAG, "unable to build inserted item from data");
+            return;
+        }
 
-        if (insertedItemId >= mCurrentItem) {
-            if (insertedItemId == mCurrentItem) {
-                viewItem.setLeftPosition(mViewItem[mCurrentItem].getLeftPosition());
+        if (bufferIndex >= BUFFER_CENTER) {
+            if (bufferIndex == BUFFER_CENTER) {
+                viewItem.setLeftPosition(mViewItems[BUFFER_CENTER].getLeftPosition());
             }
             // Shift right to make rooms for newly inserted item.
             removeItem(BUFFER_SIZE - 1);
-            for (int i = BUFFER_SIZE - 1; i > insertedItemId; i--) {
-                mViewItem[i] = mViewItem[i - 1];
-                if (mViewItem[i] != null) {
-                    mViewItem[i].setTranslationX(-offsetX);
-                    slideViewBack(mViewItem[i]);
+            for (int i = BUFFER_SIZE - 1; i > bufferIndex; i--) {
+                mViewItems[i] = mViewItems[i - 1];
+                if (mViewItems[i] != null) {
+                    mViewItems[i].setTranslationX(-offsetX);
+                    slideViewBack(mViewItems[i]);
                 }
             }
         } else {
             // Shift left. Put the inserted data on the left instead of the
             // found position.
-            --insertedItemId;
-            if (insertedItemId < 0) {
+            --bufferIndex;
+            if (bufferIndex < 0) {
                 return;
             }
             removeItem(0);
-            for (int i = 1; i <= insertedItemId; i++) {
-                if (mViewItem[i] != null) {
-                    mViewItem[i].setTranslationX(offsetX);
-                    slideViewBack(mViewItem[i]);
-                    mViewItem[i - 1] = mViewItem[i];
+            for (int i = 1; i <= bufferIndex; i++) {
+                if (mViewItems[i] != null) {
+                    mViewItems[i].setTranslationX(offsetX);
+                    slideViewBack(mViewItems[i]);
+                    mViewItems[i - 1] = mViewItems[i];
                 }
             }
         }
 
-        mViewItem[insertedItemId] = viewItem;
+        mViewItems[bufferIndex] = viewItem;
+        ensureItemAtMaxSize(bufferIndex);
         viewItem.setAlpha(0f);
         viewItem.setTranslationY(getHeight() / 8);
         slideViewBack(viewItem);
         adjustChildZOrder();
+
         invalidate();
     }
 
-    private void setDataAdapter(DataAdapter adapter) {
+    private void setDataAdapter(FilmstripDataAdapter adapter) {
         mDataAdapter = adapter;
         int maxEdge = (int) (Math.max(this.getHeight(), this.getWidth())
                 * FILM_STRIP_SCALE);
         mDataAdapter.suggestViewSizeBound(maxEdge, maxEdge);
-        mDataAdapter.setListener(new DataAdapter.Listener() {
+        mDataAdapter.setListener(new FilmstripDataAdapter.Listener() {
             @Override
-            public void onDataLoaded() {
+            public void onFilmstripItemLoaded() {
                 reload();
             }
 
             @Override
-            public void onDataUpdated(DataAdapter.UpdateReporter reporter) {
+            public void onFilmstripItemUpdated(FilmstripDataAdapter.UpdateReporter reporter) {
                 update(reporter);
             }
 
             @Override
-            public void onDataInserted(int dataId, ImageData data) {
-                if (mViewItem[mCurrentItem] == null) {
+            public void onFilmstripItemInserted(int index, FilmstripItem item) {
+                if (mViewItems[BUFFER_CENTER] == null) {
                     // empty now, simply do a reload.
                     reload();
                 } else {
-                    updateInsertion(dataId);
+                    updateInsertion(index);
                 }
                 if (mListener != null) {
-                    mListener.onDataFocusChanged(dataId, getCurrentId());
+                    mListener.onDataFocusChanged(index, getCurrentItemAdapterIndex());
                 }
+                Log.d(TAG, "onFilmstripItemInserted()");
+                ensureBufferItemsAtMaxSize();
             }
 
             @Override
-            public void onDataRemoved(int dataId, ImageData data) {
-                animateItemRemoval(dataId, data);
+            public void onFilmstripItemRemoved(int index, FilmstripItem item) {
+                animateItemRemoval(index);
                 if (mListener != null) {
-                    mListener.onDataFocusChanged(dataId, getCurrentId());
+                    mListener.onDataFocusChanged(index, getCurrentItemAdapterIndex());
                 }
+                Log.d(TAG, "onFilmstripItemRemoved()");
+                ensureBufferItemsAtMaxSize();
             }
         });
     }
@@ -1572,12 +1657,12 @@ public class FilmstripView extends ViewGroup {
     }
 
     private boolean isCameraPreview() {
-        return isViewTypeSticky(mViewItem[mCurrentItem]);
+        return isViewTypeSticky(mViewItems[BUFFER_CENTER]);
     }
 
     private boolean inCameraFullscreen() {
-        return isDataAtCenter(0) && inFullScreen()
-                && (isViewTypeSticky(mViewItem[mCurrentItem]));
+        return isItemAtIndexCentered(0) && inFullScreen()
+                && (isViewTypeSticky(mViewItems[BUFFER_CENTER]));
     }
 
     @Override
@@ -1589,9 +1674,10 @@ public class FilmstripView extends ViewGroup {
         if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
             mCheckToIntercept = true;
             mDown = MotionEvent.obtain(ev);
-            ViewItem viewItem = mViewItem[mCurrentItem];
+            ViewItem viewItem = mViewItems[BUFFER_CENTER];
             // Do not intercept touch if swipe is not enabled
-            if (viewItem != null && !mDataAdapter.canSwipeInFullScreen(viewItem.getId())) {
+            if (viewItem != null &&
+                  !mDataAdapter.canSwipeInFullScreen(viewItem.getAdapterIndex())) {
                 mCheckToIntercept = false;
             }
             return false;
@@ -1634,98 +1720,124 @@ public class FilmstripView extends ViewGroup {
         return mGestureListener;
     }
 
-    private void updateViewItem(int itemID) {
-        ViewItem item = mViewItem[itemID];
+    private void updateViewItem(int bufferIndex) {
+        ViewItem item = mViewItems[bufferIndex];
         if (item == null) {
-            Log.e(TAG, "trying to update an null item");
+            Log.w(TAG, "updateViewItem() - Trying to update an null item!");
             return;
         }
-        item.removeViewFromHierarchy(true);
 
-        ViewItem newItem = buildItemFromData(item.getId());
-        if (newItem == null) {
-            Log.e(TAG, "new item is null");
-            // keep using the old data.
-            item.addViewToHierarchy();
+        int adapterIndex = item.getAdapterIndex();
+        FilmstripItem filmstripItem = mDataAdapter.getFilmstripItemAt(adapterIndex);
+        if (filmstripItem == null) {
+            Log.w(TAG, "updateViewItem() - Trying to update item with null FilmstripItem!");
             return;
         }
-        newItem.copyAttributes(item);
-        mViewItem[itemID] = newItem;
+
+        // In case the underlying data item is changed (commonly from
+        // SessionItem to PhotoItem for an image requiring processing), set the
+        // new FilmstripItem on the ViewItem
+        item.setData(filmstripItem);
+
+        // In case state changed from a new FilmStripItem or the existing one,
+        // redraw the View contents. We call getView here as it will refill the
+        // view contents, but it is not clear as we are not using the documented
+        // method intent to get a View, we know that this always uses the view
+        // passed in to populate it.
+        // TODO: refactor 'getView' to more explicitly just update view contents
+        mDataAdapter.getView(item.getView(), adapterIndex, mVideoClickedCallback);
+
         mZoomView.resetDecoder();
 
         boolean stopScroll = clampCenterX();
         if (stopScroll) {
             mController.stopScrolling(true);
         }
+
+        Log.d(TAG, "updateViewItem(bufferIndex: " + bufferIndex + ")");
+        Log.d(TAG, "updateViewItem() - mIsUserScrolling: " + mIsUserScrolling);
+        Log.d(TAG, "updateViewItem() - mController.isScrolling() - " + mController.isScrolling());
+
+        // Relying on only isScrolling or isUserScrolling independently
+        // is unreliable. Load the full resolution if either value
+        // reports that the item is not scrolling.
+        if (!mController.isScrolling() || !mIsUserScrolling) {
+            ensureItemAtMaxSize(bufferIndex);
+        }
+
         adjustChildZOrder();
         invalidate();
         if (mListener != null) {
-            mListener.onDataUpdated(newItem.getId());
+            mListener.onDataUpdated(adapterIndex);
         }
     }
 
     /** Some of the data is changed. */
-    private void update(DataAdapter.UpdateReporter reporter) {
+    private void update(FilmstripDataAdapter.UpdateReporter reporter) {
         // No data yet.
-        if (mViewItem[mCurrentItem] == null) {
+        if (mViewItems[BUFFER_CENTER] == null) {
             reload();
             return;
         }
 
         // Check the current one.
-        ViewItem curr = mViewItem[mCurrentItem];
-        int dataId = curr.getId();
-        if (reporter.isDataRemoved(dataId)) {
+        ViewItem curr = mViewItems[BUFFER_CENTER];
+        int index = curr.getAdapterIndex();
+        if (reporter.isDataRemoved(index)) {
             reload();
             return;
         }
-        if (reporter.isDataUpdated(dataId)) {
-            updateViewItem(mCurrentItem);
-            final ImageData data = mDataAdapter.getImageData(dataId);
+        if (reporter.isDataUpdated(index)) {
+            updateViewItem(BUFFER_CENTER);
+            final FilmstripItem data = mDataAdapter.getFilmstripItemAt(index);
             if (!mIsUserScrolling && !mController.isScrolling()) {
                 // If there is no scrolling at all, adjust mCenterX to place
                 // the current item at the center.
-                Point dim = CameraUtil.resizeToFill(data.getWidth(), data.getHeight(),
-                        data.getRotation(), getMeasuredWidth(), getMeasuredHeight());
+                Point dim = CameraUtil.resizeToFill(
+                      data.getData().getDimensions().getWidth(),
+                      data.getData().getDimensions().getHeight(),
+                      data.getData().getOrientation(),
+                      getMeasuredWidth(),
+                      getMeasuredHeight());
                 mCenterX = curr.getLeftPosition() + dim.x / 2;
             }
         }
 
         // Check left
-        for (int i = mCurrentItem - 1; i >= 0; i--) {
-            curr = mViewItem[i];
+        for (int i = BUFFER_CENTER - 1; i >= 0; i--) {
+            curr = mViewItems[i];
             if (curr != null) {
-                dataId = curr.getId();
-                if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
+                index = curr.getAdapterIndex();
+                if (reporter.isDataRemoved(index) || reporter.isDataUpdated(index)) {
                     updateViewItem(i);
                 }
+
             } else {
-                ViewItem next = mViewItem[i + 1];
+                ViewItem next = mViewItems[i + 1];
                 if (next != null) {
-                    mViewItem[i] = buildItemFromData(next.getId() - 1);
+                    mViewItems[i] = buildViewItemAt(next.getAdapterIndex() - 1);
                 }
             }
         }
 
         // Check right
-        for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
-            curr = mViewItem[i];
+        for (int i = BUFFER_CENTER + 1; i < BUFFER_SIZE; i++) {
+            curr = mViewItems[i];
             if (curr != null) {
-                dataId = curr.getId();
-                if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
+                index = curr.getAdapterIndex();
+                if (reporter.isDataRemoved(index) || reporter.isDataUpdated(index)) {
                     updateViewItem(i);
                 }
             } else {
-                ViewItem prev = mViewItem[i - 1];
+                ViewItem prev = mViewItems[i - 1];
                 if (prev != null) {
-                    mViewItem[i] = buildItemFromData(prev.getId() + 1);
+                    mViewItems[i] = buildViewItemAt(prev.getAdapterIndex() + 1);
                 }
             }
         }
         adjustChildZOrder();
         // Request a layout to find the measured width/height of the view first.
         requestLayout();
-        // Update photo sphere visibility after metadata fully written.
     }
 
     /**
@@ -1736,108 +1848,112 @@ public class FilmstripView extends ViewGroup {
     private void reload() {
         mController.stopScrolling(true);
         mController.stopScale();
-        mDataIdOnUserScrolling = 0;
+        mAdapterIndexUserIsScrollingOver = 0;
 
         int prevId = -1;
-        if (mViewItem[mCurrentItem] != null) {
-            prevId = mViewItem[mCurrentItem].getId();
+        if (mViewItems[BUFFER_CENTER] != null) {
+            prevId = mViewItems[BUFFER_CENTER].getAdapterIndex();
         }
 
-        // Remove all views from the mViewItem buffer, except the camera view.
-        for (int i = 0; i < mViewItem.length; i++) {
-            if (mViewItem[i] == null) {
+        // Remove all views from the mViewItems buffer, except the camera view.
+        for (int i = 0; i < mViewItems.length; i++) {
+            if (mViewItems[i] == null) {
                 continue;
             }
-            mViewItem[i].removeViewFromHierarchy(false);
+            mViewItems[i].removeViewFromHierarchy(false);
         }
 
         // Clear out the mViewItems and rebuild with camera in the center.
-        Arrays.fill(mViewItem, null);
+        Arrays.fill(mViewItems, null);
         int dataNumber = mDataAdapter.getTotalNumber();
         if (dataNumber == 0) {
             return;
         }
 
-        mViewItem[mCurrentItem] = buildItemFromData(0);
-        if (mViewItem[mCurrentItem] == null) {
+        mViewItems[BUFFER_CENTER] = buildViewItemAt(0);
+        if (mViewItems[BUFFER_CENTER] == null) {
             return;
         }
-        mViewItem[mCurrentItem].setLeftPosition(0);
-        for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
-            mViewItem[i] = buildItemFromData(mViewItem[i - 1].getId() + 1);
-            if (mViewItem[i] == null) {
+        mViewItems[BUFFER_CENTER].setLeftPosition(0);
+        for (int i = BUFFER_CENTER + 1; i < BUFFER_SIZE; i++) {
+            mViewItems[i] = buildViewItemAt(mViewItems[i - 1].getAdapterIndex() + 1);
+            if (mViewItems[i] == null) {
                 break;
             }
         }
 
-        // Ensure that the views in mViewItem will layout the first in the
+        // Ensure that the views in mViewItems will layout the first in the
         // center of the display upon a reload.
         mCenterX = -1;
         mScale = FILM_STRIP_SCALE;
 
         adjustChildZOrder();
+
+        Log.d(TAG, "reload() - Ensure all items are loaded at max size.");
+        ensureBufferItemsAtMaxSize();
         invalidate();
 
         if (mListener != null) {
             mListener.onDataReloaded();
-            mListener.onDataFocusChanged(prevId, mViewItem[mCurrentItem].getId());
+            mListener.onDataFocusChanged(prevId, mViewItems[BUFFER_CENTER].getAdapterIndex());
         }
     }
 
-    private void promoteData(int itemID, int dataID) {
+    private void promoteData(int index) {
         if (mListener != null) {
-            mListener.onFocusedDataPromoted(dataID);
+            mListener.onFocusedDataPromoted(index);
         }
     }
 
-    private void demoteData(int itemID, int dataID) {
+    private void demoteData(int index) {
         if (mListener != null) {
-            mListener.onFocusedDataDemoted(dataID);
+            mListener.onFocusedDataDemoted(index);
         }
     }
 
     private void onEnterFilmstrip() {
+        Log.d(TAG, "onEnterFilmstrip()");
         if (mListener != null) {
-            mListener.onEnterFilmstrip(getCurrentId());
+            mListener.onEnterFilmstrip(getCurrentItemAdapterIndex());
         }
     }
 
     private void onLeaveFilmstrip() {
         if (mListener != null) {
-            mListener.onLeaveFilmstrip(getCurrentId());
+            mListener.onLeaveFilmstrip(getCurrentItemAdapterIndex());
         }
     }
 
     private void onEnterFullScreen() {
         mFullScreenUIHidden = false;
         if (mListener != null) {
-            mListener.onEnterFullScreenUiShown(getCurrentId());
+            mListener.onEnterFullScreenUiShown(getCurrentItemAdapterIndex());
         }
     }
 
     private void onLeaveFullScreen() {
         if (mListener != null) {
-            mListener.onLeaveFullScreenUiShown(getCurrentId());
+            mListener.onLeaveFullScreenUiShown(getCurrentItemAdapterIndex());
         }
     }
 
     private void onEnterFullScreenUiHidden() {
         mFullScreenUIHidden = true;
         if (mListener != null) {
-            mListener.onEnterFullScreenUiHidden(getCurrentId());
+            mListener.onEnterFullScreenUiHidden(getCurrentItemAdapterIndex());
         }
     }
 
     private void onLeaveFullScreenUiHidden() {
         mFullScreenUIHidden = false;
         if (mListener != null) {
-            mListener.onLeaveFullScreenUiHidden(getCurrentId());
+            mListener.onLeaveFullScreenUiHidden(getCurrentItemAdapterIndex());
         }
     }
 
     private void onEnterZoomView() {
         if (mListener != null) {
-            mListener.onEnterZoomView(getCurrentId());
+            mListener.onEnterZoomView(getCurrentItemAdapterIndex());
         }
     }
 
@@ -1849,23 +1965,24 @@ public class FilmstripView extends ViewGroup {
      * MyController controls all the geometry animations. It passively tells the
      * geometry information on demand.
      */
-    private class MyController implements FilmstripController {
+    private class FilmstripControllerImpl implements FilmstripController {
 
         private final ValueAnimator mScaleAnimator;
         private ValueAnimator mZoomAnimator;
         private AnimatorSet mFlingAnimator;
 
-        private final MyScroller mScroller;
+        private final FilmstripScrollGesture mScrollGesture;
         private boolean mCanStopScroll;
 
-        private final MyScroller.Listener mScrollerListener =
-                new MyScroller.Listener() {
+        private final FilmstripScrollGesture.Listener mScrollListener =
+                new FilmstripScrollGesture.Listener() {
                     @Override
                     public void onScrollUpdate(int currX, int currY) {
                         mCenterX = currX;
 
                         boolean stopScroll = clampCenterX();
                         if (stopScroll) {
+                            Log.d(TAG, "[fling] onScrollUpdate() - stopScrolling!");
                             mController.stopScrolling(true);
                         }
                         invalidate();
@@ -1874,12 +1991,32 @@ public class FilmstripView extends ViewGroup {
                     @Override
                     public void onScrollEnd() {
                         mCanStopScroll = true;
-                        if (mViewItem[mCurrentItem] == null) {
+                        Log.d(TAG, "[fling] onScrollEnd() - onScrollEnd");
+                        if (mViewItems[BUFFER_CENTER] == null) {
                             return;
                         }
-                        snapInCenter();
+                        scrollCurrentItemToCenter();
+
+                        // onScrollEnd will get called twice, once when
+                        // the fling part ends, and once when the manual
+                        // scroll center animation finishes. Once everything
+                        // stops moving ensure that the items are loaded at
+                        // full resolution.
+                        if (isCurrentItemCentered()) {
+                            // Since these are getting pushed into a queue,
+                            // we want to ensure the item that is "most in view" is
+                            // the first one rendered at max size.
+
+                            Log.d(TAG, "[fling] onScrollEnd() - Ensuring that items are at"
+                                  + " full resolution.");
+                            ensureItemAtMaxSize(BUFFER_CENTER);
+                            ensureItemAtMaxSize(BUFFER_CENTER + 1);
+                            ensureItemAtMaxSize(BUFFER_CENTER - 1);
+                            ensureItemAtMaxSize(BUFFER_CENTER + 2);
+                        }
+
                         if (isCurrentItemCentered()
-                                && isViewTypeSticky(mViewItem[mCurrentItem])) {
+                                && isViewTypeSticky(mViewItems[BUFFER_CENTER])) {
                             // Special case for the scrolling end on the camera
                             // preview.
                             goToFullScreen();
@@ -1891,7 +2028,7 @@ public class FilmstripView extends ViewGroup {
                 new ValueAnimator.AnimatorUpdateListener() {
                     @Override
                     public void onAnimationUpdate(ValueAnimator animation) {
-                        if (mViewItem[mCurrentItem] == null) {
+                        if (mViewItems[BUFFER_CENTER] == null) {
                             return;
                         }
                         mScale = (Float) animation.getAnimatedValue();
@@ -1899,11 +2036,11 @@ public class FilmstripView extends ViewGroup {
                     }
                 };
 
-        MyController(Context context) {
+        FilmstripControllerImpl() {
             TimeInterpolator decelerateInterpolator = new DecelerateInterpolator(1.5f);
-            mScroller = new MyScroller(mActivity,
+            mScrollGesture = new FilmstripScrollGesture(mActivity.getAndroidContext(),
                     new Handler(mActivity.getMainLooper()),
-                    mScrollerListener, decelerateInterpolator);
+                  mScrollListener, decelerateInterpolator);
             mCanStopScroll = true;
 
             mScaleAnimator = new ValueAnimator();
@@ -1951,12 +2088,12 @@ public class FilmstripView extends ViewGroup {
         }
 
         @Override
-        public int getCurrentId() {
-            return FilmstripView.this.getCurrentId();
+        public int getCurrentAdapterIndex() {
+            return FilmstripView.this.getCurrentItemAdapterIndex();
         }
 
         @Override
-        public void setDataAdapter(DataAdapter adapter) {
+        public void setDataAdapter(FilmstripDataAdapter adapter) {
             FilmstripView.this.setDataAdapter(adapter);
         }
 
@@ -1981,13 +2118,13 @@ public class FilmstripView extends ViewGroup {
         }
 
         @Override
-        public void setListener(FilmstripListener l) {
-            FilmstripView.this.setListener(l);
+        public void setListener(FilmstripListener listener) {
+            FilmstripView.this.setListener(listener);
         }
 
         @Override
         public boolean isScrolling() {
-            return !mScroller.isFinished();
+            return !mScrollGesture.isFinished();
         }
 
         @Override
@@ -1995,13 +2132,13 @@ public class FilmstripView extends ViewGroup {
             return mScaleAnimator.isRunning();
         }
 
-        private int estimateMinX(int dataID, int leftPos, int viewWidth) {
-            return leftPos - (dataID + 100) * (viewWidth + mViewGapInPixel);
+        private int estimateMinX(int index, int leftPos, int viewWidth) {
+            return leftPos - (index + 100) * (viewWidth + mViewGapInPixel);
         }
 
-        private int estimateMaxX(int dataID, int leftPos, int viewWidth) {
+        private int estimateMaxX(int index, int leftPos, int viewWidth) {
             return leftPos
-                    + (mDataAdapter.getTotalNumber() - dataID + 100)
+                    + (mDataAdapter.getTotalNumber() - index + 100)
                     * (viewWidth + mViewGapInPixel);
         }
 
@@ -2101,7 +2238,7 @@ public class FilmstripView extends ViewGroup {
             if (!stopScrolling(false)) {
                 return;
             }
-            final ViewItem item = mViewItem[mCurrentItem];
+            final ViewItem item = mViewItems[BUFFER_CENTER];
             if (item == null) {
                 return;
             }
@@ -2116,11 +2253,11 @@ public class FilmstripView extends ViewGroup {
             // Estimation of possible length on the left. To ensure the
             // velocity doesn't become too slow eventually, we add a huge number
             // to the estimated maximum.
-            int minX = estimateMinX(item.getId(), item.getLeftPosition(), w);
+            int minX = estimateMinX(item.getAdapterIndex(), item.getLeftPosition(), w);
             // Estimation of possible length on the right. Likewise, exaggerate
             // the possible maximum too.
-            int maxX = estimateMaxX(item.getId(), item.getLeftPosition(), w);
-            mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0);
+            int maxX = estimateMaxX(item.getAdapterIndex(), item.getLeftPosition(), w);
+            mScrollGesture.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0);
         }
 
         void flingInsideZoomView(float velocityX, float velocityY) {
@@ -2128,7 +2265,7 @@ public class FilmstripView extends ViewGroup {
                 return;
             }
 
-            final ViewItem current = mViewItem[mCurrentItem];
+            final ViewItem current = mViewItems[BUFFER_CENTER];
             if (current == null) {
                 return;
             }
@@ -2218,7 +2355,7 @@ public class FilmstripView extends ViewGroup {
             } else if (!mCanStopScroll && !forced) {
                 return false;
             }
-            mScroller.forceFinished(true);
+            mScrollGesture.forceFinished(true);
             return true;
         }
 
@@ -2228,32 +2365,32 @@ public class FilmstripView extends ViewGroup {
 
         @Override
         public void scrollToPosition(int position, int duration, boolean interruptible) {
-            if (mViewItem[mCurrentItem] == null) {
+            if (mViewItems[BUFFER_CENTER] == null) {
                 return;
             }
             mCanStopScroll = interruptible;
-            mScroller.startScroll(mCenterX, 0, position - mCenterX, 0, duration);
+            mScrollGesture.startScroll(mCenterX, position - mCenterX, duration);
         }
 
         @Override
         public boolean goToNextItem() {
-            return goToItem(mCurrentItem + 1);
+            return goToItem(BUFFER_CENTER + 1);
         }
 
         @Override
         public boolean goToPreviousItem() {
-            return goToItem(mCurrentItem - 1);
+            return goToItem(BUFFER_CENTER - 1);
         }
 
         private boolean goToItem(int itemIndex) {
-            final ViewItem nextItem = mViewItem[itemIndex];
+            final ViewItem nextItem = mViewItems[itemIndex];
             if (nextItem == null) {
                 return false;
             }
             stopScrolling(true);
             scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS * 2, false);
 
-            if (isViewTypeSticky(mViewItem[mCurrentItem])) {
+            if (isViewTypeSticky(mViewItems[BUFFER_CENTER])) {
                 // Special case when moving from camera preview.
                 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
             }
@@ -2261,7 +2398,7 @@ public class FilmstripView extends ViewGroup {
         }
 
         private void scaleTo(float scale, int duration) {
-            if (mViewItem[mCurrentItem] == null) {
+            if (mViewItems[BUFFER_CENTER] == null) {
                 return;
             }
             stopScale();
@@ -2272,7 +2409,7 @@ public class FilmstripView extends ViewGroup {
 
         @Override
         public void goToFilmstrip() {
-            if (mViewItem[mCurrentItem] == null) {
+            if (mViewItems[BUFFER_CENTER] == null) {
                 return;
             }
             if (mScale == FILM_STRIP_SCALE) {
@@ -2280,9 +2417,9 @@ public class FilmstripView extends ViewGroup {
             }
             scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
 
-            final ViewItem currItem = mViewItem[mCurrentItem];
-            final ViewItem nextItem = mViewItem[mCurrentItem + 1];
-            if (currItem.getId() == 0 && isViewTypeSticky(currItem) && nextItem != null) {
+            final ViewItem currItem = mViewItems[BUFFER_CENTER];
+            final ViewItem nextItem = mViewItems[BUFFER_CENTER + 1];
+            if (currItem.getAdapterIndex() == 0 && isViewTypeSticky(currItem) && nextItem != null) {
                 // Deal with the special case of swiping in camera preview.
                 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, false);
             }
@@ -2318,20 +2455,19 @@ public class FilmstripView extends ViewGroup {
             // Hide everything on the left
             // TODO: Need to find a better way to toggle the visibility of views
             // around the current view.
-            for (int i = 0; i < mCurrentItem; i++) {
-                if (i == mCurrentItem || mViewItem[i] == null) {
-                    continue;
+            for (int i = 0; i < BUFFER_CENTER; i++) {
+                if (mViewItems[i] != null) {
+                    mViewItems[i].setVisibility(visible ? VISIBLE : INVISIBLE);
                 }
-                mViewItem[i].setVisibility(visible ? VISIBLE : INVISIBLE);
             }
         }
 
         private Uri getCurrentUri() {
-            ViewItem curr = mViewItem[mCurrentItem];
+            ViewItem curr = mViewItems[BUFFER_CENTER];
             if (curr == null) {
                 return Uri.EMPTY;
             }
-            return mDataAdapter.getImageData(curr.getId()).getUri();
+            return mDataAdapter.getFilmstripItemAt(curr.getAdapterIndex()).getData().getUri();
         }
 
         /**
@@ -2340,18 +2476,18 @@ public class FilmstripView extends ViewGroup {
          * make the view same size as the image, in pixels.
          */
         private float getCurrentDataMaxScale(boolean allowOverScale) {
-            ViewItem curr = mViewItem[mCurrentItem];
+            ViewItem curr = mViewItems[BUFFER_CENTER];
             if (curr == null) {
                 return FULL_SCREEN_SCALE;
             }
-            ImageData imageData = mDataAdapter.getImageData(curr.getId());
-            if (imageData == null || !imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
+            FilmstripItem imageData = mDataAdapter.getFilmstripItemAt(curr.getAdapterIndex());
+            if (imageData == null || !imageData.getAttributes().canZoomInPlace()) {
                 return FULL_SCREEN_SCALE;
             }
-            float imageWidth = imageData.getWidth();
-            if (imageData.getRotation() == 90
-                    || imageData.getRotation() == 270) {
-                imageWidth = imageData.getHeight();
+            float imageWidth = imageData.getData().getDimensions().getWidth();
+            if (imageData.getData().getOrientation() == 90
+                    || imageData.getData().getOrientation() == 270) {
+                imageWidth = imageData.getData().getDimensions().getHeight();
             }
             float scale = imageWidth / curr.getWidth();
             if (allowOverScale) {
@@ -2367,12 +2503,12 @@ public class FilmstripView extends ViewGroup {
             if (!inZoomView()) {
                 return;
             }
-            ViewItem curr = mViewItem[mCurrentItem];
+            ViewItem curr = mViewItems[BUFFER_CENTER];
             if (curr == null) {
                 return;
             }
-            ImageData imageData = mDataAdapter.getImageData(curr.getId());
-            if (!imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
+            FilmstripItem imageData = mDataAdapter.getFilmstripItemAt(curr.getAdapterIndex());
+            if (!imageData.getAttributes().canZoomInPlace()) {
                 return;
             }
             Uri uri = getCurrentUri();
@@ -2380,7 +2516,7 @@ public class FilmstripView extends ViewGroup {
             if (uri == null || uri == Uri.EMPTY) {
                 return;
             }
-            int orientation = imageData.getRotation();
+            int orientation = imageData.getData().getOrientation();
             mZoomView.loadBitmap(uri, orientation, viewRect);
         }
 
@@ -2390,11 +2526,11 @@ public class FilmstripView extends ViewGroup {
 
         @Override
         public void goToFirstItem() {
-            if (mViewItem[mCurrentItem] == null) {
+            if (mViewItems[BUFFER_CENTER] == null) {
                 return;
             }
             resetZoomView();
-            // TODO: animate to camera if it is still in the mViewItem buffer
+            // TODO: animate to camera if it is still in the mViewItems buffer
             // versus a full reload which will perform an immediate transition
             reload();
         }
@@ -2413,10 +2549,10 @@ public class FilmstripView extends ViewGroup {
     }
 
     private boolean isCurrentItemCentered() {
-        return mViewItem[mCurrentItem].getCenterX() == mCenterX;
+        return mViewItems[BUFFER_CENTER].getCenterX() == mCenterX;
     }
 
-    private static class MyScroller {
+    private static class FilmstripScrollGesture {
         public interface Listener {
             public void onScrollUpdate(int currX, int currY);
 
@@ -2434,6 +2570,7 @@ public class FilmstripView extends ViewGroup {
             public void run() {
                 boolean newPosition = mScroller.computeScrollOffset();
                 if (!newPosition) {
+                    Log.d(TAG, "[fling] onScrollEnd from computeScrollOffset");
                     mListener.onScrollEnd();
                     return;
                 }
@@ -2455,27 +2592,31 @@ public class FilmstripView extends ViewGroup {
                 new Animator.AnimatorListener() {
                     @Override
                     public void onAnimationCancel(Animator animation) {
+                        Log.d(TAG, "[fling] mXScrollAnimatorListener.onAnimationCancel");
                         // Do nothing.
                     }
 
                     @Override
                     public void onAnimationEnd(Animator animation) {
+                        Log.d(TAG, "[fling] onScrollEnd from mXScrollAnimatorListener.onAnimationEnd");
                         mListener.onScrollEnd();
                     }
 
                     @Override
                     public void onAnimationRepeat(Animator animation) {
+                        Log.d(TAG, "[fling] mXScrollAnimatorListener.onAnimationRepeat");
                         // Do nothing.
                     }
 
                     @Override
                     public void onAnimationStart(Animator animation) {
+                        Log.d(TAG, "[fling] mXScrollAnimatorListener.onAnimationStart");
                         // Do nothing.
                     }
                 };
 
-        public MyScroller(Context ctx, Handler handler, Listener listener,
-                TimeInterpolator interpolator) {
+        public FilmstripScrollGesture(Context ctx, Handler handler, Listener listener,
+              TimeInterpolator interpolator) {
             mHandler = handler;
             mListener = listener;
             mScroller = new Scroller(ctx);
@@ -2500,7 +2641,7 @@ public class FilmstripView extends ViewGroup {
         }
 
         /** Only starts and updates scroll in x-axis. */
-        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+        public void startScroll(int startX, int dx, int duration) {
             mXScrollAnimator.cancel();
             mXScrollAnimator.setDuration(duration);
             mXScrollAnimator.setIntValues(startX, startX + dx);
@@ -2527,7 +2668,7 @@ public class FilmstripView extends ViewGroup {
         }
     }
 
-    private class MyGestureReceiver implements FilmstripGestureRecognizer.Listener {
+    private class FilmstripGestures implements FilmstripGestureRecognizer.Listener {
 
         private static final int SCROLL_DIR_NONE = 0;
         private static final int SCROLL_DIR_VERTICAL = 1;
@@ -2541,7 +2682,7 @@ public class FilmstripView extends ViewGroup {
 
         @Override
         public boolean onSingleTapUp(float x, float y) {
-            ViewItem centerItem = mViewItem[mCurrentItem];
+            ViewItem centerItem = mViewItems[BUFFER_CENTER];
             if (inFilmstrip()) {
                 if (centerItem != null && centerItem.areaContains(x, y)) {
                     mController.goToFullScreen();
@@ -2562,7 +2703,7 @@ public class FilmstripView extends ViewGroup {
 
         @Override
         public boolean onDoubleTap(float x, float y) {
-            ViewItem current = mViewItem[mCurrentItem];
+            ViewItem current = mViewItems[BUFFER_CENTER];
             if (current == null) {
                 return false;
             }
@@ -2577,7 +2718,7 @@ public class FilmstripView extends ViewGroup {
             }
             if (inFullScreen()) {
                 mController.zoomAt(current, x, y);
-                checkItemAtMaxSize();
+                ensureItemAtMaxSize(BUFFER_CENTER);
                 return true;
             } else if (mScale > FULL_SCREEN_SCALE) {
                 // In zoom view.
@@ -2600,7 +2741,7 @@ public class FilmstripView extends ViewGroup {
 
         @Override
         public boolean onUp(float x, float y) {
-            ViewItem currItem = mViewItem[mCurrentItem];
+            ViewItem currItem = mViewItems[BUFFER_CENTER];
             if (currItem == null) {
                 return false;
             }
@@ -2619,88 +2760,86 @@ public class FilmstripView extends ViewGroup {
             float speedY = Math.abs(y - mLastDownY)
                     / (SystemClock.uptimeMillis() - mLastDownTime);
             for (int i = 0; i < BUFFER_SIZE; i++) {
-                if (mViewItem[i] == null) {
+                if (mViewItems[i] == null) {
                     continue;
                 }
-                float transY = mViewItem[i].getTranslationY();
+                float transY = mViewItems[i].getTranslationY();
                 if (transY == 0) {
                     continue;
                 }
-                int id = mViewItem[i].getId();
+                int index = mViewItems[i].getAdapterIndex();
 
-                if (mDataAdapter.getImageData(id)
-                        .isUIActionSupported(ImageData.ACTION_DEMOTE)
+                if (mDataAdapter.getFilmstripItemAt(index).getAttributes().canSwipeAway()
                         && ((transY > promoteHeight)
                             || (transY > velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
-                    demoteData(i, id);
-                } else if (mDataAdapter.getImageData(id)
-                        .isUIActionSupported(ImageData.ACTION_PROMOTE)
+                    demoteData(index);
+                } else if (mDataAdapter.getFilmstripItemAt(index).getAttributes().canSwipeAway()
                         && (transY < -promoteHeight
                             || (transY < -velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
-                    promoteData(i, id);
+                    promoteData(index);
                 } else {
                     // put the view back.
-                    slideViewBack(mViewItem[i]);
+                    slideViewBack(mViewItems[i]);
                 }
             }
 
             // The data might be changed. Re-check.
-            currItem = mViewItem[mCurrentItem];
+            currItem = mViewItems[BUFFER_CENTER];
             if (currItem == null) {
                 return true;
             }
 
-            int currId = currItem.getId();
+            int currId = currItem.getAdapterIndex();
             if (mCenterX > currItem.getCenterX() + CAMERA_PREVIEW_SWIPE_THRESHOLD && currId == 0 &&
-                    isViewTypeSticky(currItem) && mDataIdOnUserScrolling == 0) {
+                    isViewTypeSticky(currItem) && mAdapterIndexUserIsScrollingOver == 0) {
                 mController.goToFilmstrip();
                 // Special case to go from camera preview to the next photo.
-                if (mViewItem[mCurrentItem + 1] != null) {
+                if (mViewItems[BUFFER_CENTER + 1] != null) {
                     mController.scrollToPosition(
-                            mViewItem[mCurrentItem + 1].getCenterX(),
+                            mViewItems[BUFFER_CENTER + 1].getCenterX(),
                             GEOMETRY_ADJUST_TIME_MS, false);
                 } else {
                     // No next photo.
-                    snapInCenter();
+                    scrollCurrentItemToCenter();
                 }
             }
             if (isCurrentItemCentered() && currId == 0 && isViewTypeSticky(currItem)) {
                 mController.goToFullScreen();
             } else {
-                if (mDataIdOnUserScrolling == 0 && currId != 0) {
+                if (mAdapterIndexUserIsScrollingOver == 0 && currId != 0) {
                     // Special case to go to filmstrip when the user scroll away
                     // from the camera preview and the current one is not the
                     // preview anymore.
                     mController.goToFilmstrip();
-                    mDataIdOnUserScrolling = currId;
+                    mAdapterIndexUserIsScrollingOver = currId;
                 }
-                snapInCenter();
+                scrollCurrentItemToCenter();
             }
             return false;
         }
 
         @Override
         public void onLongPress(float x, float y) {
-            final int dataId = getCurrentId();
-            if (dataId == -1) {
+            final int index = getCurrentItemAdapterIndex();
+            if (index == -1) {
                 return;
             }
-            mListener.onFocusedDataLongPressed(dataId);
+            mListener.onFocusedDataLongPressed(index);
         }
 
         @Override
         public boolean onScroll(float x, float y, float dx, float dy) {
-            final ViewItem currItem = mViewItem[mCurrentItem];
+            final ViewItem currItem = mViewItems[BUFFER_CENTER];
             if (currItem == null) {
                 return false;
             }
-            if (inFullScreen() && !mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
+            if (inFullScreen() && !mDataAdapter.canSwipeInFullScreen(currItem.getAdapterIndex())) {
                 return false;
             }
             hideZoomView();
             // When image is zoomed in to be bigger than the screen
             if (inZoomView()) {
-                ViewItem curr = mViewItem[mCurrentItem];
+                ViewItem curr = mViewItems[BUFFER_CENTER];
                 float transX = curr.getTranslationX() * mScale - dx;
                 float transY = curr.getTranslationY() * mScale - dy;
                 curr.updateTransform(transX, transY, mScale, mScale, mDrawArea.width(),
@@ -2712,7 +2851,8 @@ public class FilmstripView extends ViewGroup {
             mController.stopScrolling(true);
             if (!mIsUserScrolling) {
                 mIsUserScrolling = true;
-                mDataIdOnUserScrolling = mViewItem[mCurrentItem].getId();
+                mAdapterIndexUserIsScrollingOver =
+                      mViewItems[BUFFER_CENTER].getAdapterIndex();
             }
             if (inFilmstrip()) {
                 // Disambiguate horizontal/vertical first.
@@ -2721,7 +2861,8 @@ public class FilmstripView extends ViewGroup {
                             SCROLL_DIR_VERTICAL;
                 }
                 if (mScrollingDirection == SCROLL_DIR_HORIZONTAL) {
-                    if (mCenterX == currItem.getCenterX() && currItem.getId() == 0 && dx < 0) {
+                    if (mCenterX == currItem.getCenterX() && currItem.getAdapterIndex() == 0 &&
+                          dx < 0) {
                         // Already at the beginning, don't process the swipe.
                         mIsUserScrolling = false;
                         mScrollingDirection = SCROLL_DIR_NONE;
@@ -2733,10 +2874,10 @@ public class FilmstripView extends ViewGroup {
                     int hit = 0;
                     Rect hitRect = new Rect();
                     for (; hit < BUFFER_SIZE; hit++) {
-                        if (mViewItem[hit] == null) {
+                        if (mViewItems[hit] == null) {
                             continue;
                         }
-                        mViewItem[hit].getHitRect(hitRect);
+                        mViewItems[hit].getHitRect(hitRect);
                         if (hitRect.contains((int) x, (int) y)) {
                             break;
                         }
@@ -2746,21 +2887,22 @@ public class FilmstripView extends ViewGroup {
                         return true;
                     }
 
-                    ImageData data = mDataAdapter.getImageData(mViewItem[hit].getId());
-                    float transY = mViewItem[hit].getTranslationY() - dy / mScale;
-                    if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) &&
+                    FilmstripItem data = mDataAdapter.getFilmstripItemAt(
+                          mViewItems[hit].getAdapterIndex());
+                    float transY = mViewItems[hit].getTranslationY() - dy / mScale;
+                    if (!data.getAttributes().canSwipeAway() &&
                             transY > 0f) {
                         transY = 0f;
                     }
-                    if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) &&
+                    if (!data.getAttributes().canSwipeAway() &&
                             transY < 0f) {
                         transY = 0f;
                     }
-                    mViewItem[hit].setTranslationY(transY);
+                    mViewItems[hit].setTranslationY(transY);
                 }
             } else if (inFullScreen()) {
-                if (mViewItem[mCurrentItem] == null || (deltaX < 0 && mCenterX <=
-                        currItem.getCenterX() && currItem.getId() == 0)) {
+                if (mViewItems[BUFFER_CENTER] == null || (deltaX < 0 && mCenterX <=
+                        currItem.getCenterX() && currItem.getAdapterIndex() == 0)) {
                     return false;
                 }
                 // Multiplied by 1.2 to make it more easy to swipe.
@@ -2797,11 +2939,11 @@ public class FilmstripView extends ViewGroup {
 
         @Override
         public boolean onFling(float velocityX, float velocityY) {
-            final ViewItem currItem = mViewItem[mCurrentItem];
+            final ViewItem currItem = mViewItems[BUFFER_CENTER];
             if (currItem == null) {
                 return false;
             }
-            if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
+            if (!mDataAdapter.canSwipeInFullScreen(currItem.getAdapterIndex())) {
                 return false;
             }
             if (inZoomView()) {
@@ -2827,7 +2969,7 @@ public class FilmstripView extends ViewGroup {
                                 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
                         return true;
                     }
-                    ViewItem prevItem = mViewItem[mCurrentItem - 1];
+                    ViewItem prevItem = mViewItems[BUFFER_CENTER - 1];
                     if (prevItem == null) {
                         return false;
                     }
@@ -2842,7 +2984,7 @@ public class FilmstripView extends ViewGroup {
                                     currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
                             return true;
                         }
-                        final ViewItem nextItem = mViewItem[mCurrentItem + 1];
+                        final ViewItem nextItem = mViewItems[BUFFER_CENTER + 1];
                         if (nextItem == null) {
                             return false;
                         }
@@ -2930,7 +3072,7 @@ public class FilmstripView extends ViewGroup {
                 if (!inZoomView()) {
                     mController.setSurroundingViewsVisible(false);
                 }
-                ViewItem curr = mViewItem[mCurrentItem];
+                ViewItem curr = mViewItems[BUFFER_CENTER];
                 // Make sure the image is not overly scaled
                 newScale = Math.min(newScale, mMaxScale);
                 if (newScale == mScale) {
@@ -2944,7 +3086,7 @@ public class FilmstripView extends ViewGroup {
                 } else {
                     onEnterZoomView();
                 }
-                checkItemAtMaxSize();
+                ensureItemAtMaxSize(BUFFER_CENTER);
             }
             return true;
         }