OSDN Git Service

Show gray tile for screennails not loaded yet.
authorChih-Chung Chang <chihchung@google.com>
Fri, 11 May 2012 09:55:02 +0000 (17:55 +0800)
committerChih-Chung Chang <chihchung@google.com>
Thu, 17 May 2012 22:34:28 +0000 (15:34 -0700)
Bug: 6452217
Change-Id: Ied9c2e2c91f4ffe218a73ba1a123df92a2aab98a

src/com/android/gallery3d/app/PhotoDataAdapter.java
src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
src/com/android/gallery3d/data/LocalImage.java
src/com/android/gallery3d/data/LocalMediaItem.java
src/com/android/gallery3d/data/LocalVideo.java
src/com/android/gallery3d/ui/BitmapScreenNail.java
src/com/android/gallery3d/ui/BitmapTileProvider.java
src/com/android/gallery3d/ui/PhotoView.java
src/com/android/gallery3d/ui/PositionController.java
src/com/android/gallery3d/ui/TileImageView.java
src/com/android/gallery3d/ui/TileImageViewAdapter.java

index 66b423f..54c7115 100644 (file)
@@ -25,6 +25,7 @@ import com.android.gallery3d.common.BitmapUtils;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.ContentListener;
 import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.LocalMediaItem;
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.data.MediaSet;
@@ -36,6 +37,7 @@ import com.android.gallery3d.ui.SynchronizedHandler;
 import com.android.gallery3d.ui.TileImageViewAdapter;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
 import com.android.gallery3d.util.ThreadPool;
 import com.android.gallery3d.util.ThreadPool.Job;
 import com.android.gallery3d.util.ThreadPool.JobContext;
