Merge "New UI..." into gb-ub-photos-arches
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
index 7af1d44..c8c3514 100644
--- a/src/com/android/gallery3d/app/AlbumSetPage.java
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -37,6 +37,7 @@
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.Path;
@@ -60,6 +61,7 @@
 import com.android.gallery3d.util.HelpUtils;
 
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 
 public class AlbumSetPage extends ActivityState implements
         SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
@@ -211,7 +213,10 @@
 
     private static boolean albumShouldOpenInFilmstrip(MediaSet album) {
         int itemCount = album.getMediaItemCount();
-        return (album.isCameraRoll() && itemCount > 0) || itemCount == 1;
+        ArrayList<MediaItem> list = (itemCount == 1) ? album.getMediaItem(0, 1) : null;
+        return (album.isCameraRoll() && itemCount > 0)
+                // open in film strip only if there's one item in the album and the item exists
+                || (list != null && !list.isEmpty());
     }
 
     WeakReference<Toast> mEmptyAlbumToast = null;
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
index 66f2874..5ab022a 100644
--- a/src/com/android/gallery3d/app/PhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -35,6 +35,7 @@
 import com.android.gallery3d.ui.ScreenNail;
 import com.android.gallery3d.ui.SynchronizedHandler;
 import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.ui.TiledTexture;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.FutureListener;
 import com.android.gallery3d.util.MediaSetUtils;
@@ -59,8 +60,8 @@
     private static final int MSG_RUN_OBJECT = 3;
     private static final int MSG_UPDATE_IMAGE_REQUESTS = 4;
 
-    private static final int MIN_LOAD_COUNT = 8;
-    private static final int DATA_CACHE_SIZE = 32;
+    private static final int MIN_LOAD_COUNT = 16;
+    private static final int DATA_CACHE_SIZE = 256;
     private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX;
     private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1;
 
@@ -162,6 +163,7 @@
     private DataListener mDataListener;
 
     private final SourceListener mSourceListener = new SourceListener();
+    private final TiledTexture.Uploader mUploader;
 
     // The path of the current viewing item will be stored in mItemPath.
     // If mItemPath is not null, mCurrentIndex is only a hint for where we
@@ -183,6 +185,8 @@
 
         Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
 
+        mUploader = new TiledTexture.Uploader(activity.getGLRoot());
+
         mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
             @SuppressWarnings("unchecked")
             @Override
@@ -321,6 +325,7 @@
             }
         }
         updateImageRequests();
