blob: 056bb5a51d737d11454e7f75673a895d8878ef16 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.ui;
import android.graphics.Bitmap;
import android.os.Message;
import android.util.FloatMath;
import com.android.gallery3d.app.GalleryActivity;
import com.android.gallery3d.common.BitmapUtils;
import com.android.gallery3d.common.LruCache;
import com.android.gallery3d.common.Utils;
import com.android.gallery3d.data.MediaItem;
import com.android.gallery3d.data.Path;
import com.android.gallery3d.util.Future;
import com.android.gallery3d.util.FutureListener;
import com.android.gallery3d.util.GalleryUtils;
import com.android.gallery3d.util.JobLimiter;
import com.android.gallery3d.util.ThreadPool.Job;
import com.android.gallery3d.util.ThreadPool.JobContext;
public class AlbumSlidingWindow implements AlbumView.ModelListener {
@SuppressWarnings("unused")
private static final String TAG = "AlbumSlidingWindow";
private static final int MSG_LOAD_BITMAP_DONE = 0;
private static final int MSG_UPDATE_SLOT = 1;
private static final int JOB_LIMIT = 2;
private static final int PLACEHOLDER_COLOR = 0xFF222222;
public static interface Listener {
public void onSizeChanged(int size);
public void onContentChanged();
}
private final AlbumView.Model mSource;
private int mSize;
private int mContentStart = 0;
private int mContentEnd = 0;
private int mActiveStart = 0;
private int mActiveEnd = 0;
private Listener mListener;
private int mFocusIndex = -1;
private final AlbumDisplayItem mData[];
private final ColorTexture mWaitLoadingTexture;
private SelectionDrawer mSelectionDrawer;
private SynchronizedHandler mHandler;
private JobLimiter mThreadPool;
private int mActiveRequestCount = 0;
private boolean mIsActive = false;
private int mCacheThumbSize; // 0: Don't cache the thumbnails
private LruCache<Path, Bitmap> mImageCache = new LruCache<Path, Bitmap>(1000);
public AlbumSlidingWindow(GalleryActivity activity,
AlbumView.Model source, int cacheSize,
int cacheThumbSize) {
source.setModelListener(this);
mSource = source;
mData = new AlbumDisplayItem[cacheSize];
mSize = source.size();
mWaitLoadingTexture = new ColorTexture(PLACEHOLDER_COLOR);
mWaitLoadingTexture.setSize(1, 1);
mCacheThumbSize = cacheThumbSize;
mHandler = new SynchronizedHandler(activity.getGLRoot()) {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MSG_LOAD_BITMAP_DONE: {
((AlbumDisplayItem) message.obj).onLoadBitmapDone();
break;
}
case MSG_UPDATE_SLOT: {
updateSlotContent(message.arg1);
break;
}
}
}
};
mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT);
}
public void setSelectionDrawer(SelectionDrawer drawer) {
mSelectionDrawer = drawer;
}
public void setListener(Listener listener) {
mListener = listener;
}
public void setFocusIndex(int slotIndex) {
mFocusIndex = slotIndex;
}
public DisplayItem get(int slotIndex) {
if (!isActiveSlot(slotIndex)) {
Utils.fail("invalid slot: %s outsides (%s, %s)",
slotIndex, mActiveStart, mActiveEnd);
}
return mData[slotIndex % mData.length];
}
public boolean isActiveSlot(int slotIndex) {
return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
}
private void setContentWindow(int contentStart, int contentEnd) {
if (contentStart == mContentStart && contentEnd == mContentEnd) return;
if (!mIsActive) {
mContentStart = contentStart;
mContentEnd = contentEnd;
mSource.setActiveWindow(contentStart, contentEnd);
return;
}
if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
freeSlotContent(i);
}
mSource.setActiveWindow(contentStart, contentEnd);
for (int i = contentStart; i < contentEnd; ++i) {
prepareSlotContent(i);
}
} else {
for (int i = mContentStart; i < contentStart; ++i) {
freeSlotContent(i);
}
for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
freeSlotContent(i);
}
mSource.setActiveWindow(contentStart, contentEnd);
for (int i = contentStart, n = mContentStart; i < n; ++i) {
prepareSlotContent(i);
}
for (int i = mContentEnd; i < contentEnd; ++i) {
prepareSlotContent(i);
}
}
mContentStart = contentStart;
mContentEnd = contentEnd;
}
public void setActiveWindow(int start, int end) {
if (!(start <= end && end - start <= mData.length && end <= mSize)) {
Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize);
}
DisplayItem data[] = mData;
mActiveStart = start;
mActiveEnd = end;
int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
0, Math.max(0, mSize - data.length));
int contentEnd = Math.min(contentStart + data.length, mSize);
setContentWindow(contentStart, contentEnd);
if (mIsActive) updateAllImageRequests();
}
// We would like to request non active slots in the following order:
// Order: 8 6 4 2 1 3 5 7
// |---------|---------------|---------|
// |<- active ->|
// |<-------- cached range ----------->|
private void requestNonactiveImages() {
int range = Math.max(
(mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
for (int i = 0 ;i < range; ++i) {
requestSlotImage(mActiveEnd + i, false);
requestSlotImage(mActiveStart - 1 - i, false);
}
}
private void requestSlotImage(int slotIndex, boolean isActive) {
if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
AlbumDisplayItem item = mData[slotIndex % mData.length];
item.requestImage();
}
private void cancelNonactiveImages() {
int range = Math.max(
(mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
for (int i = 0 ;i < range; ++i) {
cancelSlotImage(mActiveEnd + i, false);
cancelSlotImage(mActiveStart - 1 - i, false);
}
}
private void cancelSlotImage(int slotIndex, boolean isActive) {
if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
AlbumDisplayItem item = mData[slotIndex % mData.length];
item.cancelImageRequest();
}
private void freeSlotContent(int slotIndex) {
AlbumDisplayItem data[] = mData;
int index = slotIndex % data.length;
AlbumDisplayItem original = data[index];
if (original != null) {
original.recycle();
data[index] = null;
}
}
private void prepareSlotContent(final int slotIndex) {
mData[slotIndex % mData.length] = new AlbumDisplayItem(
slotIndex, mSource.get(slotIndex));
}
private void updateSlotContent(final int slotIndex) {
MediaItem item = mSource.get(slotIndex);
AlbumDisplayItem data[] = mData;
int index = slotIndex % data.length;
AlbumDisplayItem original = data[index];
AlbumDisplayItem update = new AlbumDisplayItem(slotIndex, item);
data[index] = update;
boolean isActive = isActiveSlot(slotIndex);
if (mListener != null && isActive) {
mListener.onContentChanged();
}
if (original != null) {
if (isActive && original.isRequestInProgress()) {
--mActiveRequestCount;
}
original.recycle();
}
if (isActive) {
if (mActiveRequestCount == 0) cancelNonactiveImages();
++mActiveRequestCount;
update.requestImage();
} else {
if (mActiveRequestCount == 0) update.requestImage();
}
}
private void updateAllImageRequests() {
mActiveRequestCount = 0;
AlbumDisplayItem data[] = mData;
for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
AlbumDisplayItem item = data[i % data.length];
item.requestImage();
if (item.isRequestInProgress()) ++mActiveRequestCount;
}
if (mActiveRequestCount == 0) {
requestNonactiveImages();
} else {
cancelNonactiveImages();
}
}
private class AlbumDisplayItem extends AbstractDisplayItem
implements FutureListener<Bitmap>, Job<Bitmap> {
private Future<Bitmap> mFuture;
private final int mSlotIndex;
private final int mMediaType;
private Texture mContent;
private boolean mIsPanorama;
private boolean mWaitLoadingDisplayed;
public AlbumDisplayItem(int slotIndex, MediaItem item) {
super(item);
mMediaType = (item == null)
? MediaItem.MEDIA_TYPE_UNKNOWN
: item.getMediaType();
mSlotIndex = slotIndex;
mIsPanorama = GalleryUtils.isPanorama(item);
updateContent(mWaitLoadingTexture);
}
@Override
protected void recycleBitmap(Bitmap bitmap) {
// if mCacheThumbSize > 0, we will keep images in cache so that
// we cannot recycle the bitmap
if (mCacheThumbSize == 0) {
BitmapPool.recycle(BitmapPool.TYPE_MICRO_THUMB, bitmap);
}
}
@Override
protected void onBitmapAvailable(Bitmap bitmap) {
boolean isActiveSlot = isActiveSlot(mSlotIndex);
if (isActiveSlot) {
--mActiveRequestCount;
if (mActiveRequestCount == 0) requestNonactiveImages();
}
if (bitmap != null) {
BitmapTexture texture = new BitmapTexture(bitmap, true);
texture.setThrottled(true);
if (mWaitLoadingDisplayed) {
updateContent(new FadeInTexture(PLACEHOLDER_COLOR, texture));
} else {
updateContent(texture);
}
if (mListener != null && isActiveSlot) {
mListener.onContentChanged();
}
}
}
private void updateContent(Texture content) {
mContent = content;
}
@Override
public int render(GLCanvas canvas, int pass) {
// Fit the content into the box
int width = mContent.getWidth();
int height = mContent.getHeight();
float scalex = mBoxWidth / (float) width;
float scaley = mBoxHeight / (float) height;
float scale = Math.min(scalex, scaley);
width = (int) FloatMath.floor(width * scale);
height = (int) FloatMath.floor(height * scale);
// Now draw it
if (pass == 0) {
Path path = null;
if (mMediaItem != null) path = mMediaItem.getPath();
mSelectionDrawer.draw(canvas, mContent, width, height,
getRotation(), path, mMediaType, mIsPanorama);
if (mContent == mWaitLoadingTexture) {
mWaitLoadingDisplayed = true;
}
int result = 0;
if (mFocusIndex == mSlotIndex) {
result |= SlotView.RENDER_MORE_PASS;
}
if ((mContent instanceof FadeInTexture) &&
((FadeInTexture) mContent).isAnimating()) {
result |= SlotView.RENDER_MORE_FRAME;
}
return result;
} else if (pass == 1) {
mSelectionDrawer.drawFocus(canvas, width, height);
}
return 0;
}
@Override
public void startLoadBitmap() {
if (mCacheThumbSize > 0) {
Path path = mMediaItem.getPath();
if (mImageCache.containsKey(path)) {
Bitmap bitmap = mImageCache.get(path);
updateImage(bitmap, false);
return;
}
mFuture = mThreadPool.submit(this, this);
} else {
mFuture = mThreadPool.submit(mMediaItem.requestImage(
MediaItem.TYPE_MICROTHUMBNAIL), this);
}
}
// This gets the bitmap and scale it down.
public Bitmap run(JobContext jc) {
Job<Bitmap> job = mMediaItem.requestImage(
MediaItem.TYPE_MICROTHUMBNAIL);
Bitmap bitmap = job.run(jc);
if (bitmap != null) {
bitmap = BitmapUtils.resizeDownBySideLength(
bitmap, mCacheThumbSize, true);
}
return bitmap;
}
@Override
public void cancelLoadBitmap() {
if (mFuture != null) {
mFuture.cancel();
}
}
@Override
public void onFutureDone(Future<Bitmap> bitmap) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this));
}
private void onLoadBitmapDone() {
Future<Bitmap> future = mFuture;
mFuture = null;
Bitmap bitmap = future.get();
boolean isCancelled = future.isCancelled();
if (mCacheThumbSize > 0 && (bitmap != null || !isCancelled)) {
Path path = mMediaItem.getPath();
mImageCache.put(path, bitmap);
}
updateImage(bitmap, isCancelled);
}
@Override
public String toString() {
return String.format("AlbumDisplayItem[%s]", mSlotIndex);
}
}
public void onSizeChanged(int size) {
if (mSize != size) {
mSize = size;
if (mListener != null) mListener.onSizeChanged(mSize);
}
}
public void onWindowContentChanged(int index) {
if (index >= mContentStart && index < mContentEnd && mIsActive) {
updateSlotContent(index);
}
}
public void resume() {
mIsActive = true;
for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
prepareSlotContent(i);
}
updateAllImageRequests();
}
public void pause() {
mIsActive = false;
for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
freeSlotContent(i);
}
mImageCache.clear();
}
}