@@ -113,14 +115,13 @@ public class PhotoDataAdapter implements PhotoPage.Model {
     private int mContentStart = 0;
     private int mContentEnd = 0;
 
-    /*
-     * The ImageCache is a version-to-ImageEntry map. It only holds
-     * the ImageEntries in the range of [mActiveStart, mActiveEnd).
-     * We also keep mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE.
-     * Besides, the [mActiveStart, mActiveEnd) range must be contained
-     * within the[mContentStart, mContentEnd) range.
-     */
-    private HashMap<Long, ImageEntry> mImageCache = new HashMap<Long, ImageEntry>();
+    // The ImageCache is a Path-to-ImageEntry map. It only holds the
+    // ImageEntries in the range of [mActiveStart, mActiveEnd).  We also keep
+    // mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE.  Besides, the
+    // [mActiveStart, mActiveEnd) range must be contained within
+    // the [mContentStart, mContentEnd) range.
+    private HashMap<Path, ImageEntry> mImageCache =
+            new HashMap<Path, ImageEntry>();
     private int mActiveStart = 0;
     private int mActiveEnd = 0;
 
@@ -187,11 +188,15 @@ public class PhotoDataAdapter implements PhotoPage.Model {
                         ((Runnable) message.obj).run();
                         return;
                     case MSG_LOAD_START: {
-                        if (mDataListener != null) mDataListener.onLoadingStarted();
+                        if (mDataListener != null) {
+                            mDataListener.onLoadingStarted();
+                        }
                         return;
                     }
                     case MSG_LOAD_FINISH: {
-                        if (mDataListener != null) mDataListener.onLoadingFinished();
+                        if (mDataListener != null) {
+                            mDataListener.onLoadingFinished();
+                        }
                         return;
                     }
                     case MSG_UPDATE_IMAGE_REQUESTS: {
@@ -280,8 +285,8 @@ public class PhotoDataAdapter implements PhotoPage.Model {
         mDataListener = listener;
     }
 
-    private void updateScreenNail(long version, Future<ScreenNail> future) {
-        ImageEntry entry = mImageCache.get(version);
+    private void updateScreenNail(Path path, Future<ScreenNail> future) {
+        ImageEntry entry = mImageCache.get(path);
         ScreenNail screenNail = future.get();
 
         if (entry == null || entry.screenNailTask != future) {
@@ -290,15 +295,22 @@ public class PhotoDataAdapter implements PhotoPage.Model {
         }
 
         entry.screenNailTask = null;
-        Utils.assertTrue(entry.screenNail == null);
-        entry.screenNail = screenNail;
+
+        // Combine the ScreenNails if we already have a BitmapScreenNail
+        if (entry.screenNail instanceof BitmapScreenNail) {
+            BitmapScreenNail original = (BitmapScreenNail) entry.screenNail;
+            screenNail = original.combine(screenNail);
+        }
 
         if (screenNail == null) {
             entry.failToLoad = true;
+        } else {
+            entry.failToLoad = false;
+            entry.screenNail = screenNail;
         }
 
         for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
-            if (version == getVersion(mCurrentIndex + i)) {
+            if (path == getPath(mCurrentIndex + i)) {
                 if (i == 0) updateTileProvider(entry);
                 mPhotoView.notifyImageChange(i);
                 break;
@@ -307,8 +319,8 @@ public class PhotoDataAdapter implements PhotoPage.Model {
         updateImageRequests();
     }
 
-    private void updateFullImage(long version, Future<BitmapRegionDecoder> future) {
-        ImageEntry entry = mImageCache.get(version);
+    private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) {
+        ImageEntry entry = mImageCache.get(path);
         if (entry == null || entry.fullImageTask != future) {
             BitmapRegionDecoder fullImage = future.get();
             if (fullImage != null) fullImage.recycle();
@@ -318,7 +330,7 @@ public class PhotoDataAdapter implements PhotoPage.Model {
         entry.fullImageTask = null;
         entry.fullImage = future.get();
         if (entry.fullImage != null) {
-            if (version == getVersion(mCurrentIndex)) {
+            if (path == getPath(mCurrentIndex)) {
                 updateTileProvider(entry);
                 mPhotoView.notifyImageChange(0);
             }
@@ -355,14 +367,6 @@ public class PhotoDataAdapter implements PhotoPage.Model {
         mTileProvider.clear();
     }
 
-    private ScreenNail getImage(int index) {
-        if (index < 0 || index >= mSize || !mIsActive) return null;
-        Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
-
-        ImageEntry entry = mImageCache.get(getVersion(index));
-        return entry == null ? null : entry.screenNail;
-    }
-
     private MediaItem getItem(int index) {
         if (index < 0 || index >= mSize || !mIsActive) return null;
         Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
@@ -399,7 +403,23 @@ public class PhotoDataAdapter implements PhotoPage.Model {
 
     @Override
     public ScreenNail getScreenNail(int offset) {
-        return getImage(mCurrentIndex + offset);
+        int index = mCurrentIndex + offset;
+        if (index < 0 || index >= mSize || !mIsActive) return null;
+        Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+        MediaItem item = getItem(index);
+        if (item == null) return null;
+
+        ImageEntry entry = mImageCache.get(item.getPath());
+        if (entry == null) return null;
+
+        // Create a default ScreenNail if the real one is not available yet.
+        if (entry.screenNail == null) {
+            entry.screenNail = newDefaultScreenNail(item);
+            if (offset == 0) updateTileProvider(entry);
+        }
+
+        return entry.screenNail;
     }
 
     @Override
@@ -444,6 +464,15 @@ public class PhotoDataAdapter implements PhotoPage.Model {
                 : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
     }
 
+    @Override
+    public int getLoadingState(int offset) {
+        ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset));
+        if (entry == null) return LOADING_INIT;
+        if (entry.failToLoad) return LOADING_FAIL;
+        if (entry.screenNail != null) return LOADING_COMPLETE;
+        return LOADING_INIT;
+    }
+
     public ScreenNail getScreenNail() {
         return mTileProvider.getScreenNail();
     }
@@ -465,10 +494,6 @@ public class PhotoDataAdapter implements PhotoPage.Model {
         return mTileProvider.getTile(level, x, y, tileSize, borderSize);
     }
 
-    public boolean isFailedToLoad() {
-        return mTileProvider.isFailedToLoad();
-    }
-
     public boolean isEmpty() {
         return mSize == 0;
     }
@@ -501,7 +526,7 @@ public class PhotoDataAdapter implements PhotoPage.Model {
     }
 
     private void updateTileProvider() {
-        ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
+        ImageEntry entry = mImageCache.get(getPath(mCurrentIndex));
         if (entry == null) { // in loading
             mTileProvider.clear();
         } else {
@@ -524,7 +549,6 @@ public class PhotoDataAdapter implements PhotoPage.Model {
             }
         } else {
             mTileProvider.clear();
-            if (entry.failToLoad) mTileProvider.setFailedToLoad();
         }
     }
 
@@ -581,17 +605,17 @@ public class PhotoDataAdapter implements PhotoPage.Model {
             if (entry.screenNailTask != null && entry.screenNailTask != task) {
                 entry.screenNailTask.cancel();
                 entry.screenNailTask = null;
-                entry.requestedBits &= ~BIT_SCREEN_NAIL;
+                entry.requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
             }
             if (entry.fullImageTask != null && entry.fullImageTask != task) {
                 entry.fullImageTask.cancel();
                 entry.fullImageTask = null;
-                entry.requestedBits &= ~BIT_FULL_IMAGE;
+                entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
             }
         }
     }
 
-    private static class ScreenNailJob implements Job<ScreenNail> {
+    private class ScreenNailJob implements Job<ScreenNail> {
         private MediaItem mItem;
 
         public ScreenNailJob(MediaItem item) {
@@ -605,6 +629,12 @@ public class PhotoDataAdapter implements PhotoPage.Model {
             ScreenNail s = mItem.getScreenNail();
             if (s != null) return s;
 
+            // If this is a temporary item, don't try to get its bitmap because
+            // it won't be available. We will get its bitmap after a data reload.
+            if (isTemporaryItem(mItem)) {
+                return newDefaultScreenNail(mItem);
+            }
+
             Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
             if (jc.isCancelled()) return null;
             if (bitmap != null) {
@@ -615,39 +645,87 @@ public class PhotoDataAdapter implements PhotoPage.Model {
         }
     }
 
+    private class FullImageJob implements Job<BitmapRegionDecoder> {
+        private MediaItem mItem;
+
+        public FullImageJob(MediaItem item) {
+            mItem = item;
+        }
+
+        @Override
+        public BitmapRegionDecoder run(JobContext jc) {
+            if (isTemporaryItem(mItem)) {
+                return null;
+            }
+            return mItem.requestLargeImage().run(jc);
+        }
+    }
+
+    // Returns true if we think this is a temporary item created by Camera. A
+    // temporary item is an image or a video whose data is still being
+    // processed, but an incomplete entry is created first in MediaProvider, so
+    // we can display them (in grey tile) even if they are not saved to disk
+    // yet. When the image or video data is actually saved, we will get
+    // notification from MediaProvider, reload data, and show the actual image
+    // or video data.
+    private boolean isTemporaryItem(MediaItem mediaItem) {
+        // Must have camera to create a temporary item.
+        if (mCameraIndex < 0) return false;
+        // Must be an item in camera roll.
+        if (!(mediaItem instanceof LocalMediaItem)) return false;
+        LocalMediaItem item = (LocalMediaItem) mediaItem;
+        if (item.getBucketId() != MediaSetUtils.CAMERA_BUCKET_ID) return false;
+        // Must have no size, but must have width and height information
+        if (item.getSize() != 0) return false;
+        if (item.getWidth() == 0) return false;
+        if (item.getHeight() == 0) return false;
+        // Must be created in the last 10 seconds.
+        if (item.getDateInMs() - System.currentTimeMillis() > 10000) return false;
+        return true;
+    }
+
+    // Create a default ScreenNail when a ScreenNail is needed, but we don't yet
+    // have one available (because the image data is still being saved, or the
+    // Bitmap is still being loaded.
+    private ScreenNail newDefaultScreenNail(MediaItem item) {
+        int width = item.getWidth();
+        int height = item.getHeight();
+        return new BitmapScreenNail(width, height);
+    }
+
     // Returns the task if we started the task or the task is already started.
     private Future<?> startTaskIfNeeded(int index, int which) {
         if (index < mActiveStart || index >= mActiveEnd) return null;
 
-        ImageEntry entry = mImageCache.get(getVersion(index));
+        ImageEntry entry = mImageCache.get(getPath(index));
         if (entry == null) return null;
+        MediaItem item = mData[index % DATA_CACHE_SIZE];
+        Utils.assertTrue(item != null);
+        long version = item.getDataVersion();
 
-        if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) {
+        if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null
+                && entry.requestedScreenNail == version) {
             return entry.screenNailTask;
-        } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) {
+        } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null
+                && entry.requestedFullImage == version) {
             return entry.fullImageTask;
         }
 
-        MediaItem item = mData[index % DATA_CACHE_SIZE];
-        Utils.assertTrue(item != null);
-
-        if (which == BIT_SCREEN_NAIL
-                && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) {
-            entry.requestedBits |= BIT_SCREEN_NAIL;
+        if (which == BIT_SCREEN_NAIL && entry.requestedScreenNail != version) {
+            entry.requestedScreenNail = version;
             entry.screenNailTask = mThreadPool.submit(
                     new ScreenNailJob(item),
-                    new ScreenNailListener(item.getDataVersion()));
+                    new ScreenNailListener(item));
             // request screen nail
             return entry.screenNailTask;
         }
-        if (which == BIT_FULL_IMAGE
-                && (entry.requestedBits & BIT_FULL_IMAGE) == 0
+        if (which == BIT_FULL_IMAGE && entry.requestedFullImage != version
                 && (item.getSupportedOperations()
                 & MediaItem.SUPPORT_FULL_IMAGE) != 0) {
-            entry.requestedBits |= BIT_FULL_IMAGE;
+            entry.requestedFullImage = version;
             entry.fullImageTask = mThreadPool.submit(
-                    item.requestLargeImage(),
-                    new FullImageListener(item.getDataVersion()));
+                    new FullImageJob(item),
+                    new FullImageListener(item));
             // request full image
             return entry.fullImageTask;
         }
@@ -655,15 +733,13 @@ public class PhotoDataAdapter implements PhotoPage.Model {
     }
 
     private void updateImageCache() {
-        HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet());
+        HashSet<Path> toBeRemoved = new HashSet<Path>(mImageCache.keySet());
         for (int i = mActiveStart; i < mActiveEnd; ++i) {
             MediaItem item = mData[i % DATA_CACHE_SIZE];
-            long version = item == null
-                    ? MediaObject.INVALID_DATA_VERSION
-                    : item.getDataVersion();
-            if (version == MediaObject.INVALID_DATA_VERSION) continue;
-            ImageEntry entry = mImageCache.get(version);
-            toBeRemoved.remove(version);
+            if (item == null) continue;
+            Path path = item.getPath();
+            ImageEntry entry = mImageCache.get(path);
+            toBeRemoved.remove(path);
             if (entry != null) {
                 if (Math.abs(i - mCurrentIndex) > 1) {
                     if (entry.fullImageTask != null) {
@@ -671,17 +747,26 @@ public class PhotoDataAdapter implements PhotoPage.Model {
                         entry.fullImageTask = null;
                     }
                     entry.fullImage = null;
-                    entry.requestedBits &= ~BIT_FULL_IMAGE;
+                    entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+                }
+                if (entry.requestedScreenNail != item.getDataVersion()) {
+                    // This ScreenNail is outdated, we want to update it if it's
+                    // still a placeholder.
+                    if (entry.screenNail instanceof BitmapScreenNail) {
+                        BitmapScreenNail s = (BitmapScreenNail) entry.screenNail;
+                        s.updatePlaceholderSize(
+                                item.getWidth(), item.getHeight());
+                    }
                 }
             } else {
                 entry = new ImageEntry();
-                mImageCache.put(version, entry);
+                mImageCache.put(path, entry);
             }
         }
 
         // Clear the data and requests for ImageEntries outside the new window.
-        for (Long version : toBeRemoved) {
-            ImageEntry entry = mImageCache.remove(version);
+        for (Path path : toBeRemoved) {
+            ImageEntry entry = mImageCache.remove(path);
             if (entry.fullImageTask != null) entry.fullImageTask.cancel();
             if (entry.screenNailTask != null) entry.screenNailTask.cancel();
             if (entry.screenNail != null) entry.screenNail.recycle();
@@ -690,11 +775,11 @@ public class PhotoDataAdapter implements PhotoPage.Model {
 
     private class FullImageListener
             implements Runnable, FutureListener<BitmapRegionDecoder> {
-        private final long mVersion;
+        private final Path mPath;
         private Future<BitmapRegionDecoder> mFuture;
 
-        public FullImageListener(long version) {
-            mVersion = version;
+        public FullImageListener(MediaItem item) {
+            mPath = item.getPath();
         }
 
         @Override
@@ -706,17 +791,17 @@ public class PhotoDataAdapter implements PhotoPage.Model {
 
         @Override
         public void run() {
-            updateFullImage(mVersion, mFuture);
+            updateFullImage(mPath, mFuture);
         }
     }
 
     private class ScreenNailListener
             implements Runnable, FutureListener<ScreenNail> {
-        private final long mVersion;
+        private final Path mPath;
         private Future<ScreenNail> mFuture;
 
-        public ScreenNailListener(long version) {
-            mVersion = version;
+        public ScreenNailListener(MediaItem item) {
+            mPath = item.getPath();
         }
 
         @Override
@@ -728,16 +813,17 @@ public class PhotoDataAdapter implements PhotoPage.Model {
 
         @Override
         public void run() {
-            updateScreenNail(mVersion, mFuture);
+            updateScreenNail(mPath, mFuture);
         }
     }
 
     private static class ImageEntry {
-        public int requestedBits = 0;
         public BitmapRegionDecoder fullImage;
         public ScreenNail screenNail;
         public Future<ScreenNail> screenNailTask;
         public Future<BitmapRegionDecoder> fullImageTask;
+        public long requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+        public long requestedFullImage = MediaObject.INVALID_DATA_VERSION;
         public boolean failToLoad = false;
     }
 
index f26f405..111333e 100644 (file)
@@ -49,6 +49,7 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter
 
     private PhotoView mPhotoView;
     private ThreadPool mThreadPool;
+    private int mLoadingState = LOADING_INIT;
 
     public SinglePhotoDataAdapter(
             GalleryActivity activity, PhotoView view, MediaItem item) {
@@ -123,7 +124,12 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter
     private void onDecodeThumbComplete(Future<Bitmap> future) {
         try {
             Bitmap backup = future.get();
-            if (backup == null) return;
+            if (backup == null) {
+                mLoadingState = LOADING_FAIL;
+                return;
+            } else {
+                mLoadingState = LOADING_COMPLETE;
+            }
             setScreenNail(backup, backup.getWidth(), backup.getHeight());
             mPhotoView.notifyImageChange(0);
         } catch (Throwable t) {
@@ -199,15 +205,23 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter
         return mItem.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
     }
 
+    @Override
     public MediaItem getMediaItem(int offset) {
         return offset == 0 ? mItem : null;
     }
 
+    @Override
     public int getCurrentIndex() {
         return 0;
     }
 
+    @Override
     public void setCurrentPhoto(Path path, int indexHint) {
         // ignore
     }
+
+    @Override
+    public int getLoadingState(int offset) {
+        return mLoadingState;
+    }
 }
index f96aca3..aa27c6f 100644 (file)
@@ -81,8 +81,6 @@ public class LocalImage extends LocalMediaItem {
     private final GalleryApp mApplication;
 
     public int rotation;
-    public int width;
-    public int height;
 
     public LocalImage(Path path, GalleryApp application, Cursor cursor) {
         super(path, nextVersionNumber());
index 2749ebe..7a54e80 100644 (file)
@@ -44,6 +44,8 @@ public abstract class LocalMediaItem extends MediaItem {
     public long dateModifiedInSec;
     public String filePath;
     public int bucketId;
+    public int width;
+    public int height;
 
     public LocalMediaItem(Path path, long version) {
         super(path, version);
index 0ba59f5..8eb6f91 100644 (file)
@@ -33,7 +33,7 @@ import com.android.gallery3d.util.UpdateHelper;
 
 // LocalVideo represents a video in the local storage.
 public class LocalVideo extends LocalMediaItem {
-
+    private static final String TAG = "LocalVideo";
     static final Path ITEM_PATH = Path.fromString("/local/video/item");
 
     // Must preserve order between these indices and the order of the terms in
@@ -49,7 +49,8 @@ public class LocalVideo extends LocalMediaItem {
     private static final int INDEX_DATA = 8;
     private static final int INDEX_DURATION = 9;
     private static final int INDEX_BUCKET_ID = 10;
-    private static final int INDEX_SIZE_ID = 11;
+    private static final int INDEX_SIZE = 11;
+    private static final int INDEX_RESOLUTION = 12;
 
     static final String[] PROJECTION = new String[] {
             VideoColumns._ID,
@@ -63,7 +64,8 @@ public class LocalVideo extends LocalMediaItem {
             VideoColumns.DATA,
             VideoColumns.DURATION,
             VideoColumns.BUCKET_ID,
-            VideoColumns.SIZE
+            VideoColumns.SIZE,
+            VideoColumns.RESOLUTION,
     };
 
     private final GalleryApp mApplication;
@@ -106,7 +108,21 @@ public class LocalVideo extends LocalMediaItem {
         filePath = cursor.getString(INDEX_DATA);
         durationInSec = cursor.getInt(INDEX_DURATION) / 1000;
         bucketId = cursor.getInt(INDEX_BUCKET_ID);
-        fileSize = cursor.getLong(INDEX_SIZE_ID);
+        fileSize = cursor.getLong(INDEX_SIZE);
+        parseResolution(cursor.getString(INDEX_RESOLUTION));
+    }
+
+    private void parseResolution(String resolution) {
+        int m = resolution.indexOf('x');
+        if (m == -1) return;
+        try {
+            int w = Integer.parseInt(resolution.substring(0, m));
+            int h = Integer.parseInt(resolution.substring(m + 1));
+            width = w;
+            height = h;
+        } catch (Throwable t) {
+            Log.w(TAG, t);
+        }
     }
 
     @Override
@@ -127,7 +143,7 @@ public class LocalVideo extends LocalMediaItem {
         durationInSec = uh.update(
                 durationInSec, cursor.getInt(INDEX_DURATION) / 1000);
         bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
-        fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE_ID));
+        fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE));
         return uh.isUpdated();
     }
 
@@ -206,11 +222,11 @@ public class LocalVideo extends LocalMediaItem {
 
     @Override
     public int getWidth() {
-        return 0;
+        return width;
     }
 
     @Override
     public int getHeight() {
-        return 0;
+        return height;
     }
 }
index 7f65405..14d3f19 100644 (file)
@@ -20,17 +20,32 @@ import android.graphics.Bitmap;
 import android.graphics.RectF;
 import android.util.Log;
 
+import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.MediaItem;
 
-// This is a ScreenNail wraps a Bitmap. It also includes the rotation
-// information. The getWidth() and getHeight() methods return the width/height
-// before rotation.
+// This is a ScreenNail wraps a Bitmap. There are some extra functions:
+//
+// - If we need to draw before the bitmap is available, we draw a rectange of
+// placeholder color (gray).
+//
+// - When the the bitmap is available, and we have drawn the placeholder color
+// before, we will do a fade-in animation.
 public class BitmapScreenNail implements ScreenNail {
     private static final String TAG = "BitmapScreenNail";
-    private final int mWidth;
-    private final int mHeight;
+    private static final int PLACEHOLDER_COLOR = 0xFF222222;
+    // The duration of the fading animation in milliseconds
+    private static final int DURATION = 180;
+    // These are special values for mAnimationStartTime
+    private static final long ANIMATION_NOT_NEEDED = -1;
+    private static final long ANIMATION_NEEDED = -2;
+    private static final long ANIMATION_DONE = -3;
+
+    private int mWidth;
+    private int mHeight;
     private Bitmap mBitmap;
     private BitmapTexture mTexture;
+    private FadeInTexture mFadeInTexture;
+    private long mAnimationStartTime = ANIMATION_NOT_NEEDED;
 
     public BitmapScreenNail(Bitmap bitmap) {
         mWidth = bitmap.getWidth();
@@ -40,6 +55,56 @@ public class BitmapScreenNail implements ScreenNail {
         // actually need it.
     }
 
+    public BitmapScreenNail(int width, int height) {
+        if (width == 0 || height == 0) {
+            width = 640;
+            height = 480;
+        }
+        mWidth = width;
+        mHeight = height;
+    }
+
+    // Combines the two ScreenNails.
+    // Returns the used one and recycle the unused one.
+    public ScreenNail combine(ScreenNail other) {
+        if (other == null) {
+            return this;
+        }
+
+        if (!(other instanceof BitmapScreenNail)) {
+            recycle();
+            return other;
+        }
+
+        // Now both are BitmapScreenNail. Move over the information about width,
+        // height, and Bitmap, then recycle the other.
+        BitmapScreenNail newer = (BitmapScreenNail) other;
+        mWidth = newer.mWidth;
+        mHeight = newer.mHeight;
+        if (newer.mBitmap != null) {
+            if (mBitmap != null) {
+                MediaItem.getThumbPool().recycle(mBitmap);
+            }
+            mBitmap = newer.mBitmap;
+            newer.mBitmap = null;
+
+            if (mTexture != null) {
+                mTexture.recycle();
+                mTexture = null;
+            }
+        }
+
+        newer.recycle();
+        return this;
+    }
+
+    public void updatePlaceholderSize(int width, int height) {
+        if (mBitmap != null) return;
+        if (width == 0 || height == 0) return;
+        mWidth = width;
+        mHeight = height;
+    }
+
     @Override
     public int getWidth() {
         return mWidth;
@@ -68,17 +133,60 @@ public class BitmapScreenNail implements ScreenNail {
 
     @Override
     public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        if (mBitmap == null) {
+            if (mAnimationStartTime == ANIMATION_NOT_NEEDED) {
+                mAnimationStartTime = ANIMATION_NEEDED;
+            }
+            canvas.fillRect(x, y, width, height, PLACEHOLDER_COLOR);
+            return;
+        }
+
         if (mTexture == null) {
             mTexture = new BitmapTexture(mBitmap);
         }
-        mTexture.draw(canvas, x, y, width, height);
+
+        if (mAnimationStartTime == ANIMATION_NEEDED) {
+            mAnimationStartTime = now();
+        }
+
+        if (isAnimating()) {
+            canvas.drawMixed(mTexture, PLACEHOLDER_COLOR, getRatio(), x, y,
+                    width, height);
+        } else {
+            mTexture.draw(canvas, x, y, width, height);
+        }
     }
 
     @Override
     public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        if (mBitmap == null) {
+            canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(),
+                    PLACEHOLDER_COLOR);
+            return;
+        }
+
         if (mTexture == null) {
             mTexture = new BitmapTexture(mBitmap);
         }
+
         canvas.drawTexture(mTexture, source, dest);
     }
+
+    public boolean isAnimating() {
+        if (mAnimationStartTime < 0) return false;
+        if (now() - 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;
+        return Utils.clamp(1.0f - r, 0.0f, 1.0f);
+    }
 }
index be05b33..320118e 100644 (file)
@@ -99,8 +99,4 @@ public class BitmapTileProvider implements TileImageView.Model {
             mScreenNail.recycle();
         }
     }
-
-    public boolean isFailedToLoad() {
-        return false;
-    }
 }
index 46d7c93..e7ec3a9 100644 (file)
@@ -76,6 +76,12 @@ public class PhotoView extends GLView {
 
         // Returns true if the item is a Video.
         public boolean isVideo(int offset);
+
+        public static final int LOADING_INIT = 0;
+        public static final int LOADING_COMPLETE = 1;
+        public static final int LOADING_FAIL = 2;
+
+        public int getLoadingState(int offset);
     }
 
     public interface Listener {
@@ -111,18 +117,10 @@ public class PhotoView extends GLView {
     // There are four transitions we need to check if we need to
     // lock/unlock. Marked as A to D above and in the code.
 
-    private static final int MSG_SHOW_LOADING = 1;
     private static final int MSG_CANCEL_EXTRA_SCALING = 2;
     private static final int MSG_SWITCH_FOCUS = 3;
     private static final int MSG_CAPTURE_ANIMATION_DONE = 4;
 
-    private static final long DELAY_SHOW_LOADING = 250; // 250ms;
-
-    private static final int LOADING_INIT = 0;
-    private static final int LOADING_TIMEOUT = 1;
-    private static final int LOADING_COMPLETE = 2;
-    private static final int LOADING_FAIL = 3;
-
     private static final int MOVE_THRESHOLD = 256;
     private static final float SWIPE_THRESHOLD = 300f;
 
@@ -160,12 +158,8 @@ public class PhotoView extends GLView {
     private EdgeView mEdgeView;
     private Texture mVideoPlayIcon;
 
-    private ProgressSpinner mLoadingSpinner;
-
     private SynchronizedHandler mHandler;
 
-    private int mLoadingState = LOADING_COMPLETE;
-
     private Point mImageCenter = new Point();
     private boolean mCancelExtraScalingPending;
     private boolean mFilmMode = false;
@@ -195,7 +189,6 @@ public class PhotoView extends GLView {
         Context context = activity.getAndroidContext();
         mEdgeView = new EdgeView(context);
         addComponent(mEdgeView);
-        mLoadingSpinner = new ProgressSpinner(context);
         mLoadingText = StringTexture.newInstance(
                 context.getString(R.string.loading),
                 DEFAULT_TEXT_SIZE, Color.WHITE);
@@ -249,17 +242,6 @@ public class PhotoView extends GLView {
         @Override
         public void handleMessage(Message message) {
             switch (message.what) {
-                case MSG_SHOW_LOADING: {
-                    if (mLoadingState == LOADING_INIT) {
-                        // We don't need the opening animation
-                        mPositionController.setOpenAnimationRect(null);
-
-                        mLoadingSpinner.startAnimation();
-                        mLoadingState = LOADING_TIMEOUT;
-                        invalidate();
-                    }
-                    break;
-                }
                 case MSG_CANCEL_EXTRA_SCALING: {
                     mGestureRecognizer.cancelScale();
                     mPositionController.setExtraScalingRange(false);
@@ -281,28 +263,6 @@ public class PhotoView extends GLView {
         }
     };
 
-    private void updateLoadingState() {
-        // Possible transitions of mLoadingState:
-        //        INIT --> TIMEOUT, COMPLETE, FAIL
-        //     TIMEOUT --> COMPLETE, FAIL, INIT
-        //    COMPLETE --> INIT
-        //        FAIL --> INIT
-        if (mModel.getLevelCount() != 0 || mModel.getScreenNail() != null) {
-            mHandler.removeMessages(MSG_SHOW_LOADING);
-            mLoadingState = LOADING_COMPLETE;
-        } else if (mModel.isFailedToLoad()) {
-            mHandler.removeMessages(MSG_SHOW_LOADING);
-            mLoadingState = LOADING_FAIL;
-            // We don't want the opening animation after loading failure
-            mPositionController.setOpenAnimationRect(null);
-        } else if (mLoadingState != LOADING_INIT) {
-            mLoadingState = LOADING_INIT;
-            mHandler.removeMessages(MSG_SHOW_LOADING);
-            mHandler.sendEmptyMessageDelayed(
-                    MSG_SHOW_LOADING, DELAY_SHOW_LOADING);
-        }
-    }
-
     ////////////////////////////////////////////////////////////////////////////
     //  Data/Image change notifications
     ////////////////////////////////////////////////////////////////////////////
@@ -427,6 +387,7 @@ public class PhotoView extends GLView {
         private boolean mIsCamera;
         private boolean mIsPanorama;
         private boolean mIsVideo;
+        private int mLoadingState = Model.LOADING_INIT;
         private boolean mWasCameraCenter;
 
         public void FullPicture(TileImageView tileView) {
@@ -441,9 +402,9 @@ public class PhotoView extends GLView {
             mIsCamera = mModel.isCamera(0);
             mIsPanorama = mModel.isPanorama(0);
             mIsVideo = mModel.isVideo(0);
+            mLoadingState = mModel.getLoadingState(0);
             setScreenNail(mModel.getScreenNail(0));
             updateSize(false);
-            updateLoadingState();
         }
 
         @Override
@@ -466,13 +427,9 @@ public class PhotoView extends GLView {
 
         @Override
         public void draw(GLCanvas canvas, Rect r) {
-            boolean isCenter = mPositionController.isCenter();
-
-            if (mLoadingState == LOADING_COMPLETE) {
-                drawTileView(canvas, r);
-            }
-            renderMessage(canvas, r.centerX(), r.centerY());
+            drawTileView(canvas, r);
 
+            boolean isCenter = mPositionController.isCenter();
             if (mIsCamera) {
                 boolean full = !mFilmMode && isCenter
                         && mPositionController.isAtMinimalScale();
@@ -573,13 +530,18 @@ public class PhotoView extends GLView {
             setTileViewPosition(cx, cy, viewW, viewH, imageScale);
             PhotoView.super.render(canvas);
 
-            // Draw the play video icon.
-            if (mIsVideo) {
-                canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
-                int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
-                drawVideoPlayIcon(canvas, s);
+            // Draw the play video icon and the message.
+            canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
+            int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
+            if (mIsVideo) drawVideoPlayIcon(canvas, s);
+            if (mLoadingState == Model.LOADING_FAIL) {
+                drawLoadingFailMessage(canvas);
             }
 
+            // Draw a debug indicator showing which picture has focus (index ==
+            // 0).
+            //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF);
+
             canvas.restore();
         }
 
@@ -605,33 +567,6 @@ public class PhotoView extends GLView {
             }
             mTileView.setPosition(x, y, scale, mRotation);
         }
-
-        private void renderMessage(GLCanvas canvas, int x, int y) {
-            // Draw the progress spinner and the text below it
-            //
-            // (x, y) is where we put the center of the spinner.
-            // s is the size of the video play icon, and we use s to layout text
-            // because we want to keep the text at the same place when the video
-            // play icon is shown instead of the spinner.
-            int w = getWidth();
-            int h = getHeight();
-            int s = Math.min(w, h) / ICON_RATIO;
-
-            if (mLoadingState == LOADING_TIMEOUT) {
-                StringTexture m = mLoadingText;
-                ProgressSpinner p = mLoadingSpinner;
-                p.draw(canvas, x - p.getWidth() / 2, y - p.getHeight() / 2);
-                m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
-                invalidate(); // we need to keep the spinner rotating
-            } else if (mLoadingState == LOADING_FAIL) {
-                StringTexture m = mNoThumbnailText;
-                m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
-            }
-
-            // Draw a debug indicator showing which picture has focus (index ==
-            // 0).
-            // canvas.fillRect(x - 10, y - 10, 20, 20, 0x80FF00FF);
-        }
     }
 
     private class ScreenNailPicture implements Picture {
@@ -642,6 +577,7 @@ public class PhotoView extends GLView {
         private boolean mIsCamera;
         private boolean mIsPanorama;
         private boolean mIsVideo;
+        private int mLoadingState = Model.LOADING_INIT;
 
         public ScreenNailPicture(int index) {
             mIndex = index;
@@ -652,17 +588,17 @@ public class PhotoView extends GLView {
             mIsCamera = mModel.isCamera(mIndex);
             mIsPanorama = mModel.isPanorama(mIndex);
             mIsVideo = mModel.isVideo(mIndex);
+            mLoadingState = mModel.getLoadingState(mIndex);
             setScreenNail(mModel.getScreenNail(mIndex));
         }
 
         @Override
         public void draw(GLCanvas canvas, Rect r) {
             if (mScreenNail == null) {
-                // Draw a placeholder rectange if there will be a picture in
-                // this position.
+                // Draw a placeholder rectange if there should be a picture in
+                // this position (but somehow there isn't).
                 if (mIndex >= mPrevBound && mIndex <= mNextBound) {
-                    canvas.fillRect(r.left, r.top, r.width(), r.height(),
-                            PLACEHOLDER_COLOR);
+                    drawPlaceHolder(canvas, r);
                 }
                 return;
             }
@@ -703,10 +639,22 @@ public class PhotoView extends GLView {
             int drawW = getRotated(mRotation, r.width(), r.height());
             int drawH = getRotated(mRotation, r.height(), r.width());
             mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
-            if (mIsVideo) drawVideoPlayIcon(canvas, Math.min(drawW, drawH));
+            if (isScreenNailAnimating()) {
+                invalidate();
+            }
+            int s = Math.min(drawW, drawH);
+            if (mIsVideo) drawVideoPlayIcon(canvas, s);
+            if (mLoadingState == Model.LOADING_FAIL) {
+                drawLoadingFailMessage(canvas);
+            }
             canvas.restore();
         }
 
+        private boolean isScreenNailAnimating() {
+            return (mScreenNail instanceof BitmapScreenNail)
+                    && ((BitmapScreenNail) mScreenNail).isAnimating();
+        }
+
         @Override
         public void setScreenNail(ScreenNail s) {
             if (mScreenNail == s) return;
@@ -750,6 +698,11 @@ public class PhotoView extends GLView {
         }
     }
 
+    // Draw a gray placeholder in the specified rectangle.
+    private void drawPlaceHolder(GLCanvas canvas, Rect r) {
+        canvas.fillRect(r.left, r.top, r.width(), r.height(), PLACEHOLDER_COLOR);
+    }
+
     // Draw the video play icon (in the place where the spinner was)
     private void drawVideoPlayIcon(GLCanvas canvas, int side) {
         int s = side / ICON_RATIO;
@@ -757,6 +710,12 @@ public class PhotoView extends GLView {
         mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s);
     }
 
+    // Draw the "no thumbnail" message
+    private void drawLoadingFailMessage(GLCanvas canvas) {
+        StringTexture m = mNoThumbnailText;
+        m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2);
+    }
+
     private static int getRotated(int degree, int original, int theother) {
         return (degree % 180 == 0) ? original : theother;
     }
@@ -1239,7 +1198,7 @@ public class PhotoView extends GLView {
         }
         mHolding |= HOLD_CAPTURE_ANIMATION;
         Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0);
-        mHandler.sendMessageDelayed(m, 800);
+        mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME);
         return true;
     }
 
index 9797ce9..226826d 100644 (file)
@@ -34,6 +34,8 @@ class PositionController {
     public static final int IMAGE_AT_TOP_EDGE = 4;
     public static final int IMAGE_AT_BOTTOM_EDGE = 8;
 
+    public static final int CAPTURE_ANIMATION_TIME = 600;
+
     // Special values for animation time.
     private static final long NO_ANIMATION = -1;
     private static final long LAST_ANIMATION = -2;
@@ -56,7 +58,7 @@ class PositionController {
         300,  // ANIM_KIND_ZOOM
         400,  // ANIM_KIND_OPENING
         0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
-        800,  // ANIM_KIND_CAPTURE
+        CAPTURE_ANIMATION_TIME,  // ANIM_KIND_CAPTURE
     };
 
     // We try to scale up the image to fill the screen. But in order not to
index 7ee203d..fb0e333 100644 (file)
@@ -139,7 +139,6 @@ public class TileImageView extends GLView {
         // The method would be called in another thread.
         public Bitmap getTile(int level, int x, int y, int tileSize,
                 int borderSize);
-        public boolean isFailedToLoad();
     }
 
     public TileImageView(GalleryContext context) {
@@ -407,7 +406,7 @@ public class TileImageView extends GLView {
             }
         }
         try {
-            if (level != mLevelCount) {
+            if (level != mLevelCount && !isScreenNailAnimating()) {
                 if (mScreenNail != null) {
                     mScreenNail.noDraw();
                 }
@@ -427,6 +426,9 @@ public class TileImageView extends GLView {
                 mScreenNail.draw(canvas, mOffsetX, mOffsetY,
                         Math.round(mImageWidth * mScale),
                         Math.round(mImageHeight * mScale));
+                if (isScreenNailAnimating()) {
+                    invalidate();
+                }
             }
         } finally {
             if (flags != 0) canvas.restore();
@@ -439,6 +441,11 @@ public class TileImageView extends GLView {
         }
     }
 
+    private boolean isScreenNailAnimating() {
+        return (mScreenNail instanceof BitmapScreenNail)
+                && ((BitmapScreenNail) mScreenNail).isAnimating();
+    }
+
     private void uploadBackgroundTiles(GLCanvas canvas) {
         mBackgroundTileUploaded = true;
         int n = mActiveTiles.size();
index 0400de6..5c92812 100644 (file)
@@ -33,7 +33,6 @@ public class TileImageViewAdapter implements TileImageView.Model {
     protected int mImageWidth;
     protected int mImageHeight;
     protected int mLevelCount;
-    protected boolean mFailedToLoad;
 
     public TileImageViewAdapter() {
     }
@@ -54,7 +53,6 @@ public class TileImageViewAdapter implements TileImageView.Model {
         mImageHeight = 0;
         mLevelCount = 0;
         mRegionDecoder = null;
-        mFailedToLoad = false;
     }
 
     public synchronized void setScreenNail(Bitmap bitmap, int width, int height) {
@@ -64,7 +62,6 @@ public class TileImageViewAdapter implements TileImageView.Model {
         mImageHeight = height;
         mRegionDecoder = null;
         mLevelCount = 0;
-        mFailedToLoad = false;
     }
 
     public synchronized void setScreenNail(
@@ -75,7 +72,6 @@ public class TileImageViewAdapter implements TileImageView.Model {
         mImageHeight = height;
         mRegionDecoder = null;
         mLevelCount = 0;
-        mFailedToLoad = false;
     }
 
     private void updateScreenNail(ScreenNail screenNail, boolean own) {
@@ -91,7 +87,6 @@ public class TileImageViewAdapter implements TileImageView.Model {
         mImageWidth = decoder.getWidth();
         mImageHeight = decoder.getHeight();
         mLevelCount = calculateLevelCount();
-        mFailedToLoad = false;
     }
 
     private int calculateLevelCount() {
@@ -184,13 +179,4 @@ public class TileImageViewAdapter implements TileImageView.Model {
     public int getLevelCount() {
         return mLevelCount;
     }
-
-    public void setFailedToLoad() {
-        mFailedToLoad = true;
-    }
-
-    @Override
-    public boolean isFailedToLoad() {
-        return mFailedToLoad;
-    }
 }