+        updateScreenNailUploadQueue();
     }
 
     private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) {
@@ -345,6 +350,8 @@
     @Override
     public void resume() {
         mIsActive = true;
+        TiledTexture.prepareResources();
+
         mSource.addContentListener(mSourceListener);
         updateImageCache();
         updateImageRequests();
@@ -371,6 +378,9 @@
         }
         mImageCache.clear();
         mTileProvider.clear();
+
+        mUploader.clear();
+        TiledTexture.freeResources();
     }
 
     private MediaItem getItem(int index) {
@@ -394,6 +404,7 @@
         updateImageCache();
         updateImageRequests();
         updateTileProvider();
+        updateScreenNailUploadQueue();
 
         if (mDataListener != null) {
             mDataListener.onPhotoChanged(index, mItemPath);
@@ -402,6 +413,32 @@
         fireDataChange();
     }
 
+    private void uploadScreenNail(int offset) {
+        int index = mCurrentIndex + offset;
+        if (index < mActiveStart || index >= mActiveEnd) return;
+
+        MediaItem item = getItem(index);
+        if (item == null) return;
+
+        ImageEntry e = mImageCache.get(item.getPath());
+        if (e == null) return;
+
+        ScreenNail s = e.screenNail;
+        if (s instanceof BitmapScreenNail) {
+            TiledTexture t = ((BitmapScreenNail) s).getTexture();
+            if (t != null && !t.isReady()) mUploader.addTexture(t);
+        }
+    }
+
+    private void updateScreenNailUploadQueue() {
+        mUploader.clear();
+        uploadScreenNail(0);
+        for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
+            uploadScreenNail(i);
+            uploadScreenNail(-i);
+        }
+    }
+
     @Override
     public void moveTo(int index) {
         updateCurrentIndex(index);
diff --git a/src/com/android/gallery3d/ui/BasicTexture.java b/src/com/android/gallery3d/ui/BasicTexture.java
index 7b8e30d..99cf057 100644
--- a/src/com/android/gallery3d/ui/BasicTexture.java
+++ b/src/com/android/gallery3d/ui/BasicTexture.java
@@ -42,8 +42,8 @@
     protected int mWidth = UNSPECIFIED;
     protected int mHeight = UNSPECIFIED;
 
-    private int mTextureWidth;
-    private int mTextureHeight;
+    protected int mTextureWidth;
+    protected int mTextureHeight;
 
     private boolean mHasBorder;
 
diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java
index 6fbcf01..25f88a1 100644
--- a/src/com/android/gallery3d/ui/BitmapScreenNail.java
+++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java
@@ -46,16 +46,16 @@
 
     private int mWidth;
     private int mHeight;
-    private Bitmap mBitmap;
-    private BitmapTexture mTexture;
     private long mAnimationStartTime = ANIMATION_NOT_NEEDED;
 
+    private Bitmap mBitmap;
+    private TiledTexture mTexture;
+
     public BitmapScreenNail(Bitmap bitmap) {
         mWidth = bitmap.getWidth();
         mHeight = bitmap.getHeight();
         mBitmap = bitmap;
-        // We create mTexture lazily, so we don't incur the cost if we don't
-        // actually need it.
+        mTexture = new TiledTexture(bitmap);
     }
 
     public BitmapScreenNail(int width, int height) {
@@ -103,17 +103,14 @@
         BitmapScreenNail newer = (BitmapScreenNail) other;
         mWidth = newer.mWidth;
         mHeight = newer.mHeight;
-        if (newer.mBitmap != null) {
+        if (newer.mTexture != null) {
             recycleBitmap(MediaItem.getThumbPool(), mBitmap);
+            if (mTexture != null) mTexture.recycle();
             mBitmap = newer.mBitmap;
+            mTexture = newer.mTexture;
             newer.mBitmap = null;
-
-            if (mTexture != null) {
-                mTexture.recycle();
-                mTexture = null;
-            }
+            newer.mTexture = null;
         }
-
         newer.recycle();
         return this;
     }
@@ -158,7 +155,7 @@
 
     @Override
     public void draw(GLCanvas canvas, int x, int y, int width, int height) {
-        if (mBitmap == null) {
+        if (mTexture == null || !mTexture.isReady()) {
             if (mAnimationStartTime == ANIMATION_NOT_NEEDED) {
                 mAnimationStartTime = ANIMATION_NEEDED;
             }
@@ -168,16 +165,12 @@
             return;
         }
 
-        if (mTexture == null) {
-            mTexture = new BitmapTexture(mBitmap);
-        }
-
         if (mAnimationStartTime == ANIMATION_NEEDED) {
-            mAnimationStartTime = now();
+            mAnimationStartTime = AnimationTime.get();
         }
 
         if (isAnimating()) {
-            canvas.drawMixed(mTexture, mPlaceholderColor, getRatio(), x, y,
+            mTexture.drawMixed(canvas, mPlaceholderColor, getRatio(), x, y,
                     width, height);
         } else {
             mTexture.draw(canvas, x, y, width, height);
@@ -186,38 +179,34 @@
 
     @Override
     public void draw(GLCanvas canvas, RectF source, RectF dest) {
-        if (mBitmap == null) {
+        if (mTexture == null || !mTexture.isReady()) {
             canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(),
                     mPlaceholderColor);
             return;
         }
 
-        if (mTexture == null) {
-            mTexture = new BitmapTexture(mBitmap);
-        }
-
-        canvas.drawTexture(mTexture, source, dest);
+        mTexture.draw(canvas, source, dest);
     }
 
     public boolean isAnimating() {
         if (mAnimationStartTime < 0) return false;
-        if (now() - mAnimationStartTime >= DURATION) {
+        if (AnimationTime.get() - mAnimationStartTime >= DURATION) {
             mAnimationStartTime = ANIMATION_DONE;
             return false;
         }
         return true;
     }
 
-    private static long now() {
-        return AnimationTime.get();
-    }
-
     private float getRatio() {
-        float r = (float)(now() - mAnimationStartTime) / DURATION;
+        float r = (float) (AnimationTime.get() - mAnimationStartTime) / DURATION;
         return Utils.clamp(1.0f - r, 0.0f, 1.0f);
     }
 
     public boolean isShowingPlaceholder() {
         return (mBitmap == null) || isAnimating();
     }
+
+    public TiledTexture getTexture() {
+        return mTexture;
+    }
 }
diff --git a/src/com/android/gallery3d/ui/GLCanvas.java b/src/com/android/gallery3d/ui/GLCanvas.java
index e3a32ef..6f8baef 100644
--- a/src/com/android/gallery3d/ui/GLCanvas.java
+++ b/src/com/android/gallery3d/ui/GLCanvas.java
@@ -99,6 +99,13 @@
     public void drawMixed(BasicTexture from, int toColor,
             float ratio, int x, int y, int w, int h);
 
+    // Draw a region of a texture and a specified color to the specified
+    // rectangle. The actual color used is from * (1 - ratio) + to * ratio.
+    // The region of the texture is defined by parameter "src". The target
+    // rectangle is specified by parameter "target".
+    public void drawMixed(BasicTexture from, int toColor,
+            float ratio, RectF src, RectF target);
+
     // Gets the underlying GL instance. This is used only when direct access to
     // GL is needed.
     public GL11 getGLInstance();
diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java
index d83daf3..45903b3 100644
--- a/src/com/android/gallery3d/ui/GLCanvasImpl.java
+++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java
@@ -415,7 +415,7 @@
     // This function changes the source coordinate to the texture coordinates.
     // It also clips the source and target coordinates if it is beyond the
     // bound of the texture.
-    private void convertCoordinate(RectF source, RectF target,
+    private static void convertCoordinate(RectF source, RectF target,
             BasicTexture texture) {
 
         int width = texture.getWidth();
@@ -465,6 +465,82 @@
         color[3] = alpha;
     }
 
+    private void setMixedColor(int toColor, float ratio, float alpha) {
+        //
+        // The formula we want:
+        //     alpha * ((1 - ratio) * from + ratio * to)
+        //
+        // The formula that GL supports is in the form of:
+        //     combo * from + (1 - combo) * to * scale
+        //
+        // So, we have combo = alpha * (1 - ratio)
+        //     and     scale = alpha * ratio / (1 - combo)
+        //
+        float combo = alpha * (1 - ratio);
+        float scale = alpha * ratio / (1 - combo);
+
+        // Specify the interpolation factor via the alpha component of
+        // GL_TEXTURE_ENV_COLORs.
+        // RGB component are get from toColor and will used as SRC1
+        float colorScale = scale * (toColor >>> 24) / (0xff * 0xff);
+        setTextureColor(((toColor >>> 16) & 0xff) * colorScale,
+                ((toColor >>> 8) & 0xff) * colorScale,
+                (toColor & 0xff) * colorScale, combo);
+        GL11 gl = mGL;
+        gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
+
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);
+
+        // Wire up the interpolation factor for RGB.
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
+
+        // Wire up the interpolation factor for alpha.
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+
+    }
+
+    @Override
+    public void drawMixed(BasicTexture from, int toColor, float ratio,
+            RectF source, RectF target) {
+        if (target.width() <= 0 || target.height() <= 0) return;
+
+        if (ratio <= 0.01f) {
+            drawTexture(from, source, target);
+            return;
+        } else if (ratio >= 1) {
+            fillRect(target.left, target.top, target.width(), target.height(), toColor);
+            return;
+        }
+
+        float alpha = mAlpha;
+
+        // Copy the input to avoid changing it.
+        mDrawTextureSourceRect.set(source);
+        mDrawTextureTargetRect.set(target);
+        source = mDrawTextureSourceRect;
+        target = mDrawTextureTargetRect;
+
+        mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
+                || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));
+
+        if (!bindTexture(from)) return;
+
+        // Interpolate the RGB and alpha values between both textures.
+        mGLState.setTexEnvMode(GL11.GL_COMBINE);
+        setMixedColor(toColor, ratio, alpha);
+        convertCoordinate(source, target, from);
+        setTextureCoords(source);
+        textureRect(target.left, target.top, target.width(), target.height());
+        mGLState.setTexEnvMode(GL11.GL_REPLACE);
+    }
+
     private void drawMixed(BasicTexture from, int toColor,
             float ratio, int x, int y, int width, int height, float alpha) {
         // change from 0 to 0.01f to prevent getting divided by zero below
@@ -482,45 +558,9 @@
         final GL11 gl = mGL;
         if (!bindTexture(from)) return;
 
-        //
-        // The formula we want:
-        //     alpha * ((1 - ratio) * from + ratio * to)
-        //
-        // The formula that GL supports is in the form of:
-        //     combo * from + (1 - combo) * to * scale
-        //
-        // So, we have combo = alpha * (1 - ratio)
-        //     and     scale = alpha * ratio / (1 - combo)
-        //
-        float combo = alpha * (1 - ratio);
-        float scale = alpha * ratio / (1 - combo);
-
         // Interpolate the RGB and alpha values between both textures.
         mGLState.setTexEnvMode(GL11.GL_COMBINE);
-
-        // Specify the interpolation factor via the alpha component of
-        // GL_TEXTURE_ENV_COLORs.
-        // RGB component are get from toColor and will used as SRC1
-        float colorScale = scale * (toColor >>> 24) / (0xff * 0xff);
-        setTextureColor(((toColor >>> 16) & 0xff) * colorScale,
-                ((toColor >>> 8) & 0xff) * colorScale,
-                (toColor & 0xff) * colorScale, combo);
-        gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
-
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);
-
-        // Wire up the interpolation factor for RGB.
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
-
-        // Wire up the interpolation factor for alpha.
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+        setMixedColor(toColor, ratio, alpha);
 
         drawBoundTexture(from, x, y, width, height);
         mGLState.setTexEnvMode(GL11.GL_REPLACE);
diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
index 5ce06be..18a7af8 100644
--- a/src/com/android/gallery3d/ui/TileImageView.java
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -575,8 +575,10 @@
                 }
                 if (tile == null) break;
                 if (!tile.isContentValid()) {
+                    boolean hasBeenLoaded = tile.isLoaded();
                     Utils.assertTrue(tile.mTileState == STATE_DECODED);
                     tile.updateContent(canvas);
+                    if (!hasBeenLoaded) tile.draw(canvas, 0, 0);
                     --quota;
                 }
             }
@@ -621,7 +623,6 @@
         }
     }
 
