diff options
author | Chih-Chung Chang <chihchung@google.com> | 2012-05-11 17:55:02 +0800 |
---|---|---|
committer | Chih-Chung Chang <chihchung@google.com> | 2012-05-17 15:34:28 -0700 |
commit | 17df03ce167eab04f51bf6f67d7f8085ac8ae10d (patch) | |
tree | 6343eeb9aa5d85b397ee9e0c3ff2d3a09d3a03a8 /src/com/android/gallery3d | |
parent | 931a10095f72a0334ac5ab5da174e646cb5bacf9 (diff) | |
download | android_packages_apps_Snap-17df03ce167eab04f51bf6f67d7f8085ac8ae10d.tar.gz android_packages_apps_Snap-17df03ce167eab04f51bf6f67d7f8085ac8ae10d.tar.bz2 android_packages_apps_Snap-17df03ce167eab04f51bf6f67d7f8085ac8ae10d.zip |
Show gray tile for screennails not loaded yet.
Bug: 6452217
Change-Id: Ied9c2e2c91f4ffe218a73ba1a123df92a2aab98a
Diffstat (limited to 'src/com/android/gallery3d')
-rw-r--r-- | src/com/android/gallery3d/app/PhotoDataAdapter.java | 228 | ||||
-rw-r--r-- | src/com/android/gallery3d/app/SinglePhotoDataAdapter.java | 16 | ||||
-rw-r--r-- | src/com/android/gallery3d/data/LocalImage.java | 2 | ||||
-rw-r--r-- | src/com/android/gallery3d/data/LocalMediaItem.java | 2 | ||||
-rw-r--r-- | src/com/android/gallery3d/data/LocalVideo.java | 30 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/BitmapScreenNail.java | 120 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/BitmapTileProvider.java | 4 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/PhotoView.java | 141 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/PositionController.java | 4 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/TileImageView.java | 11 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/TileImageViewAdapter.java | 14 |
11 files changed, 373 insertions, 199 deletions
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java index 66b423f69..54c7115c3 100644 --- a/src/com/android/gallery3d/app/PhotoDataAdapter.java +++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java @@ -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; } diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java index f26f405d4..111333e3b 100644 --- a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java +++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java @@ -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; + } } diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java index f96aca3e3..aa27c6f42 100644 --- a/src/com/android/gallery3d/data/LocalImage.java +++ b/src/com/android/gallery3d/data/LocalImage.java @@ -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()); diff --git a/src/com/android/gallery3d/data/LocalMediaItem.java b/src/com/android/gallery3d/data/LocalMediaItem.java index 2749ebe55..7a54e8062 100644 --- a/src/com/android/gallery3d/data/LocalMediaItem.java +++ b/src/com/android/gallery3d/data/LocalMediaItem.java @@ -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); diff --git a/src/com/android/gallery3d/data/LocalVideo.java b/src/com/android/gallery3d/data/LocalVideo.java index 0ba59f581..8eb6f914a 100644 --- a/src/com/android/gallery3d/data/LocalVideo.java +++ b/src/com/android/gallery3d/data/LocalVideo.java @@ -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; } } diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java index 7f654058e..14d3f1919 100644 --- a/src/com/android/gallery3d/ui/BitmapScreenNail.java +++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java @@ -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); + } } diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java index be05b33ab..320118e89 100644 --- a/src/com/android/gallery3d/ui/BitmapTileProvider.java +++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java @@ -99,8 +99,4 @@ public class BitmapTileProvider implements TileImageView.Model { mScreenNail.recycle(); } } - - public boolean isFailedToLoad() { - return false; - } } diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java index 46d7c9362..e7ec3a948 100644 --- a/src/com/android/gallery3d/ui/PhotoView.java +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -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; } diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java index 9797ce903..226826da7 100644 --- a/src/com/android/gallery3d/ui/PositionController.java +++ b/src/com/android/gallery3d/ui/PositionController.java @@ -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 diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java index 7ee203dd0..fb0e333a1 100644 --- a/src/com/android/gallery3d/ui/TileImageView.java +++ b/src/com/android/gallery3d/ui/TileImageView.java @@ -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(); diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java index 0400de6f3..5c9281202 100644 --- a/src/com/android/gallery3d/ui/TileImageViewAdapter.java +++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java @@ -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; - } } |