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