-    // TODO: avoid drawing the unused part of the textures.
     static boolean drawTile(
             Tile tile, GLCanvas canvas, RectF source, RectF target) {
         while (true) {
diff --git a/src/com/android/gallery3d/ui/TiledTexture.java b/src/com/android/gallery3d/ui/TiledTexture.java
new file mode 100644
index 0000000..6e9ad9e
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TiledTexture.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2012 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.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RectF;
+
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+
+// This class is similar to BitmapTexture, except the bitmap is
+// split into tiles. By doing so, we may increase the time required to
+// upload the whole bitmap but we reduce the time of uploading each tile
+// so it make the animation more smooth and prevents jank.
+public class TiledTexture {
+    private static final int CONTENT_SIZE = 254;
+    private static final int BORDER_SIZE = 1;
+    private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE;
+    private static final int INIT_CAPACITY = 8;
+
+    private static Tile sFreeTileHead = null;
+    private static final Object sFreeTileLock = new Object();
+
+    private static Bitmap sUploadBitmap;
+    private static Canvas sCanvas;
+    private static Paint sPaint;
+
+    private int mUploadIndex = 0;
+
+    private final Tile[] mTiles;
+    private final int mWidth;
+    private final int mHeight;
+    private final RectF mSrcRect = new RectF();
+    private final RectF mDestRect = new RectF();
+
+    public static class Uploader implements OnGLIdleListener {
+        private final ArrayDeque<TiledTexture> mTextures =
+                new ArrayDeque<TiledTexture>(INIT_CAPACITY);
+
+        private final GLRoot mGlRoot;
+        private boolean mIsQueued = false;
+
+        public Uploader(GLRoot glRoot) {
+            mGlRoot = glRoot;
+        }
+
+        public synchronized void clear() {
+            mTextures.clear();
+        }
+
+        public synchronized void addTexture(TiledTexture t) {
+            if (t.isReady()) return;
+            mTextures.addLast(t);
+
+            if (mIsQueued) return;
+            mIsQueued = true;
+            mGlRoot.addOnGLIdleListener(this);
+        }
+
+
+        @Override
+        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+            ArrayDeque<TiledTexture> deque = mTextures;
+            synchronized (this) {
+                if (!deque.isEmpty()) {
+                    TiledTexture t = deque.peekFirst();
+                    if (t.uploadNextTile(canvas)) {
+                        deque.removeFirst();
+                        mGlRoot.requestRender();
+                    }
+                }
+                mIsQueued = !mTextures.isEmpty();
+
+                // return true to keep this listener in the queue
+                return mIsQueued;
+            }
+        }
+    }
+
+    private static class Tile extends UploadedTexture {
+        public int offsetX;
+        public int offsetY;
+        public Bitmap bitmap;
+        public Tile nextFreeTile;
+        public int contentWidth;
+        public int contentHeight;
+
+        @Override
+        public void setSize(int width, int height) {
+            contentWidth = width;
+            contentHeight = height;
+            mWidth = width + 2 * BORDER_SIZE;
+            mHeight = height + 2 * BORDER_SIZE;
+            mTextureWidth = TILE_SIZE;
+            mTextureHeight = TILE_SIZE;
+        }
+
+        @Override
+        protected Bitmap onGetBitmap() {
+            int x = BORDER_SIZE - offsetX;
+            int y = BORDER_SIZE - offsetY;
+            int r = bitmap.getWidth() - x;
+            int b = bitmap.getHeight() - y ;
+            sCanvas.drawBitmap(bitmap, x, y, null);
+            bitmap = null;
+
+            // draw borders if need
+            if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint);
+            if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint);
+            if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint);
+            if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint);
+
+            return sUploadBitmap;
+        }
+
+        @Override
+        protected void onFreeBitmap(Bitmap bitmap) {
+            // do nothing
+        }
+    }
+
+    private static void freeTile(Tile tile) {
+        tile.invalidateContent();
+        tile.bitmap = null;
+        synchronized (sFreeTileLock) {
+            tile.nextFreeTile = sFreeTileHead;
+            sFreeTileHead = tile;
+        }
+    }
+
+    private static Tile obtainTile() {
+        synchronized (sFreeTileLock) {
+            Tile result = sFreeTileHead;
+            if (result == null) return new Tile();
+            sFreeTileHead = result.nextFreeTile;
+            result.nextFreeTile = null;
+            return result;
+        }
+    }
+
+    private boolean uploadNextTile(GLCanvas canvas) {
+        if (mUploadIndex == mTiles.length) return true;
+        Tile next = mTiles[mUploadIndex++];
+        boolean hasBeenLoad = next.isLoaded();
+        next.updateContent(canvas);
+
+        // It will take some time for a texture to be drawn for the first
+        // time. When scrolling, we need to draw several tiles on the screen
+        // at the same time. It may cause a UI jank even these textures has
+        // been uploaded.
+        if (!hasBeenLoad) next.draw(canvas, 0, 0);
+        return mUploadIndex == mTiles.length;
+    }
+
+    public TiledTexture(Bitmap bitmap) {
+        mWidth = bitmap.getWidth();
+        mHeight = bitmap.getHeight();
+        ArrayList<Tile> list = new ArrayList<Tile>();
+
+        for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) {
+            for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) {
+                Tile tile = obtainTile();
+                tile.offsetX = x;
+                tile.offsetY = y;
+                tile.bitmap = bitmap;
+                tile.setSize(
+                        Math.min(CONTENT_SIZE, mWidth - x),
+                        Math.min(CONTENT_SIZE, mHeight - y));
+                list.add(tile);
+            }
+        }
+        mTiles = list.toArray(new Tile[list.size()]);
+    }
+
+    public boolean isReady() {
+        return mUploadIndex == mTiles.length;
+    }
+
+    public void recycle() {
+        for (int i = 0, n = mTiles.length; i < n; ++i) {
+            freeTile(mTiles[i]);
+        }
+    }
+
+    public static void freeResources() {
+        sUploadBitmap = null;
+        sCanvas = null;
+        sPaint = null;
+    }
+
+    public static void prepareResources() {
+        sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888);
+        sCanvas = new Canvas(sUploadBitmap);
+        sPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+        sPaint.setColor(Color.TRANSPARENT);
+        sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+    }
+
+    // We want to draw the "source" on the "target".
+    // This method is to find the "output" rectangle which is
+    // the corresponding area of the "src".
+    //                                   (x,y)  target
+    // (x0,y0)  source                     +---------------+
+    //    +----------+                     |               |
+    //    | src      |                     | output        |
+    //    | +--+     |    linear map       | +----+        |
+    //    | +--+     |    ---------->      | |    |        |
+    //    |          | by (scaleX, scaleY) | +----+        |
+    //    +----------+                     |               |
+    //      Texture                        +---------------+
+    //                                          Canvas
+    private static void mapRect(RectF output,
+            RectF src, float x0, float y0, float x, float y, float scaleX,
+            float scaleY) {
+        output.set(x + (src.left - x0) * scaleX,
+                y + (src.top - y0) * scaleY,
+                x + (src.right - x0) * scaleX,
+                y + (src.bottom - y0) * scaleY);
+    }
+
+    // Draws a mixed color of this texture and a specified color onto the
+    // a rectangle. The used color is: from * (1 - ratio) + to * ratio.
+    public void drawMixed(GLCanvas canvas, int color, float ratio,
+            int x, int y, int width, int height) {
+        RectF src = mSrcRect;
+        RectF dest = mDestRect;
+        float scaleX = (float) width / mWidth ;
+        float scaleY = (float) height / mHeight;
+        for (int i = 0, n = mTiles.length; i < n; ++i) {
+            Tile t = mTiles[i];
+            src.set(0, 0, t.contentWidth, t.contentHeight);
+            src.offset(t.offsetX, t.offsetY);
+            mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+            src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+            canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect);
+        }
+    }
+
+    // Draws the texture on to the specified rectangle.
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        RectF src = mSrcRect;
+        RectF dest = mDestRect;
+        float scaleX = (float) width / mWidth ;
+        float scaleY = (float) height / mHeight;
+        for (int i = 0, n = mTiles.length; i < n; ++i) {
+            Tile t = mTiles[i];
+            src.set(0, 0, t.contentWidth, t.contentHeight);
+            src.offset(t.offsetX, t.offsetY);
+            mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+            src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+            canvas.drawTexture(t, mSrcRect, mDestRect);
+        }
+    }
+
+    // Draws a sub region of this texture on to the specified rectangle.
+    public void draw(GLCanvas canvas, RectF source, RectF target) {
+        RectF src = mSrcRect;
+        RectF dest = mDestRect;
+        float x0 = source.left;
+        float y0 = source.top;
+        float x = target.left;
+        float y = target.top;
+        float scaleX = target.width() / source.width();
+        float scaleY = target.height() / source.height();
+
+        for (int i = 0, n = mTiles.length; i < n; ++i) {
+            Tile t = mTiles[i];
+            src.set(0, 0, t.contentWidth, t.contentHeight);
+            src.offset(t.offsetX, t.offsetY);
+            if (!src.intersect(source)) continue;
+            mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);
+            src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+            canvas.drawTexture(t, src, dest);
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
index 2f2d753..5a08b85 100644
--- a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
+++ b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
@@ -83,4 +83,6 @@
     public void dumpStatisticsAndClear() {}
     public void beginRenderTarget(RawTexture texture) {}
     public void endRenderTarget() {}
+    public void drawMixed(BasicTexture from, int toColor,
+            float ratio, RectF src, RectF target) {}
 }