AlbumSlidingWindow.java revision f9a0a4306d589b4a4e20554fed512a603426bfa1
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.ThreadPool; 28import com.android.gallery3d.util.ThreadPool.Job; 29import com.android.gallery3d.util.ThreadPool.JobContext; 30 31import android.graphics.Bitmap; 32import android.graphics.Color; 33import android.os.Message; 34 35public class AlbumSlidingWindow implements AlbumView.ModelListener { 36 @SuppressWarnings("unused") 37 private static final String TAG = "AlbumSlidingWindow"; 38 39 private static final int MSG_LOAD_BITMAP_DONE = 0; 40 private static final int MSG_UPDATE_SLOT = 1; 41 private static final int MIN_THUMB_SIZE = 100; 42 43 public static interface Listener { 44 public void onSizeChanged(int size); 45 public void onContentInvalidated(); 46 public void onWindowContentChanged( 47 int slot, DisplayItem old, DisplayItem update); 48 } 49 50 private final AlbumView.Model mSource; 51 private int mSize; 52 53 private int mContentStart = 0; 54 private int mContentEnd = 0; 55 56 private int mActiveStart = 0; 57 private int mActiveEnd = 0; 58 59 private Listener mListener; 60 private int mFocusIndex = -1; 61 62 private final AlbumDisplayItem mData[]; 63 private final ColorTexture mWaitLoadingTexture; 64 private SelectionDrawer mSelectionDrawer; 65 66 private SynchronizedHandler mHandler; 67 private ThreadPool mThreadPool; 68 private int mSlotWidth, mSlotHeight; 69 70 private int mActiveRequestCount = 0; 71 private boolean mIsActive = false; 72 73 private int mDisplayItemSize; // 0: disabled 74 private LruCache<Path, Bitmap> mImageCache = new LruCache<Path, Bitmap>(1000); 75 76 public AlbumSlidingWindow(GalleryActivity activity, 77 AlbumView.Model source, int cacheSize, 78 int slotWidth, int slotHeight, int displayItemSize) { 79 source.setModelListener(this); 80 mSource = source; 81 mData = new AlbumDisplayItem[cacheSize]; 82 mSize = source.size(); 83 mSlotWidth = slotWidth; 84 mSlotHeight = slotHeight; 85 mDisplayItemSize = displayItemSize; 86 87 mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT); 88 mWaitLoadingTexture.setSize(1, 1); 89 90 mHandler = new SynchronizedHandler(activity.getGLRoot()) { 91 @Override 92 public void handleMessage(Message message) { 93 switch (message.what) { 94 case MSG_LOAD_BITMAP_DONE: { 95 ((AlbumDisplayItem) message.obj).onLoadBitmapDone(); 96 break; 97 } 98 case MSG_UPDATE_SLOT: { 99 updateSlotContent(message.arg1); 100 break; 101 } 102 } 103 } 104 }; 105 106 mThreadPool = activity.getThreadPool(); 107 } 108 109 public void setSelectionDrawer(SelectionDrawer drawer) { 110 mSelectionDrawer = drawer; 111 } 112 113 public void setListener(Listener listener) { 114 mListener = listener; 115 } 116 117 public void setFocusIndex(int slotIndex) { 118 mFocusIndex = slotIndex; 119 } 120 121 public DisplayItem get(int slotIndex) { 122 Utils.assertTrue(isActiveSlot(slotIndex), 123 "invalid slot: %s outsides (%s, %s)", 124 slotIndex, mActiveStart, mActiveEnd); 125 return mData[slotIndex % mData.length]; 126 } 127 128 public int size() { 129 return mSize; 130 } 131 132 public boolean isActiveSlot(int slotIndex) { 133 return slotIndex >= mActiveStart && slotIndex < mActiveEnd; 134 } 135 136 private void setContentWindow(int contentStart, int contentEnd) { 137 if (contentStart == mContentStart && contentEnd == mContentEnd) return; 138 139 if (!mIsActive) { 140 mContentStart = contentStart; 141 mContentEnd = contentEnd; 142 mSource.setActiveWindow(contentStart, contentEnd); 143 return; 144 } 145 146 if (contentStart >= mContentEnd || mContentStart >= contentEnd) { 147 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 148 freeSlotContent(i); 149 } 150 mSource.setActiveWindow(contentStart, contentEnd); 151 for (int i = contentStart; i < contentEnd; ++i) { 152 prepareSlotContent(i); 153 } 154 } else { 155 for (int i = mContentStart; i < contentStart; ++i) { 156 freeSlotContent(i); 157 } 158 for (int i = contentEnd, n = mContentEnd; i < n; ++i) { 159 freeSlotContent(i); 160 } 161 mSource.setActiveWindow(contentStart, contentEnd); 162 for (int i = contentStart, n = mContentStart; i < n; ++i) { 163 prepareSlotContent(i); 164 } 165 for (int i = mContentEnd; i < contentEnd; ++i) { 166 prepareSlotContent(i); 167 } 168 } 169 170 mContentStart = contentStart; 171 mContentEnd = contentEnd; 172 } 173 174 public void setActiveWindow(int start, int end) { 175 Utils.assertTrue(start <= end 176 && end - start <= mData.length && end <= mSize, 177 "%s, %s, %s, %s", start, end, mData.length, mSize); 178 DisplayItem data[] = mData; 179 180 mActiveStart = start; 181 mActiveEnd = end; 182 183 // If no data is visible, keep the cache content 184 if (start == end) return; 185 186 int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, 187 0, Math.max(0, mSize - data.length)); 188 int contentEnd = Math.min(contentStart + data.length, mSize); 189 setContentWindow(contentStart, contentEnd); 190 if (mIsActive) updateAllImageRequests(); 191 } 192 193 // We would like to request non active slots in the following order: 194 // Order: 8 6 4 2 1 3 5 7 195 // |---------|---------------|---------| 196 // |<- active ->| 197 // |<-------- cached range ----------->| 198 private void requestNonactiveImages() { 199 int range = Math.max( 200 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); 201 for (int i = 0 ;i < range; ++i) { 202 requestSlotImage(mActiveEnd + i, false); 203 requestSlotImage(mActiveStart - 1 - i, false); 204 } 205 } 206 207 private void requestSlotImage(int slotIndex, boolean isActive) { 208 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; 209 AlbumDisplayItem item = mData[slotIndex % mData.length]; 210 item.requestImage(); 211 } 212 213 private void cancelNonactiveImages() { 214 int range = Math.max( 215 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); 216 for (int i = 0 ;i < range; ++i) { 217 cancelSlotImage(mActiveEnd + i, false); 218 cancelSlotImage(mActiveStart - 1 - i, false); 219 } 220 } 221 222 private void cancelSlotImage(int slotIndex, boolean isActive) { 223 if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; 224 AlbumDisplayItem item = mData[slotIndex % mData.length]; 225 item.cancelImageRequest(); 226 } 227 228 private void freeSlotContent(int slotIndex) { 229 AlbumDisplayItem data[] = mData; 230 int index = slotIndex % data.length; 231 AlbumDisplayItem original = data[index]; 232 if (original != null) { 233 original.recycle(); 234 data[index] = null; 235 } 236 } 237 238 private void prepareSlotContent(final int slotIndex) { 239 mData[slotIndex % mData.length] = new AlbumDisplayItem( 240 slotIndex, mSource.get(slotIndex)); 241 } 242 243 private void updateSlotContent(final int slotIndex) { 244 MediaItem item = mSource.get(slotIndex); 245 AlbumDisplayItem data[] = mData; 246 int index = slotIndex % data.length; 247 AlbumDisplayItem original = data[index]; 248 AlbumDisplayItem update = new AlbumDisplayItem(slotIndex, item); 249 data[index] = update; 250 boolean isActive = isActiveSlot(slotIndex); 251 if (mListener != null && isActive) { 252 mListener.onWindowContentChanged(slotIndex, original, update); 253 } 254 if (original != null) { 255 if (isActive && original.isRequestInProgress()) { 256 --mActiveRequestCount; 257 } 258 original.recycle(); 259 } 260 if (isActive) { 261 if (mActiveRequestCount == 0) cancelNonactiveImages(); 262 ++mActiveRequestCount; 263 update.requestImage(); 264 } else { 265 if (mActiveRequestCount == 0) update.requestImage(); 266 } 267 } 268 269 private void updateAllImageRequests() { 270 mActiveRequestCount = 0; 271 AlbumDisplayItem data[] = mData; 272 for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { 273 AlbumDisplayItem item = data[i % data.length]; 274 item.requestImage(); 275 if (item.isRequestInProgress()) ++mActiveRequestCount; 276 } 277 if (mActiveRequestCount == 0) { 278 requestNonactiveImages(); 279 } else { 280 cancelNonactiveImages(); 281 } 282 } 283 284 private class AlbumDisplayItem extends AbstractDisplayItem 285 implements FutureListener<Bitmap>, Job<Bitmap> { 286 private Future<Bitmap> mFuture; 287 private final int mSlotIndex; 288 private final int mMediaType; 289 private Texture mContent; 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 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); 309 texture.setThrottled(true); 310 updateContent(texture); 311 if (mListener != null && isActiveSlot) { 312 mListener.onContentInvalidated(); 313 } 314 } 315 } 316 317 private void updateContent(Texture content) { 318 mContent = content; 319 320 int width = mContent.getWidth(); 321 int height = mContent.getHeight(); 322 323 float scalex = mDisplayItemSize / (float) width; 324 float scaley = mDisplayItemSize / (float) height; 325 float scale = Math.min(scalex, scaley); 326 327 width = (int) Math.floor(width * scale); 328 height = (int) Math.floor(height * scale); 329 330 setSize(width, height); 331 } 332 333 @Override 334 public boolean render(GLCanvas canvas, int pass) { 335 if (pass == 0) { 336 Path path = null; 337 if (mMediaItem != null) path = mMediaItem.getPath(); 338 mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight, 339 getRotation(), path, mMediaType); 340 return (mFocusIndex == mSlotIndex); 341 } else if (pass == 1) { 342 mSelectionDrawer.drawFocus(canvas, mWidth, mHeight); 343 } 344 return false; 345 } 346 347 @Override 348 public void startLoadBitmap() { 349 if (mDisplayItemSize < MIN_THUMB_SIZE) { 350 Path path = mMediaItem.getPath(); 351 if (mImageCache.containsKey(path)) { 352 Bitmap bitmap = mImageCache.get(path); 353 updateImage(bitmap, false); 354 return; 355 } 356 mFuture = mThreadPool.submit(this, this); 357 } else { 358 mFuture = mThreadPool.submit(mMediaItem.requestImage( 359 MediaItem.TYPE_MICROTHUMBNAIL), this); 360 } 361 } 362 363 // This gets the bitmap and scale it down. 364 public Bitmap run(JobContext jc) { 365 Job<Bitmap> job = mMediaItem.requestImage( 366 MediaItem.TYPE_MICROTHUMBNAIL); 367 Bitmap bitmap = job.run(jc); 368 if (bitmap != null) { 369 bitmap = BitmapUtils.resizeDownBySideLength( 370 bitmap, mDisplayItemSize, true); 371 } 372 return bitmap; 373 } 374 375 @Override 376 public void cancelLoadBitmap() { 377 if (mFuture != null) { 378 mFuture.cancel(); 379 } 380 } 381 382 @Override 383 public void onFutureDone(Future<Bitmap> bitmap) { 384 mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this)); 385 } 386 387 private void onLoadBitmapDone() { 388 Future<Bitmap> future = mFuture; 389 mFuture = null; 390 Bitmap bitmap = future.get(); 391 boolean isCancelled = future.isCancelled(); 392 if (mDisplayItemSize < MIN_THUMB_SIZE && (bitmap != null || !isCancelled)) { 393 Path path = mMediaItem.getPath(); 394 mImageCache.put(path, bitmap); 395 } 396 updateImage(bitmap, isCancelled); 397 } 398 399 @Override 400 public String toString() { 401 return String.format("AlbumDisplayItem[%s]", mSlotIndex); 402 } 403 } 404 405 public void onSizeChanged(int size) { 406 if (mSize != size) { 407 mSize = size; 408 if (mListener != null) mListener.onSizeChanged(mSize); 409 } 410 } 411 412 public void onWindowContentChanged(int index) { 413 if (index >= mContentStart && index < mContentEnd && mIsActive) { 414 updateSlotContent(index); 415 } 416 } 417 418 public void resume() { 419 mIsActive = true; 420 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 421 prepareSlotContent(i); 422 } 423 updateAllImageRequests(); 424 } 425 426 public void pause() { 427 mIsActive = false; 428 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 429 freeSlotContent(i); 430 } 431 mImageCache.clear(); 432 } 433} 434