AlbumSlidingWindow.java revision 6c1f01e21406a05dc7d3258001aa901bd8628a79
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.gallery3d.ui; 18 19import com.android.gallery3d.app.GalleryActivity; 20import com.android.gallery3d.common.BitmapUtils; 21import com.android.gallery3d.common.LruCache; 22import com.android.gallery3d.common.Utils; 23import com.android.gallery3d.data.MediaItem; 24import com.android.gallery3d.data.Path; 25import com.android.gallery3d.util.Future; 26import com.android.gallery3d.util.FutureListener; 27import com.android.gallery3d.util.GalleryUtils; 28import com.android.gallery3d.util.JobLimiter; 29import com.android.gallery3d.util.ThreadPool.Job; 30import com.android.gallery3d.util.ThreadPool.JobContext; 31 32import android.graphics.Bitmap; 33import android.graphics.Color; 34import android.os.Message; 35 36public class AlbumSlidingWindow implements AlbumView.ModelListener { 37 @SuppressWarnings("unused") 38 private static final String TAG = "AlbumSlidingWindow"; 39 40 private static final int MSG_LOAD_BITMAP_DONE = 0; 41 private static final int MSG_UPDATE_SLOT = 1; 42 private static final int JOB_LIMIT = 2; 43 private static final int PLACEHOLDER_COLOR = 0xFF222222; 44 45 public static interface Listener { 46 public void onSizeChanged(int size); 47 public void onContentInvalidated(); 48 public void onWindowContentChanged( 49 int slot, DisplayItem old, DisplayItem update); 50 } 51 52 private final AlbumView.Model mSource; 53 private int mSize; 54 55 private int mContentStart = 0; 56 private int mContentEnd = 0; 57 58 private int mActiveStart = 0; 59 private int mActiveEnd = 0; 60 61 private Listener mListener; 62 private int mFocusIndex = -1; 63 64 private final AlbumDisplayItem mData[]; 65 private final ColorTexture mWaitLoadingTexture; 66 private SelectionDrawer mSelectionDrawer; 67 68 private SynchronizedHandler mHandler; 69 private JobLimiter mThreadPool; 70 71 private int mActiveRequestCount = 0; 72 private boolean mIsActive = false; 73 74 private int mCacheThumbSize; // 0: Don't cache the thumbnails 75 private LruCache<Path, Bitmap> mImageCache = new LruCache<Path, Bitmap>(1000); 76 77 public AlbumSlidingWindow(GalleryActivity activity, 78 AlbumView.Model source, int cacheSize, 79 int cacheThumbSize) { 80 source.setModelListener(this); 81 mSource = source; 82 mData = new AlbumDisplayItem[cacheSize]; 83 mSize = source.size(); 84 85 mWaitLoadingTexture = new ColorTexture(PLACEHOLDER_COLOR); 86 mWaitLoadingTexture.setSize(1, 1); 87 88 mHandler = new SynchronizedHandler(activity.getGLRoot()) { 89 @Override 90 public void handleMessage(Message message) { 91 switch (message.what) { 92 case MSG_LOAD_BITMAP_DONE: { 93 ((AlbumDisplayItem) message.obj).onLoadBitmapDone(); 94 break; 95 } 96 case MSG_UPDATE_SLOT: { 97 updateSlotContent(message.arg1); 98 break; 99 } 100 } 101 } 102 }; 103 104 mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT); 105 } 106 107 public void setSelectionDrawer(SelectionDrawer drawer) { 108 mSelectionDrawer = drawer; 109 } 110 111 public void setListener(Listener listener) { 112 mListener = listener; 113 } 114 115 public void setFocusIndex(int slotIndex) { 116 mFocusIndex = slotIndex; 117 } 118 119 public DisplayItem get(int slotIndex) { 120 Utils.assertTrue(isActiveSlot(slotIndex), 121 "invalid slot: %s outsides (%s, %s)", 122 slotIndex, mActiveStart, mActiveEnd); 123 return mData[slotIndex % mData.length]; 124 } 125 126 public int size() { 127 return mSize; 128 } 129 130 public boolean isActiveSlot(int slotIndex) { 131 return slotIndex >= mActiveStart && slotIndex < mActiveEnd; 132 } 133 134 private void setContentWindow(int contentStart, int contentEnd) { 135 if (contentStart == mContentStart && contentEnd == mContentEnd) return; 136 137 if (!mIsActive) { 138 mContentStart = contentStart; 139 mContentEnd = contentEnd; 140 mSource.setActiveWindow(contentStart, contentEnd); 141 return; 142 } 143 144 if (contentStart >= mContentEnd || mContentStart >= contentEnd) { 145 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 146 freeSlotContent(i); 147 } 148 mSource.setActiveWindow(contentStart, contentEnd); 149 for (int i = contentStart; i < contentEnd; ++i) { 150 prepareSlotContent(i); 151 } 152 } else { 153 for (int i = mContentStart; i < contentStart; ++i) { 154 freeSlotContent(i); 155 } 156 for (int i = contentEnd, n = mContentEnd; i < n; ++i) { 157 freeSlotContent(i); 158 } 159 mSource.setActiveWindow(contentStart, contentEnd); 160 for (int i = contentStart, n = mContentStart; i < n; ++i) { 161 prepareSlotContent(i); 162 } 163 for (int i = mContentEnd; i < contentEnd; ++i) { 164 prepareSlotContent(i); 165 } 166 } 167 168 mContentStart = contentStart; 169 mContentEnd = contentEnd; 170 } 171 172 public void setActiveWindow(int start, int end) { 173 Utils.assertTrue(start <= end 174 && end - start <= mData.length && end <= mSize, 175 "%s, %s, %s, %s", start, end, mData.length, mSize); 176 DisplayItem data[] = mData; 177 178 mActiveStart = start; 179 mActiveEnd = end; 180 181 // If no data is visible, keep the cache content 182 if (start == end) return; 183 184 int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, 185 0, Math.max(0, mSize - data.length)); 186 int contentEnd = Math.min(contentStart + data.length, mSize); 187 setContentWindow(contentStart, contentEnd); 188 if (mIsActive) updateAllImageRequests(); 189 } 190 191 // We would like to request non active slots in the following order: 192 // Order: 8 6 4 2 1 3 5 7 193 // |---------|---------------|---------| 194 // |<- active ->| 195 // |<-------- cached range ----------->| 196 private void requestNonactiveImages() { 197 int range = Math.max( 198 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); 199 for (int i = 0 ;i < range; ++i) { 200 requestSlotImage(mActiveEnd + i, false); 201 requestSlotImage(mActiveStart - 1 - i, false); 202 } 203 } 204 205 private void requestSlotImage(int slotIndex, boolean isActive) { 206 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; 207 AlbumDisplayItem item = mData[slotIndex % mData.length]; 208 item.requestImage(); 209 } 210 211 private void cancelNonactiveImages() { 212 int range = Math.max( 213 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); 214 for (int i = 0 ;i < range; ++i) { 215 cancelSlotImage(mActiveEnd + i, false); 216 cancelSlotImage(mActiveStart - 1 - i, false); 217 } 218 } 219 220 private void cancelSlotImage(int slotIndex, boolean isActive) { 221 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; 222 AlbumDisplayItem item = mData[slotIndex % mData.length]; 223 item.cancelImageRequest(); 224 } 225 226 private void freeSlotContent(int slotIndex) { 227 AlbumDisplayItem data[] = mData; 228 int index = slotIndex % data.length; 229 AlbumDisplayItem original = data[index]; 230 if (original != null) { 231 original.recycle(); 232 data[index] = null; 233 } 234 } 235 236 private void prepareSlotContent(final int slotIndex) { 237 mData[slotIndex % mData.length] = new AlbumDisplayItem( 238 slotIndex, mSource.get(slotIndex)); 239 } 240 241 private void updateSlotContent(final int slotIndex) { 242 MediaItem item = mSource.get(slotIndex); 243 AlbumDisplayItem data[] = mData; 244 int index = slotIndex % data.length; 245 AlbumDisplayItem original = data[index]; 246 AlbumDisplayItem update = new AlbumDisplayItem(slotIndex, item); 247 data[index] = update; 248 boolean isActive = isActiveSlot(slotIndex); 249 if (mListener != null && isActive) { 250 mListener.onWindowContentChanged(slotIndex, original, update); 251 } 252 if (original != null) { 253 if (isActive && original.isRequestInProgress()) { 254 --mActiveRequestCount; 255 } 256 original.recycle(); 257 } 258 if (isActive) { 259 if (mActiveRequestCount == 0) cancelNonactiveImages(); 260 ++mActiveRequestCount; 261 update.requestImage(); 262 } else { 263 if (mActiveRequestCount == 0) update.requestImage(); 264 } 265 } 266 267 private void updateAllImageRequests() { 268 mActiveRequestCount = 0; 269 AlbumDisplayItem data[] = mData; 270 for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { 271 AlbumDisplayItem item = data[i % data.length]; 272 item.requestImage(); 273 if (item.isRequestInProgress()) ++mActiveRequestCount; 274 } 275 if (mActiveRequestCount == 0) { 276 requestNonactiveImages(); 277 } else { 278 cancelNonactiveImages(); 279 } 280 } 281 282 private class AlbumDisplayItem extends AbstractDisplayItem 283 implements FutureListener<Bitmap>, Job<Bitmap> { 284 private Future<Bitmap> mFuture; 285 private final int mSlotIndex; 286 private final int mMediaType; 287 private Texture mContent; 288 private boolean mIsPanorama; 289 private boolean mWaitLoadingDisplayed; 290 291 public AlbumDisplayItem(int slotIndex, MediaItem item) { 292 super(item); 293 mMediaType = (item == null) 294 ? MediaItem.MEDIA_TYPE_UNKNOWN 295 : item.getMediaType(); 296 mSlotIndex = slotIndex; 297 mIsPanorama = GalleryUtils.isPanorama(item); 298 updateContent(mWaitLoadingTexture); 299 } 300 301 @Override 302 protected void onBitmapAvailable(Bitmap bitmap) { 303 boolean isActiveSlot = isActiveSlot(mSlotIndex); 304 if (isActiveSlot) { 305 --mActiveRequestCount; 306 if (mActiveRequestCount == 0) requestNonactiveImages(); 307 } 308 if (bitmap != null) { 309 BitmapTexture texture = new BitmapTexture(bitmap, true); 310 texture.setThrottled(true); 311 if (mWaitLoadingDisplayed) { 312 updateContent(new FadeInTexture(PLACEHOLDER_COLOR, texture)); 313 } else { 314 updateContent(texture); 315 } 316 if (mListener != null && isActiveSlot) { 317 mListener.onContentInvalidated(); 318 } 319 } 320 } 321 322 private void updateContent(Texture content) { 323 mContent = content; 324 } 325 326 @Override 327 public int render(GLCanvas canvas, int pass) { 328 // Fit the content into the box 329 int width = mContent.getWidth(); 330 int height = mContent.getHeight(); 331 332 float scalex = mBoxWidth / (float) width; 333 float scaley = mBoxHeight / (float) height; 334 float scale = Math.min(scalex, scaley); 335 336 width = (int) Math.floor(width * scale); 337 height = (int) Math.floor(height * scale); 338 339 // Now draw it 340 if (pass == 0) { 341 Path path = null; 342 if (mMediaItem != null) path = mMediaItem.getPath(); 343 mSelectionDrawer.draw(canvas, mContent, width, height, 344 getRotation(), path, mMediaType, mIsPanorama); 345 if (mContent == mWaitLoadingTexture) { 346 mWaitLoadingDisplayed = true; 347 } 348 int result = 0; 349 if (mFocusIndex == mSlotIndex) { 350 result |= RENDER_MORE_PASS; 351 } 352 if ((mContent instanceof FadeInTexture) && 353 ((FadeInTexture) mContent).isAnimating()) { 354 result |= RENDER_MORE_FRAME; 355 } 356 return result; 357 } else if (pass == 1) { 358 mSelectionDrawer.drawFocus(canvas, width, height); 359 } 360 return 0; 361 } 362 363 @Override 364 public void startLoadBitmap() { 365 if (mCacheThumbSize > 0) { 366 Path path = mMediaItem.getPath(); 367 if (mImageCache.containsKey(path)) { 368 Bitmap bitmap = mImageCache.get(path); 369 updateImage(bitmap, false); 370 return; 371 } 372 mFuture = mThreadPool.submit(this, this); 373 } else { 374 mFuture = mThreadPool.submit(mMediaItem.requestImage( 375 MediaItem.TYPE_MICROTHUMBNAIL), this); 376 } 377 } 378 379 // This gets the bitmap and scale it down. 380 public Bitmap run(JobContext jc) { 381 Job<Bitmap> job = mMediaItem.requestImage( 382 MediaItem.TYPE_MICROTHUMBNAIL); 383 Bitmap bitmap = job.run(jc); 384 if (bitmap != null) { 385 bitmap = BitmapUtils.resizeDownBySideLength( 386 bitmap, mCacheThumbSize, true); 387 } 388 return bitmap; 389 } 390 391 @Override 392 public void cancelLoadBitmap() { 393 if (mFuture != null) { 394 mFuture.cancel(); 395 } 396 } 397 398 @Override 399 public void onFutureDone(Future<Bitmap> bitmap) { 400 mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this)); 401 } 402 403 private void onLoadBitmapDone() { 404 Future<Bitmap> future = mFuture; 405 mFuture = null; 406 Bitmap bitmap = future.get(); 407 boolean isCancelled = future.isCancelled(); 408 if (mCacheThumbSize > 0 && (bitmap != null || !isCancelled)) { 409 Path path = mMediaItem.getPath(); 410 mImageCache.put(path, bitmap); 411 } 412 updateImage(bitmap, isCancelled); 413 } 414 415 @Override 416 public String toString() { 417 return String.format("AlbumDisplayItem[%s]", mSlotIndex); 418 } 419 } 420 421 public void onSizeChanged(int size) { 422 if (mSize != size) { 423 mSize = size; 424 if (mListener != null) mListener.onSizeChanged(mSize); 425 } 426 } 427 428 public void onWindowContentChanged(int index) { 429 if (index >= mContentStart && index < mContentEnd && mIsActive) { 430 updateSlotContent(index); 431 } 432 } 433 434 public void resume() { 435 mIsActive = true; 436 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 437 prepareSlotContent(i); 438 } 439 updateAllImageRequests(); 440 } 441 442 public void pause() { 443 mIsActive = false; 444 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 445 freeSlotContent(i); 446 } 447 mImageCache.clear(); 448 } 449} 450