AlbumSlidingWindow.java revision da071d27a1435cce080b5c609d0d833555e5a175
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 290 public AlbumDisplayItem(int slotIndex, MediaItem item) { 291 super(item); 292 mMediaType = (item == null) 293 ? MediaItem.MEDIA_TYPE_UNKNOWN 294 : item.getMediaType(); 295 mSlotIndex = slotIndex; 296 mIsPanorama = GalleryUtils.isPanorama(item); 297 updateContent(mWaitLoadingTexture); 298 } 299 300 @Override 301 protected void onBitmapAvailable(Bitmap bitmap) { 302 boolean isActiveSlot = isActiveSlot(mSlotIndex); 303 if (isActiveSlot) { 304 --mActiveRequestCount; 305 if (mActiveRequestCount == 0) requestNonactiveImages(); 306 } 307 if (bitmap != null) { 308 BitmapTexture texture = new BitmapTexture(bitmap, true); 309 texture.setThrottled(true); 310 updateContent(new FadeInTexture(PLACEHOLDER_COLOR, texture)); 311 if (mListener != null && isActiveSlot) { 312 mListener.onContentInvalidated(); 313 } 314 } 315 } 316 317 private void updateContent(Texture content) { 318 mContent = content; 319 } 320 321 @Override 322 public int render(GLCanvas canvas, int pass) { 323 // Fit the content into the box 324 int width = mContent.getWidth(); 325 int height = mContent.getHeight(); 326 327 float scalex = mBoxWidth / (float) width; 328 float scaley = mBoxHeight / (float) height; 329 float scale = Math.min(scalex, scaley); 330 331 width = (int) Math.floor(width * scale); 332 height = (int) Math.floor(height * scale); 333 334 // Now draw it 335 if (pass == 0) { 336 Path path = null; 337 if (mMediaItem != null) path = mMediaItem.getPath(); 338 mSelectionDrawer.draw(canvas, mContent, width, height, 339 getRotation(), path, mMediaType, mIsPanorama); 340 int result = 0; 341 if (mFocusIndex == mSlotIndex) { 342 result |= RENDER_MORE_PASS; 343 } 344 if (mContent != mWaitLoadingTexture && 345 ((FadeInTexture) mContent).isAnimating()) { 346 result |= RENDER_MORE_FRAME; 347 } 348 return result; 349 } else if (pass == 1) { 350 mSelectionDrawer.drawFocus(canvas, width, height); 351 } 352 return 0; 353 } 354 355 @Override 356 public void startLoadBitmap() { 357 if (mCacheThumbSize > 0) { 358 Path path = mMediaItem.getPath(); 359 if (mImageCache.containsKey(path)) { 360 Bitmap bitmap = mImageCache.get(path); 361 updateImage(bitmap, false); 362 return; 363 } 364 mFuture = mThreadPool.submit(this, this); 365 } else { 366 mFuture = mThreadPool.submit(mMediaItem.requestImage( 367 MediaItem.TYPE_MICROTHUMBNAIL), this); 368 } 369 } 370 371 // This gets the bitmap and scale it down. 372 public Bitmap run(JobContext jc) { 373 Job<Bitmap> job = mMediaItem.requestImage( 374 MediaItem.TYPE_MICROTHUMBNAIL); 375 Bitmap bitmap = job.run(jc); 376 if (bitmap != null) { 377 bitmap = BitmapUtils.resizeDownBySideLength( 378 bitmap, mCacheThumbSize, true); 379 } 380 return bitmap; 381 } 382 383 @Override 384 public void cancelLoadBitmap() { 385 if (mFuture != null) { 386 mFuture.cancel(); 387 } 388 } 389 390 @Override 391 public void onFutureDone(Future<Bitmap> bitmap) { 392 mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this)); 393 } 394 395 private void onLoadBitmapDone() { 396 Future<Bitmap> future = mFuture; 397 mFuture = null; 398 Bitmap bitmap = future.get(); 399 boolean isCancelled = future.isCancelled(); 400 if (mCacheThumbSize > 0 && (bitmap != null || !isCancelled)) { 401 Path path = mMediaItem.getPath(); 402 mImageCache.put(path, bitmap); 403 } 404 updateImage(bitmap, isCancelled); 405 } 406 407 @Override 408 public String toString() { 409 return String.format("AlbumDisplayItem[%s]", mSlotIndex); 410 } 411 } 412 413 public void onSizeChanged(int size) { 414 if (mSize != size) { 415 mSize = size; 416 if (mListener != null) mListener.onSizeChanged(mSize); 417 } 418 } 419 420 public void onWindowContentChanged(int index) { 421 if (index >= mContentStart && index < mContentEnd && mIsActive) { 422 updateSlotContent(index); 423 } 424 } 425 426 public void resume() { 427 mIsActive = true; 428 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 429 prepareSlotContent(i); 430 } 431 updateAllImageRequests(); 432 } 433 434 public void pause() { 435 mIsActive = false; 436 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 437 freeSlotContent(i); 438 } 439 mImageCache.clear(); 440 } 441} 442