Implement image prefetching in GalleryView/AlbumView.

Also refactor the code a little.

Change-Id: I3be16a0d65b302e9d8d96a2c115487d598050c49
diff --git a/new3d/src/com/android/gallery3d/app/AlbumPage.java b/new3d/src/com/android/gallery3d/app/AlbumPage.java
index 164fad1..758a9af 100644
--- a/new3d/src/com/android/gallery3d/app/AlbumPage.java
+++ b/new3d/src/com/android/gallery3d/app/AlbumPage.java
@@ -25,8 +25,9 @@
 import com.android.gallery3d.R;
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.ui.AdaptiveBackground;
+import com.android.gallery3d.ui.AlbumDataAdapter;
+import com.android.gallery3d.ui.AlbumView;
 import com.android.gallery3d.ui.GLView;
-import com.android.gallery3d.ui.GridSlotAdapter;
 import com.android.gallery3d.ui.HeadUpDisplay;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SlotView;
@@ -34,10 +35,12 @@
 
 public class AlbumPage extends ActivityState implements SlotView.SlotTapListener {
     public static final String KEY_BUCKET_INDEX = "keyBucketIndex";
+
     private static final int CHANGE_BACKGROUND = 1;
     private static final int MARGIN_HUD_SLOTVIEW = 5;
     private static final int HORIZONTAL_GAP_SLOTS = 5;
     private static final int VERTICAL_GAP_SLOTS = 5;
+    private static final int CACHE_SIZE = 64;
 
     private AdaptiveBackground mBackground;
     private SlotView mSlotView;
@@ -134,8 +137,10 @@
     private void intializeData(Bundle data) {
         mBucketIndex = data.getInt(KEY_BUCKET_INDEX);
         MediaSet mediaSet = mContext.getDataManager().getSubMediaSet(mBucketIndex);
-        mSlotView.setListener(new GridSlotAdapter(mContext.getAndroidContext(),
-                mediaSet, mSlotView, mSelectionManager));
+        AlbumDataAdapter model = new AlbumDataAdapter(
+                mContext, mSelectionManager, mediaSet, CACHE_SIZE);
+        mSlotView.setListener(new AlbumView(
+                mContext.getAndroidContext(), model, mSlotView));
     }
 
     private void changeBackground() {
diff --git a/new3d/src/com/android/gallery3d/app/GalleryPage.java b/new3d/src/com/android/gallery3d/app/GalleryPage.java
index 8874a24..b74d1bf 100644
--- a/new3d/src/com/android/gallery3d/app/GalleryPage.java
+++ b/new3d/src/com/android/gallery3d/app/GalleryPage.java
@@ -26,8 +26,9 @@
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.ui.AdaptiveBackground;
 import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.GalleryAdapter;
+import com.android.gallery3d.ui.GalleryView;
 import com.android.gallery3d.ui.HeadUpDisplay;
-import com.android.gallery3d.ui.MediaSetSlotAdapter;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SlotView;
 import com.android.gallery3d.ui.SynchronizedHandler;
@@ -36,6 +37,7 @@
     private static final int CHANGE_BACKGROUND = 1;
 
     private static final int MARGIN_HUD_SLOTVIEW = 5;
+    private static final int CACHE_SIZE = 32;
 
     private AdaptiveBackground mBackground;
     private SlotView mSlotView;
@@ -120,8 +122,9 @@
 
     private void intializeData() {
         MediaSet mediaSet = mContext.getDataManager().getRootSet();
-        mSlotView.setListener(new MediaSetSlotAdapter(
-                mContext.getAndroidContext(), mediaSet, mSlotView, mSelectionManager));
+        GalleryAdapter adapter = new GalleryAdapter(
+                mContext, mSelectionManager, mediaSet, CACHE_SIZE);
+        mSlotView.setListener(new GalleryView(mContext, adapter, mSlotView));
     }
 
     private void initializeViews() {
diff --git a/new3d/src/com/android/gallery3d/ui/AlbumDataAdapter.java b/new3d/src/com/android/gallery3d/ui/AlbumDataAdapter.java
new file mode 100644
index 0000000..2415740
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/AlbumDataAdapter.java
@@ -0,0 +1,338 @@
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryContext;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.MediaSet.MediaSetListener;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.Utils;
+
+public class AlbumDataAdapter implements MediaSetListener {
+    private static final String TAG = "GalleryAdapter";
+
+    private static final int UPDATE_LIMIT = 10;
+
+    private static final int STATE_INVALID = 0;
+    private static final int STATE_VALID = 1;
+    private static final int STATE_UPDATING = 2;
+    private static final int STATE_RECYCLED = 3;
+    private static final int STATE_ERROR = -1;
+
+    private static final int MSG_UPDATE_IMAGE = 0;
+
+    public static interface Listener {
+        public void onSizeChanged(int size);
+        public void onContentInvalidated();
+        public void onWindowContentChanged(
+                int slot, DisplayItem old, DisplayItem update);
+    }
+
+    private final MediaSet mSource;
+    private int mSize;
+
+    private int mContentStart = 0;
+    private int mContentEnd = 0;
+
+    private int mActiveStart = 0;
+    private int mActiveEnd = 0;
+
+    private Listener mListener;
+
+    private final CoverDisplayItem mData[];
+    private final SelectionManager mSelectionManager;
+    private final ColorTexture mWaitLoadingTexture;
+
+    private SynchronizedHandler mHandler;
+
+    private int mActiveRequestCount = 0;
+
+    public AlbumDataAdapter(GalleryContext context,
+            SelectionManager manager, MediaSet source, int cacheSize) {
+        source.setContentListener(this);
+        mSource = source;
+        mSelectionManager = manager;
+        mData = new CoverDisplayItem[cacheSize];
+        mSize = source.getMediaItemCount();
+
+        mWaitLoadingTexture = new ColorTexture(Color.GRAY);
+        mWaitLoadingTexture.setSize(64, 48);
+
+        mHandler = new SynchronizedHandler(context.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                Utils.Assert(message.what == MSG_UPDATE_IMAGE);
+                ((CoverDisplayItem) message.obj).updateImage();
+            }
+        };
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public DisplayItem get(int slotIndex) {
+        if (!isActiveSlot(slotIndex)) {
+            throw new IllegalArgumentException(
+                    String.format("invalid slot: %s outsides (%s, %s)",
+                    slotIndex, mActiveStart, mActiveEnd));
+        }
+        Utils.Assert(isActiveSlot(slotIndex));
+        return mData[slotIndex % mData.length];
+    }
+
+    public int size() {
+        return mSize;
+    }
+
+    public boolean isActiveSlot(int slotIndex) {
+        return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+    }
+
+    private void setContentWindow(int contentStart, int contentEnd) {
+
+        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+        Log.v(TAG, String.format("content range: %s, %s", contentStart, contentEnd));
+        if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
+            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+                freeSlotContent(i);
+            }
+            for (int i = contentStart; i < contentEnd; ++i) {
+                prepareSlotContent(i);
+            }
+        } else {
+            for (int i = mContentStart; i < contentStart; ++i) {
+                freeSlotContent(i);
+            }
+            for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
+                freeSlotContent(i);
+            }
+            for (int i = contentStart, n = mContentStart; i < n; ++i) {
+                prepareSlotContent(i);
+            }
+            for (int i = mContentEnd; i < contentEnd; ++i) {
+                prepareSlotContent(i);
+            }
+        }
+
+        mContentStart = contentStart;
+        mContentEnd = contentEnd;
+    }
+
+    public void setActiveWindow(int start, int end) {
+
+        Utils.Assert(start <= end
+                && end - start <= mData.length && end <= mSize);
+        DisplayItem data[] = mData;
+
+        mActiveStart = start;
+        mActiveEnd = end;
+
+        // If no data is visible, keep the cache content
+        if (start == end) return;
+
+        int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+                0, Math.max(0, mSize - data.length));
+        int contentEnd = Math.min(contentStart + data.length, mSize);
+        if (mContentStart > start || mContentEnd < end
+                || Math.abs(contentStart - mContentStart) > UPDATE_LIMIT) {
+            setContentWindow(contentStart, contentEnd);
+        }
+        updateAllImageRequests();
+    }
+
+    // We would like to request non active slots in the following order:
+    // Order:    8 6 4 2                   1 3 5 7
+    //         |---------|---------------|---------|
+    //                   |<-  active  ->|
+    //         |<-------- cached range ----------->|
+    private void requestNonactiveImages() {
+        Log.v(TAG, "request non active images");
+        int range = Math.max(
+                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+        for (int i = 0 ;i < range; ++i) {
+            requestSlotImage(mActiveEnd + i, false);
+            requestSlotImage(mActiveStart - 1 - i, false);
+        }
+    }
+
+    private void requestSlotImage(int slotIndex, boolean isActive) {
+        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+        CoverDisplayItem item = mData[slotIndex % mData.length];
+        item.requestImageIfNeed();
+    }
+
+    private void freeSlotContent(int slotIndex) {
+        CoverDisplayItem data[] = mData;
+        int index = slotIndex % data.length;
+        CoverDisplayItem original = data[index];
+        if (original != null) {
+            original.recycle();
+            data[index] = null;
+        }
+    }
+
+    private void prepareSlotContent(final int slotIndex) {
+        mData[slotIndex % mData.length] = new CoverDisplayItem(
+                slotIndex, mSource.getMediaItem(slotIndex));
+    }
+
+    private void updateSlotContent(final int slotIndex) {
+
+        MediaItem item = mSource.getMediaItem(slotIndex);
+        CoverDisplayItem data[] = mData;
+        int index = slotIndex % data.length;
+        CoverDisplayItem original = data[index];
+        CoverDisplayItem update = new CoverDisplayItem(slotIndex, item);
+        data[index] = update;
+        if (mListener != null && isActiveSlot(slotIndex)) {
+            mListener.onWindowContentChanged(slotIndex, original, update);
+        }
+        if (original != null) original.recycle();
+        updateAllImageRequests();
+    }
+
+    public void onContentChanged() {
+        int oldSize = mSize;
+        mSize = mSource.getMediaItemCount();
+        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+            updateSlotContent(i);
+        }
+        if (mSize != oldSize && mListener != null) {
+            mListener.onSizeChanged(mSize);
+        }
+        updateAllImageRequests();
+    }
+
+    public void onContentDirty() {
+    }
+
+    private void updateAllImageRequests() {
+        mActiveRequestCount = 0;
+        CoverDisplayItem data[] = mData;
+        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+            CoverDisplayItem item = data[i % data.length];
+            if (item.requestImageIfNeed()) {
+                ++mActiveRequestCount;
+            } else if (item.mState == STATE_UPDATING) {
+                ++mActiveRequestCount;
+            }
+        }
+        if (mActiveRequestCount == 0) requestNonactiveImages();
+    }
+
+    private class CoverDisplayItem
+            extends DisplayItem implements FutureListener<Bitmap> {
+
+        private final int mSlotIndex;
+        private final MediaItem mMediaItem;
+
+        private int mState = STATE_INVALID;
+        private Future<Bitmap> mFuture;
+        private Texture mContent;
+        private Bitmap mBitmap;
+
+        public CoverDisplayItem(int slotIndex, MediaItem item) {
+            Log.v(TAG, "create slot: " + slotIndex);
+            mSlotIndex = slotIndex;
+            mMediaItem = item;
+            updateContent(mWaitLoadingTexture);
+        }
+
+        public void updateImage() {
+            if (mState != STATE_UPDATING) {
+                Log.v(TAG, String.format(
+                        "invalid state %s for slot %s: update image fail", mState, mSlotIndex));
+                mFuture = null;
+                return; /* RECYCLED*/
+            }
+
+            Utils.Assert(mBitmap == null);
+
+            if (isActiveSlot(mSlotIndex)) {
+                --mActiveRequestCount;
+                if (mActiveRequestCount == 0) requestNonactiveImages();
+            }
+            try {
+                mBitmap = mFuture.get();
+                mState = STATE_VALID;
+            } catch (Exception e){
+                mState = STATE_ERROR;
+                Log.w(TAG, "cannot get image" , e);
+                return;
+            } finally {
+                mFuture = null;
+            }
+            updateContent(new BitmapTexture(mBitmap));
+            if (mListener != null) mListener.onContentInvalidated();
+        }
+
+        private void updateContent(Texture content) {
+            mContent = content;
+
+            int width = mContent.getWidth();
+            int height = mContent.getHeight();
+
+            float scale = (float) Math.sqrt(
+                    GalleryView.EXPECTED_AREA / (width * height));
+            width = (int) (width * scale + 0.5f);
+            height = (int) (height * scale + 0.5f);
+
+            int widthLimit = GalleryView.LENGTH_LIMIT;
+            int heightLimit = GalleryView.LENGTH_LIMIT;
+
+            if (width > widthLimit || height > heightLimit) {
+                if (width * heightLimit > height * widthLimit) {
+                    height = height * widthLimit / width;
+                    width = widthLimit;
+                } else {
+                    width = width * heightLimit / height;
+                    height = heightLimit;
+                }
+            }
+            setSize(width, height);
+        }
+
+        @Override
+        public void render(GLCanvas canvas) {
+            SelectionManager manager = mSelectionManager;
+            boolean checked = manager.isSlotSelected(mSlotIndex);
+
+            manager.getSelectionDrawer().draw(
+                    canvas, mContent, mWidth, mHeight, checked);
+        }
+
+        @Override
+        public long getIdentity() {
+            // TODO: should use item's id
+            return System.identityHashCode(this);
+        }
+
+        public void recycle() {
+            if (mBitmap != null) {
+                ((BasicTexture) mContent).recycle();
+                mBitmap.recycle();
+            }
+            mState = STATE_RECYCLED;
+        }
+
+        public void onFutureDone(Future<? extends Bitmap> future) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_IMAGE, this));
+        }
+
+        public boolean requestImageIfNeed() {
+            if (mState != STATE_INVALID) return false;
+            mState = STATE_UPDATING;
+            mFuture = mMediaItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL, this);
+            return true;
+        }
+    }
+
+}
diff --git a/new3d/src/com/android/gallery3d/ui/AlbumView.java b/new3d/src/com/android/gallery3d/ui/AlbumView.java
new file mode 100644
index 0000000..b2f6c3f
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/AlbumView.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import com.android.gallery3d.ui.PositionRepository.Position;
+
+public class AlbumView implements SlotView.Listener {
+    private static final String TAG = "AlbumView";
+
+    private static final int SLOT_WIDTH = 162;
+    private static final int SLOT_HEIGHT = 132;
+    private static final int HORIZONTAL_GAP = 5;
+    private static final int VERTICAL_GAP = 5;
+
+    private final SlotView mSlotView;
+
+    private int mVisibleStart = 0;
+    private int mVisibleEnd = 0;
+
+    private AlbumDataAdapter mModel;
+
+    public AlbumView(Context context, AlbumDataAdapter model, SlotView slotView) {
+        mSlotView = slotView;
+        mModel = model;
+        mModel.setListener(new MyDataModelListener());
+        mSlotView.setSlotSize(SLOT_WIDTH, SLOT_HEIGHT);
+        mSlotView.setSlotCount(model.size());
+        mSlotView.setSlotGaps(HORIZONTAL_GAP, VERTICAL_GAP);
+    }
+
+    private void putSlotContent(int slotIndex, DisplayItem item) {
+        Rect rect = mSlotView.getSlotRect(slotIndex);
+        Position position = new Position(
+                (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2, 0);
+        mSlotView.putDisplayItem(position, item);
+    }
+
+    private void updateVisibleRange(int start, int end) {
+        if (start == mVisibleStart && end == mVisibleEnd) return;
+        Log.v(TAG, String.format("visible range: %s - %s", start, end));
+        if (start >= mVisibleEnd || mVisibleStart >= end) {
+            for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+                mSlotView.removeDisplayItem(mModel.get(i));
+            }
+            mModel.setActiveWindow(start, end);
+            for (int i = start; i < end; ++i) {
+                putSlotContent(i, mModel.get(i));
+            }
+        } else {
+            for (int i = mVisibleStart; i < start; ++i) {
+                mSlotView.removeDisplayItem(mModel.get(i));
+            }
+            for (int i = end, n = mVisibleEnd; i < n; ++i) {
+                mSlotView.removeDisplayItem(mModel.get(i));
+            }
+            mModel.setActiveWindow(start, end);
+            for (int i = start, n = mVisibleStart; i < n; ++i) {
+                putSlotContent(i, mModel.get(i));
+            }
+            for (int i = mVisibleEnd; i < end; ++i) {
+                putSlotContent(i, mModel.get(i));
+            }
+        }
+
+        mVisibleStart = start;
+        mVisibleEnd = end;
+    }
+
+    public void onLayoutChanged(int width, int height) {
+        updateVisibleRange(0, 0);
+        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
+    }
+
+    public void onScrollPositionChanged(int position) {
+        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
+    }
+
+    private class MyDataModelListener implements AlbumDataAdapter.Listener {
+
+        public void onContentInvalidated() {
+            mSlotView.invalidate();
+        }
+
+        public void onSizeChanged(int size) {
+            SlotView slotView = mSlotView;
+            slotView.setSlotCount(size);
+            updateVisibleRange(
+                    slotView.getVisibleStart(), slotView.getVisibleEnd());
+        }
+
+        public void onWindowContentChanged(
+                int slotIndex, DisplayItem old, DisplayItem update) {
+            mSlotView.removeDisplayItem(old);
+            putSlotContent(slotIndex, update);
+        }
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/ui/GalleryAdapter.java b/new3d/src/com/android/gallery3d/ui/GalleryAdapter.java
new file mode 100644
index 0000000..5531a5e
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/GalleryAdapter.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryContext;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.ui.GalleryView.GalleryItem;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.Utils;
+
+public class GalleryAdapter implements MediaSet.MediaSetListener {
+    private static final String TAG = "GalleryAdapter";
+    private static final int UPDATE_LIMIT = 8;
+
+    private static final int STATE_INVALID = 0;
+    private static final int STATE_VALID = 1;
+    private static final int STATE_UPDATING = 2;
+    private static final int STATE_RECYCLED = 3;
+    private static final int STATE_ERROR = -1;
+
+    private static final int MSG_UPDATE_IMAGE = 0;
+
+    public static interface Listener {
+        public void onSizeChanged(int size);
+        public void onContentInvalidated();
+        public void onWindowContentChanged(
+                int slot, GalleryItem old, GalleryItem update);
+    }
+
+    private final MediaSet mSource;
+    private int mSize;
+
+    private int mContentStart = 0;
+    private int mContentEnd = 0;
+
+    private int mActiveStart = 0;
+    private int mActiveEnd = 0;
+
+    private Listener mListener;
+
+    private final GalleryItem mData[];
+    private final SelectionManager mSelectionManager;
+    private final ColorTexture mWaitLoadingTexture;
+
+    private SynchronizedHandler mHandler;
+
+    private int mActiveRequestCount = 0;
+
+    public GalleryAdapter(GalleryContext context,
+            SelectionManager manager, MediaSet source, int cacheSize) {
+        source.setContentListener(this);
+        mSource = source;
+        mSelectionManager = manager;
+        mData = new GalleryItem[cacheSize];
+        mSize = source.getSubMediaSetCount();
+
+        mWaitLoadingTexture = new ColorTexture(Color.GRAY);
+        mWaitLoadingTexture.setSize(64, 48);
+
+        mHandler = new SynchronizedHandler(context.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                Utils.Assert(message.what == MSG_UPDATE_IMAGE);
+                ((CoverDisplayItem) message.obj).updateImage();
+            }
+        };
+
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public GalleryItem get(int slotIndex) {
+        if (!isActiveSlot(slotIndex)) {
+            throw new IllegalArgumentException(
+                    String.format("invalid slot: %s outsides (%s, %s)",
+                    slotIndex, mActiveStart, mActiveEnd));
+        }
+        return mData[slotIndex % mData.length];
+    }
+
+    public int size() {
+        return mSize;
+    }
+
+    public boolean isActiveSlot(int slotIndex) {
+        return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+    }
+
+    private void setContentWindow(int contentStart, int contentEnd) {
+        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+        if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
+            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+                freeSlotContent(i);
+            }
+            for (int i = contentStart; i < contentEnd; ++i) {
+                prepareSlotContent(i);
+            }
+        } else {
+            for (int i = mContentStart; i < contentStart; ++i) {
+                freeSlotContent(i);
+            }
+            for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
+                freeSlotContent(i);
+            }
+            for (int i = contentStart, n = mContentStart; i < n; ++i) {
+                prepareSlotContent(i);
+            }
+            for (int i = mContentEnd; i < contentEnd; ++i) {
+                prepareSlotContent(i);
+            }
+        }
+
+        mContentStart = contentStart;
+        mContentEnd = contentEnd;
+    }
+
+    public void setActiveWindow(int start, int end) {
+        Utils.Assert(start <= end
+                && end - start <= mData.length && end <= mSize);
+        GalleryItem data[] = mData;
+
+        mActiveStart = start;
+        mActiveEnd = end;
+
+        // If no data is visible, keep the cache content
+        if (start == end) return;
+
+        int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+                0, Math.max(0, mSize - data.length));
+        int contentEnd = Math.min(contentStart + data.length, mSize);
+        if (mContentStart > start || mContentEnd < end
+                || Math.abs(contentStart - mContentStart) > UPDATE_LIMIT) {
+            setContentWindow(contentStart, contentEnd);
+        }
+
+        updateAllImageRequests();
+    }
+
+    // We would like to request non active slots in the following order:
+    // Order:    8 6 4 2                   1 3 5 7
+    //         |---------|---------------|---------|
+    //                   |<-  active  ->|
+    //         |<-------- cached range ----------->|
+    private void requestNonactiveImages() {
+        int range = Math.max(
+                mContentEnd - mActiveEnd, mActiveStart - mContentStart);
+        for (int i = 0 ;i < range; ++i) {
+            requestImagesInSlot(mActiveEnd + i, false);
+            requestImagesInSlot(mActiveStart - 1 - i, false);
+        }
+    }
+
+    private void requestImagesInSlot(int slotIndex, boolean isActive) {
+        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+        GalleryItem items = mData[slotIndex % mData.length];
+        for (DisplayItem item : items.covers) {
+            ((CoverDisplayItem) item).requestImageIfNeed();
+        }
+    }
+
+    private void freeSlotContent(int slotIndex) {
+        mSource.getSubMediaSet(slotIndex).setContentListener(null);
+        GalleryItem data[] = mData;
+        int index = slotIndex % data.length;
+        GalleryItem original = data[index];
+        if (original != null) {
+            data[index] = null;
+            for (DisplayItem item : original.covers) {
+                ((CoverDisplayItem) item).recycle();
+            }
+        }
+    }
+
+    private void prepareSlotContent(final int slotIndex) {
+        MediaSet set = mSource.getSubMediaSet(slotIndex);
+        set.setContentListener(new MediaSet.MediaSetListener() {
+            public void onContentDirty() {
+                // TODO: handle dirty event
+            }
+
+            public void onContentChanged() {
+                onSlotChanged(slotIndex);
+            }
+        });
+        GalleryItem item = new GalleryItem();
+        MediaItem[] coverItems = set.getCoverMediaItems();
+        item.covers = new CoverDisplayItem[coverItems.length];
+        for (int i = 0; i < coverItems.length; ++i) {
+            item.covers[i] = new CoverDisplayItem(slotIndex, i, coverItems[i]);
+        }
+        mData[slotIndex % mData.length] = item;
+    }
+
+    private void updateSlotContent(final int slotIndex) {
+        MediaSet set = mSource.getSubMediaSet(slotIndex);
+        set.setContentListener(new MediaSet.MediaSetListener() {
+            public void onContentDirty() {
+                // TODO: handle dirty event
+            }
+
+            public void onContentChanged() {
+                onSlotChanged(slotIndex);
+            }
+        });
+
+        GalleryItem data[] = mData;
+
+        int index = slotIndex % data.length;
+        GalleryItem original = data[index];
+        GalleryItem update = new GalleryItem();
+        data[index] = update;
+
+        MediaItem[] coverItems = set.getCoverMediaItems();
+        update.covers = new CoverDisplayItem[coverItems.length];
+        for (int i = 0; i < coverItems.length; ++i) {
+            CoverDisplayItem cover =
+                    new CoverDisplayItem(slotIndex, i, coverItems[i]);
+            update.covers[i] = cover;
+        }
+        if (mListener != null && isActiveSlot(slotIndex)) {
+            mListener.onWindowContentChanged(slotIndex, original, update);
+        }
+        if (original != null) {
+            for (DisplayItem item : original.covers) {
+                ((CoverDisplayItem) item).recycle();
+            }
+        }
+        updateAllImageRequests();
+    }
+
+    protected void onSlotChanged(int slotIndex) {
+        // If the updated content is not cached, ignore it
+        if (slotIndex < mContentStart || slotIndex >= mContentEnd) {
+            Log.w(TAG, String.format(
+                    "invalid update: %s is outside (%s, %s)",
+                    slotIndex, mContentStart, mContentEnd) );
+            return;
+        }
+        updateSlotContent(slotIndex);
+        boolean isActive = isActiveSlot(slotIndex);
+        if (mActiveRequestCount == 0 || isActive) {
+            for (DisplayItem item : mData[slotIndex % mData.length].covers) {
+                if (((CoverDisplayItem) item).requestImageIfNeed()) {
+                    if (isActive) ++mActiveRequestCount;
+                }
+            }
+        }
+    }
+
+    public void onContentChanged() {
+        int oldSize = mSize;
+        mSize = mSource.getSubMediaSetCount();
+        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+            updateSlotContent(i);
+        }
+        if (mSize != oldSize && mListener != null) {
+            mListener.onSizeChanged(mSize);
+        }
+        updateAllImageRequests();
+    }
+
+    public void onContentDirty() {
+        // TODO: handle dirty event
+    }
+
+    private void updateAllImageRequests() {
+        mActiveRequestCount = 0;
+        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+            for (DisplayItem item : mData[i % mData.length].covers) {
+                CoverDisplayItem coverItem = (CoverDisplayItem) item;
+                if (coverItem.requestImageIfNeed()
+                        || coverItem.mState == STATE_UPDATING) {
+                    ++mActiveRequestCount;
+                }
+            }
+        }
+        if (mActiveRequestCount == 0) requestNonactiveImages();
+    }
+
+    private class CoverDisplayItem
+            extends DisplayItem implements FutureListener<Bitmap> {
+
+        private final int mSlotIndex;
+        private final int mCoverIndex;
+        private final MediaItem mMediaItem;
+
+        private int mState = STATE_INVALID;
+        private Future<Bitmap> mFuture;
+        private Texture mContent;
+        private Bitmap mBitmap;
+
+        public CoverDisplayItem(int slotIndex, int coverIndex, MediaItem item) {
+            mSlotIndex = slotIndex;
+            mCoverIndex = coverIndex;
+            mMediaItem = item;
+            updateContent(mWaitLoadingTexture);
+        }
+
+        public void updateImage() {
+            if (mState != STATE_UPDATING) {
+                Log.v(TAG, String.format(
+                        "invalid update for image: (%s, %s) state: %s",
+                        mSlotIndex, mCoverIndex, mState));
+                mFuture = null;
+                return; /* RECYCLED*/
+            }
+            Log.v(TAG, String.format(
+                    "update for image: (%s, %s)", mSlotIndex, mCoverIndex));
+
+            Utils.Assert(mBitmap == null);
+
+            if (isActiveSlot(mSlotIndex)) {
+                --mActiveRequestCount;
+                if (mActiveRequestCount == 0) requestNonactiveImages();
+            }
+            try {
+                mBitmap = mFuture.get();
+                mState = STATE_VALID;
+            } catch (Exception e){
+                mState = STATE_ERROR;
+                Log.w(TAG, "cannot get image" , e);
+                return;
+            } finally {
+                mFuture = null;
+            }
+            updateContent(new BitmapTexture(mBitmap));
+            if (mListener != null) mListener.onContentInvalidated();
+        }
+
+        private void updateContent(Texture content) {
+            mContent = content;
+
+            int width = mContent.getWidth();
+            int height = mContent.getHeight();
+
+            float scale = (float) Math.sqrt(
+                    GalleryView.EXPECTED_AREA / (width * height));
+            width = (int) (width * scale + 0.5f);
+            height = (int) (height * scale + 0.5f);
+
+            int widthLimit = GalleryView.LENGTH_LIMIT;
+            int heightLimit = GalleryView.LENGTH_LIMIT;
+
+            if (width > widthLimit || height > heightLimit) {
+                if (width * heightLimit > height * widthLimit) {
+                    height = height * widthLimit / width;
+                    width = widthLimit;
+                } else {
+                    width = width * heightLimit / height;
+                    height = heightLimit;
+                }
+            }
+            setSize(width, height);
+        }
+
+        @Override
+        public void render(GLCanvas canvas) {
+            SelectionManager manager = mSelectionManager;
+            boolean topItem = mCoverIndex == 0;
+            boolean checked = topItem && manager.isSlotSelected(mSlotIndex);
+            manager.getSelectionDrawer().draw(
+                    canvas, mContent, mWidth, mHeight, checked, topItem);
+        }
+
+        @Override
+        public long getIdentity() {
+            // TODO: should use item's id
+            return System.identityHashCode(this);
+        }
+
+        public void recycle() {
+            if (mBitmap != null) {
+                ((BasicTexture) mContent).recycle();
+                mBitmap.recycle();
+            }
+            mState = STATE_RECYCLED;
+        }
+
+        public void onFutureDone(Future<? extends Bitmap> future) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_IMAGE, this));
+        }
+
+        public boolean requestImageIfNeed() {
+            if (mState != STATE_INVALID) return false;
+            mState = STATE_UPDATING;
+            Log.v(TAG, String.format("Request image %s %s", mSlotIndex, mCoverIndex));
+            mFuture = mMediaItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL, this);
+            return true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/new3d/src/com/android/gallery3d/ui/GalleryView.java b/new3d/src/com/android/gallery3d/ui/GalleryView.java
new file mode 100644
index 0000000..9308529
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/GalleryView.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Rect;
+
+import com.android.gallery3d.app.GalleryContext;
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.util.Utils;
+
+import java.util.Random;
+
+public class GalleryView implements SlotView.Listener {
+    private static final String TAG = GalleryView.class.getSimpleName();
+
+    static final int LENGTH_LIMIT = 180;
+    static final double EXPECTED_AREA = LENGTH_LIMIT * LENGTH_LIMIT / 2;
+    private static final int SLOT_WIDTH = 220;
+    private static final int SLOT_HEIGHT = 200;
+
+    final SlotView mSlotView;
+
+    private int mVisibleStart;
+    private int mVisibleEnd;
+
+    private Random mRandom = new Random();
+    private long mSeed = mRandom.nextLong();
+
+    final GalleryAdapter mModel;
+
+    public static class GalleryItem {
+        public DisplayItem[] covers;
+    }
+
+    public GalleryView(GalleryContext context, GalleryAdapter model, SlotView slotView) {
+
+        mModel = Utils.checkNotNull(model);
+        mSlotView = slotView;
+        mSlotView.setSlotSize(SLOT_WIDTH, SLOT_HEIGHT);
+        mSlotView.setSlotCount(model.size());
+        mModel.setListener(new MyCacheListener());
+        updateVisibleRange(
+                slotView.getVisibleStart(), slotView.getVisibleEnd());
+    }
+
+    private void putSlotContent(int slotIndex, GalleryItem entry) {
+        // Get displayItems from mItemsetMap or create them from MediaSet.
+        Utils.Assert(entry != null);
+        Rect rect = mSlotView.getSlotRect(slotIndex);
+
+        DisplayItem[] items = entry.covers;
+        mRandom.setSeed(slotIndex ^ mSeed);
+
+        int x = (rect.left + rect.right) / 2;
+        int y = (rect.top + rect.bottom) / 2;
+
+        // Put the cover items in reverse order, so that the first item is on
+        // top of the rest.
+        for (int i = items.length -1; i > 0; --i) {
+            DisplayItem item = items[i];
+            int dx = mRandom.nextInt(11) - 5;
+            int itemX = (i & 0x01) == 0
+                    ? rect.left + dx + item.getWidth() / 2
+                    : rect.right - dx - item.getWidth() / 2;
+            int dy = mRandom.nextInt(11) - 10;
+            int theta = mRandom.nextInt(31) - 15;
+            Position position = new Position();
+            position.set(itemX, y + dy, 0, theta, 1f);
+            mSlotView.putDisplayItem(position, item);
+        }
+        if (items.length > 0) {
+            Position position = new Position();
+            position.set(x, y, 0, 0, 1f);
+            mSlotView.putDisplayItem(position, items[0]);
+        }
+    }
+
+    private void freeSlotContent(int index, GalleryItem entry) {
+        for (DisplayItem item : entry.covers) {
+            mSlotView.removeDisplayItem(item);
+        }
+    }
+
+    public int size() {
+        return mModel.size();
+    }
+
+    public void onLayoutChanged(int width, int height) {
+        updateVisibleRange(0, 0);
+        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
+    }
+
+    public void onScrollPositionChanged(int position) {
+        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
+    }
+
+    private void updateVisibleRange(int start, int end) {
+        if (start == mVisibleStart && end == mVisibleEnd) return;
+        if (start >= mVisibleEnd || mVisibleStart >= end) {
+            for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+                freeSlotContent(i, mModel.get(i));
+            }
+            mModel.setActiveWindow(start, end);
+            for (int i = start; i < end; ++i) {
+                putSlotContent(i, mModel.get(i));
+            }
+        } else {
+            for (int i = mVisibleStart; i < start; ++i) {
+                freeSlotContent(i, mModel.get(i));
+            }
+            for (int i = end, n = mVisibleEnd; i < n; ++i) {
+                freeSlotContent(i, mModel.get(i));
+            }
+            mModel.setActiveWindow(start, end);
+            for (int i = start, n = mVisibleStart; i < n; ++i) {
+                putSlotContent(i, mModel.get(i));
+            }
+            for (int i = mVisibleEnd; i < end; ++i) {
+                putSlotContent(i, mModel.get(i));
+            }
+        }
+        mVisibleStart = start;
+        mVisibleEnd = end;
+        mSlotView.invalidate();
+    }
+
+    private class MyCacheListener implements GalleryAdapter.Listener {
+
+        public void onSizeChanged(int size) {
+            SlotView slotView = mSlotView;
+            slotView.setSlotCount(size);
+            updateVisibleRange(
+                    slotView.getVisibleStart(), slotView.getVisibleEnd());
+        }
+
+        public void onWindowContentChanged(int slot, GalleryItem old, GalleryItem update) {
+            SlotView slotView = mSlotView;
+            freeSlotContent(slot, old);
+            putSlotContent(slot, update);
+            slotView.invalidate();
+        }
+
+        public void onContentInvalidated() {
+            mSlotView.invalidate();
+        }
+    }
+}
\ No newline at end of file
diff --git a/new3d/src/com/android/gallery3d/ui/GridSlotAdapter.java b/new3d/src/com/android/gallery3d/ui/GridSlotAdapter.java
deleted file mode 100644
index bc6db7b..0000000
--- a/new3d/src/com/android/gallery3d/ui/GridSlotAdapter.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.util.Log;
-
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.ui.PositionRepository.Position;
-import com.android.gallery3d.util.Future;
-import com.android.gallery3d.util.FutureListener;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.Map;
-
-public class GridSlotAdapter implements SlotView.Listener {
-    private static final String TAG = "GridSlotAdapter";
-
-    private static final int LENGTH_LIMIT = 162;
-    private static final double EXPECTED_AREA = 150 * 120;
-    private static final int SLOT_WIDTH = 162;
-    private static final int SLOT_HEIGHT = 132;
-    private static final int CACHE_CAPACITY = 48;
-    private static final int HORIZONTAL_GAP = 5;
-    private static final int VERTICAL_GAP = 5;
-
-    private final Map<Integer, MyDisplayItem> mItemMap =
-            new HashMap<Integer, MyDisplayItem>(CACHE_CAPACITY);
-    private final LinkedHashSet<Integer> mLruSlot =
-            new LinkedHashSet<Integer>(CACHE_CAPACITY);
-    private final MediaSet mMediaSet;
-    private final Texture mWaitLoadingTexture;
-    private final SlotView mSlotView;
-    private final SelectionManager mSelectionManager;
-    private boolean mContentInvalidated = false;
-
-    private int mVisibleStart = 0;
-    private int mVisibleEnd = 0;
-
-    public GridSlotAdapter(Context context, MediaSet mediaSet, SlotView slotView,
-            SelectionManager selectionManager) {
-        mSlotView = slotView;
-        mMediaSet = mediaSet;
-        mSelectionManager = selectionManager;
-        ColorTexture gray = new ColorTexture(Color.GRAY);
-        gray.setSize(64, 48);
-        mWaitLoadingTexture = gray;
-        mediaSet.setContentListener(new MyContentListener());
-        mSlotView.setSlotSize(SLOT_WIDTH, SLOT_HEIGHT);
-        mSlotView.setSlotCount(mMediaSet.getMediaItemCount());
-        mSlotView.setSlotGaps(HORIZONTAL_GAP, VERTICAL_GAP);
-    }
-
-    private void freeSlot(int slotIndex) {
-        MyDisplayItem displayItem = mItemMap.get(slotIndex);
-        mSlotView.removeDisplayItem(displayItem);
-        mLruSlot.add(slotIndex);
-    }
-
-    private void putSlot(int slotIndex) {
-        MyDisplayItem displayItem = mItemMap.get(slotIndex);
-        if (displayItem == null || mContentInvalidated) {
-            MediaItem item = mMediaSet.getMediaItem(slotIndex);
-            displayItem = new MyDisplayItem(slotIndex, mWaitLoadingTexture,
-                    mSelectionManager.getSelectionDrawer());
-            mItemMap.put(slotIndex, displayItem);
-            item.requestImage(MediaItem.TYPE_MICROTHUMBNAIL,
-                    new MyMediaItemListener(slotIndex));
-
-            // Remove an item if the size of mItemsetMap is no less than
-            // CACHE_CAPACITY and there exists a slot in mLruSlot.
-            Iterator<Integer> iter = mLruSlot.iterator();
-            while (mItemMap.size() >= CACHE_CAPACITY && iter.hasNext()) {
-                mItemMap.remove(iter.next());
-                iter.remove();
-            }
-        }
-
-        // Reclaim the slot
-        mLruSlot.remove(slotIndex);
-
-        Rect rect = mSlotView.getSlotRect(slotIndex);
-        Position position = new Position(
-                (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2, 0);
-
-        mSlotView.putDisplayItem(position, displayItem);
-    }
-
-
-    private class MyMediaItemListener implements FutureListener<Bitmap> {
-
-        private final int mSlotIndex;
-
-        public MyMediaItemListener(int slotIndex) {
-            mSlotIndex = slotIndex;
-        }
-
-        public void onFutureDone(Future<? extends Bitmap> future) {
-            try {
-                MyDisplayItem displayItem = mItemMap.get(mSlotIndex);
-                displayItem.updateContent(new BitmapTexture(future.get()));
-                mSlotView.invalidate();
-            } catch (Exception e) {
-                Log.v(TAG, "cannot get image", e);
-            }
-        }
-    }
-
-    private void onContentChanged() {
-        // remove all items
-        updateVisibleRange(0, 0);
-        mItemMap.clear();
-        mLruSlot.clear();
-
-        mSlotView.setSlotCount(mMediaSet.getMediaItemCount());
-        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
-    }
-
-    private class MyDisplayItem extends DisplayItem {
-
-        private Texture mContent;
-        private final SelectionDrawer mDrawer;
-        private int mIndex;
-
-        public MyDisplayItem(int index, Texture content, SelectionDrawer drawer) {
-            mIndex = index;
-            mDrawer = drawer;
-            updateContent(content);
-        }
-
-        public synchronized void updateContent(Texture content) {
-            mContent = content;
-            Rect p = mDrawer.getFramePadding();
-
-            int width = mContent.getWidth();
-            int height = mContent.getHeight();
-
-            float scale = (float) Math.sqrt(EXPECTED_AREA / (width * height));
-            width = (int) (width * scale + 0.5f);
-            height = (int) (height * scale + 0.5f);
-
-            int widthLimit = LENGTH_LIMIT - p.left - p.right;
-            int heightLimit = LENGTH_LIMIT - p.top - p.bottom;
-
-            if (width > widthLimit || height > heightLimit) {
-                if (width * heightLimit > height * widthLimit) {
-                    height = height * widthLimit / width;
-                    width = widthLimit;
-                } else {
-                    width = width * heightLimit / height;
-                    height = heightLimit;
-                }
-            }
-            setSize(width + p.left + p.right, height + p.top + p.bottom);
-        }
-
-        @Override
-        public synchronized void render(GLCanvas canvas) {
-            boolean checked = mSelectionManager.isSlotSelected(mIndex);
-            mDrawer.draw(canvas, mContent, mWidth, mHeight, checked);
-        }
-
-        @Override
-        public long getIdentity() {
-            // TODO: change to use the item's id
-            return System.identityHashCode(this);
-        }
-    }
-
-    private class MyContentListener implements MediaSet.MediaSetListener {
-        public void onContentChanged() {
-            GridSlotAdapter.this.onContentChanged();
-        }
-
-        public void onContentDirty() {
-            //TODO: reload the album
-        }
-    }
-
-    public void updateVisibleRange(int start, int end) {
-        if (start == mVisibleStart && end == mVisibleEnd) return;
-
-        if (start >= mVisibleEnd || mVisibleStart >= end) {
-            for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
-                freeSlot(i);
-            }
-            for (int i = start; i < end; ++i) {
-                putSlot(i);
-            }
-        } else {
-            for (int i = mVisibleStart; i < start; ++i) {
-                freeSlot(i);
-            }
-            for (int i = end, n = mVisibleEnd; i < n; ++i) {
-                freeSlot(i);
-            }
-            for (int i = start, n = mVisibleStart; i < n; ++i) {
-                putSlot(i);
-            }
-            for (int i = mVisibleEnd; i < end; ++i) {
-                putSlot(i);
-            }
-        }
-        mVisibleStart = start;
-        mVisibleEnd = end;
-    }
-
-    public void onLayoutChanged(int width, int height) {
-        updateVisibleRange(0, 0);
-        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
-    }
-
-    public void onScrollPositionChanged(int position) {
-        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
-    }
-}
diff --git a/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java b/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java
deleted file mode 100644
index de01dce..0000000
--- a/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.util.Log;
-
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.ui.PositionRepository.Position;
-import com.android.gallery3d.util.Future;
-import com.android.gallery3d.util.FutureListener;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Random;
-
-public class MediaSetSlotAdapter implements SlotView.Listener {
-    private static final String TAG = MediaSetSlotAdapter.class.getSimpleName();
-
-    private static final int LENGTH_LIMIT = 180;
-    private static final double EXPECTED_AREA = LENGTH_LIMIT * LENGTH_LIMIT / 2;
-    private static final int SLOT_WIDTH = 220;
-    private static final int SLOT_HEIGHT = 200;
-    private static final int MARGIN_TO_SLOTSIDE = 10;
-    private static final int CACHE_CAPACITY = 32;
-    private static final int INDEX_NONE = -1;
-
-    private final MediaSet mRootSet;
-    private final Texture mWaitLoadingTexture;
-
-    private final Map<Integer, MyDisplayItems> mItemsetMap =
-            new HashMap<Integer, MyDisplayItems>(CACHE_CAPACITY);
-    private final LinkedHashSet<Integer> mLruSlot =
-            new LinkedHashSet<Integer>(CACHE_CAPACITY);
-
-    private final SlotView mSlotView;
-    private final SelectionManager mSelectionManager;
-
-    private boolean mContentInvalidated = false;
-    private int mInvalidateIndex = INDEX_NONE;
-
-    private int mVisibleStart;
-    private int mVisibleEnd;
-
-    public MediaSetSlotAdapter(Context context,
-            MediaSet rootSet, SlotView slotView, SelectionManager manager) {
-        mRootSet = rootSet;
-        mSelectionManager = manager;
-        ColorTexture gray = new ColorTexture(Color.GRAY);
-        gray.setSize(64, 48);
-        mWaitLoadingTexture = gray;
-
-        mSlotView = slotView;
-        mSlotView.setSlotSize(SLOT_WIDTH, SLOT_HEIGHT);
-
-        rootSet.setContentListener(new MyContentListener());
-    }
-
-    private void putSlot(int slotIndex) {
-
-        // Get displayItems from mItemsetMap or create them from MediaSet.
-        MyDisplayItems displayItems = mItemsetMap.get(slotIndex);
-        if (displayItems == null
-                || mContentInvalidated || mInvalidateIndex == slotIndex) {
-            displayItems = createDisplayItems(slotIndex);
-        }
-        // Reclaim the slot
-        mLruSlot.remove(slotIndex);
-
-        Rect rect = mSlotView.getSlotRect(slotIndex);
-
-        displayItems.putSlot(mSlotView,
-                (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2,
-                rect.left + MARGIN_TO_SLOTSIDE, rect.right - MARGIN_TO_SLOTSIDE,
-                slotIndex);
-    }
-
-    private MyDisplayItems createDisplayItems(int slotIndex) {
-        MediaSet set = mRootSet.getSubMediaSet(slotIndex);
-        set.setContentListener(new SlotContentListener(slotIndex));
-
-        MediaItem[] items = set.getCoverMediaItems();
-        MyDisplayItems displayItems = new MyDisplayItems(mSelectionManager, items.length);
-        addSlotToCache(slotIndex, displayItems);
-
-        SelectionDrawer drawer = mSelectionManager.getSelectionDrawer();
-        for (int i = 0; i < items.length; ++i) {
-            items[i].requestImage(MediaItem.TYPE_MICROTHUMBNAIL,
-                    new MyMediaItemListener(slotIndex, i));
-            int itemIndex = i == 0 ? slotIndex : INDEX_NONE;
-            MyDisplayItem item = new MyDisplayItem(
-                    itemIndex, mWaitLoadingTexture, drawer);
-            displayItems.setDisplayItem(i, item);
-        }
-
-        return displayItems;
-    }
-
-    private void addSlotToCache(int slotIndex, MyDisplayItems displayItems) {
-        mItemsetMap.put(slotIndex, displayItems);
-
-        // Remove an itemset if the size of mItemsetMap is no less than
-        // INITIAL_CACHE_CAPACITY and there exists a slot in mLruSlot.
-        Iterator<Integer> iter = mLruSlot.iterator();
-        for (int i = mItemsetMap.size() - CACHE_CAPACITY;
-                i >= 0 && iter.hasNext(); --i) {
-            mItemsetMap.remove(iter.next());
-            iter.remove();
-        }
-    }
-
-    public int size() {
-        return mRootSet.getSubMediaSetCount();
-    }
-
-    private class MyMediaItemListener implements FutureListener<Bitmap> {
-
-        private final int mSlotIndex;
-        private final int mItemIndex;
-
-        public MyMediaItemListener(int slotIndex, int itemIndex) {
-            mSlotIndex = slotIndex;
-            mItemIndex = itemIndex;
-        }
-
-        public void onFutureDone(Future<? extends Bitmap> future) {
-            try {
-                MyDisplayItems items = mItemsetMap.get(mSlotIndex);
-                items.updateContent(mItemIndex, new BitmapTexture(future.get()));
-                mSlotView.invalidate();
-            } catch (Exception e) {
-                Log.v(TAG, "cannot get image", e);
-            }
-        }
-    }
-
-    private class MyDisplayItem extends DisplayItem {
-        private int mIndex;
-        private Texture mContent;
-        private final SelectionDrawer mDrawer;
-
-        public MyDisplayItem(int index, Texture content, SelectionDrawer drawer) {
-            mIndex = index;
-            mDrawer = drawer;
-            updateContent(content);
-        }
-
-        public void updateContent(Texture content) {
-            mContent = content;
-            Rect p = mDrawer.getFramePadding();
-
-            int width = mContent.getWidth();
-            int height = mContent.getHeight();
-
-            float scale = (float) Math.sqrt(EXPECTED_AREA / (width * height));
-            width = (int) (width * scale + 0.5f);
-            height = (int) (height * scale + 0.5f);
-
-            int widthLimit = LENGTH_LIMIT - p.left - p.right;
-            int heightLimit = LENGTH_LIMIT - p.top - p.bottom;
-
-            if (width > widthLimit || height > heightLimit) {
-                if (width * heightLimit > height * widthLimit) {
-                    height = height * widthLimit / width;
-                    width = widthLimit;
-                } else {
-                    width = width * heightLimit / height;
-                    height = heightLimit;
-                }
-            }
-            setSize(width + p.left + p.right, height + p.top + p.bottom);
-        }
-
-        @Override
-        public void render(GLCanvas canvas) {
-            boolean topItem = mIndex != INDEX_NONE;
-            boolean checked = mSelectionManager.isSlotSelected(mIndex);
-            mDrawer.draw(canvas, mContent, mWidth, mHeight, checked, topItem);
-        }
-
-        @Override
-        public long getIdentity() {
-            // TODO: should use item's id
-            return System.identityHashCode(this);
-        }
-    }
-
-    private static class MyDisplayItems {
-        MyDisplayItem[] mDisplayItems;
-        SelectionManager mSelectionManager;
-        Random mRandom = new Random();
-
-        public MyDisplayItems(SelectionManager manager, int size) {
-            mSelectionManager = manager;
-            mDisplayItems = new MyDisplayItem[size];
-        }
-
-        public void setDisplayItem(int itemIndex, MyDisplayItem displayItem) {
-            mDisplayItems[itemIndex] = displayItem;
-        }
-
-        public void updateContent(int itemIndex, Texture texture) {
-            mDisplayItems[itemIndex].updateContent(texture);
-        }
-
-        public void putSlot(
-                SlotView panel, int x, int y, int l, int r, int slotIndex) {
-            // Put the cover items in reverse order, so that the first item is on
-            // top of the rest.
-            for (int i = mDisplayItems.length -1; i > 0; --i) {
-                int dx = mRandom.nextInt(11) - 5;
-                int itemX = (i & 0x01) == 0
-                        ? l + dx + mDisplayItems[i].getWidth() / 2
-                        : r + dx - mDisplayItems[i].getWidth() / 2;
-                int dy = mRandom.nextInt(11) - 10;
-                int theta = mRandom.nextInt(31) - 15;
-                Position position = new Position();
-                position.set(itemX, y + dy, 0, theta, 1f);
-                panel.putDisplayItem(position, mDisplayItems[i]);
-            }
-            if (mDisplayItems.length > 0) {
-                Position position = new Position();
-                position.set(x, y, 0, 0, 1f);
-                panel.putDisplayItem(position, mDisplayItems[0]);
-            }
-        }
-
-        public void removeDisplayItems(SlotView panel) {
-            for (MyDisplayItem item : mDisplayItems) {
-                panel.removeDisplayItem(item);
-            }
-        }
-    }
-
-    private void freeSlot(int index) {
-        MyDisplayItems displayItems;
-        if (mContentInvalidated) {
-            displayItems = mItemsetMap.remove(index);
-        } else {
-            displayItems = mItemsetMap.get(index);
-            mLruSlot.add(index);
-        }
-
-        displayItems.removeDisplayItems(mSlotView);
-    }
-
-    private void onContentChanged() {
-        updateVisibleRange(0, 0);
-        mItemsetMap.clear();
-        mLruSlot.clear();
-
-        mSlotView.setSlotCount(mRootSet.getSubMediaSetCount());
-        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
-    }
-
-    private class SlotContentListener implements MediaSet.MediaSetListener {
-        private final int mSlotIndex;
-
-        public SlotContentListener(int slotIndex) {
-            mSlotIndex = slotIndex;
-        }
-
-        public void onContentChanged() {
-            // Update the corresponding itemset to the slot based on whether
-            // the slot is visible or in mLruSlot.
-            // Remove the original corresponding itemset from cache if the
-            // slot was already in mLruSlot. That is the itemset was invisible
-            // and freed before.
-            if (mLruSlot.remove(mSlotIndex)) {
-                mItemsetMap.remove(mSlotIndex);
-            } else {
-                // Refresh the corresponding items in the slot if the slot is
-                // visible. Note that only visible slots are refreshed in
-                // mSlotView.notifySlotInvalidate(mSlotIndex).
-                freeSlot(mSlotIndex);
-                mItemsetMap.remove(mSlotIndex);
-                putSlot(mSlotIndex);
-                mSlotView.invalidate();
-            }
-        }
-
-        public void onContentDirty() {
-            // TODO Reload the album set
-        }
-    }
-
-    private class MyContentListener implements MediaSet.MediaSetListener {
-
-        public void onContentChanged() {
-            MediaSetSlotAdapter.this.onContentChanged();
-        }
-
-        public void onContentDirty() {
-            // TODO Reload the album set
-        }
-    }
-
-    public void updateVisibleRange(int start, int end) {
-        if (start == mVisibleStart && end == mVisibleEnd) return;
-
-        if (start >= mVisibleEnd || mVisibleStart >= end) {
-            for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
-                freeSlot(i);
-            }
-            for (int i = start; i < end; ++i) {
-                putSlot(i);
-            }
-        } else {
-            for (int i = mVisibleStart; i < start; ++i) {
-                freeSlot(i);
-            }
-            for (int i = end, n = mVisibleEnd; i < n; ++i) {
-                freeSlot(i);
-            }
-            for (int i = start, n = mVisibleStart; i < n; ++i) {
-                putSlot(i);
-            }
-            for (int i = mVisibleEnd; i < end; ++i) {
-                putSlot(i);
-            }
-        }
-        mVisibleEnd = end;
-        mVisibleStart = start;
-    }
-
-    public void onLayoutChanged(int width, int height) {
-        updateVisibleRange(0, 0);
-        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
-    }
-
-    public void onScrollPositionChanged(int position) {
-        updateVisibleRange(mSlotView.getVisibleStart(), mSlotView.getVisibleEnd());
-    }
-}
\ No newline at end of file
diff --git a/new3d/src/com/android/gallery3d/ui/PositionRepository.java b/new3d/src/com/android/gallery3d/ui/PositionRepository.java
index 9d08985..6667b4c 100644
--- a/new3d/src/com/android/gallery3d/ui/PositionRepository.java
+++ b/new3d/src/com/android/gallery3d/ui/PositionRepository.java
@@ -70,11 +70,28 @@
     }
 
     private HashMap<Long, Position> mData = new HashMap<Long, Position>();
+    private float mOffsetX;
+    private float mOffsetY;
+    private float mOffsetZ;
 
     public Position get(Long identity) {
         return mData.get(identity);
     }
 
+    public void setPositionOffset(int offsetX, int offsetY, int offsetZ) {
+        float deltaX = offsetX - mOffsetX;
+        float deltaY = offsetY - mOffsetY;
+        float deltaZ = offsetZ - mOffsetZ;
+        mOffsetX = offsetX;
+        mOffsetY = offsetY;
+        mOffsetZ = offsetZ;
+        for (Position position : mData.values()) {
+            position.x += deltaX;
+            position.y += deltaY;
+            position.z += deltaZ;
+        }
+    }
+
     public void putPosition(Long identity, Position position) {
         mData.put(identity, position);
     }