ExtendedBitmapDrawable.java revision 93a35b93dc582e38ff8ee5979754a16b4bf4da0c
1/*
2 * Copyright (C) 2013 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.bitmap.drawable;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.animation.ValueAnimator.AnimatorUpdateListener;
23import android.content.res.Resources;
24import android.graphics.Canvas;
25import android.graphics.ColorFilter;
26import android.graphics.Paint;
27import android.graphics.PixelFormat;
28import android.graphics.Rect;
29import android.graphics.drawable.Drawable;
30import android.os.Handler;
31import android.util.DisplayMetrics;
32import android.util.Log;
33import android.view.animation.LinearInterpolator;
34
35import com.android.bitmap.R;
36import com.android.bitmap.BitmapCache;
37import com.android.bitmap.DecodeAggregator;
38import com.android.bitmap.DecodeTask;
39import com.android.bitmap.DecodeTask.Request;
40import com.android.bitmap.ReusableBitmap;
41import com.android.bitmap.util.BitmapUtils;
42import com.android.bitmap.util.RectUtils;
43import com.android.bitmap.util.Trace;
44
45import java.util.concurrent.Executor;
46import java.util.concurrent.LinkedBlockingQueue;
47import java.util.concurrent.ThreadPoolExecutor;
48import java.util.concurrent.TimeUnit;
49
50/**
51 * This class encapsulates all functionality needed to display a single image bitmap,
52 * including request creation/cancelling, data unbinding and re-binding, and fancy animations
53 * to draw upon state changes.
54 * <p>
55 * The actual bitmap decode work is handled by {@link DecodeTask}.
56 * TODO: have this class extend from BasicBitmapDrawable
57 */
58public class ExtendedBitmapDrawable extends Drawable implements DecodeTask.DecodeCallback,
59        Drawable.Callback, Runnable, Parallaxable, DecodeAggregator.Callback {
60
61    private BitmapRequestKey mCurrKey;
62    private ReusableBitmap mBitmap;
63    private final BitmapCache mCache;
64    private DecodeAggregator mDecodeAggregator;
65    private DecodeTask mTask;
66    private int mDecodeWidth;
67    private int mDecodeHeight;
68    private int mLoadState = LOAD_STATE_UNINITIALIZED;
69    private float mParallaxFraction = 0.5f;
70    private float mParallaxSpeedMultiplier;
71
72    // each attachment gets its own placeholder and progress indicator, to be shown, hidden,
73    // and animated based on Drawable#setVisible() changes, which are in turn driven by
74    // #setLoadState().
75    private Placeholder mPlaceholder;
76    private Progress mProgress;
77
78    private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(4, 4,
79            1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
80
81    private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
82
83    private static final boolean LIMIT_BITMAP_DENSITY = true;
84
85    private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH;
86
87    private static final int LOAD_STATE_UNINITIALIZED = 0;
88    private static final int LOAD_STATE_NOT_YET_LOADED = 1;
89    private static final int LOAD_STATE_LOADING = 2;
90    private static final int LOAD_STATE_LOADED = 3;
91    private static final int LOAD_STATE_FAILED = 4;
92
93    private final float mDensity;
94    private int mProgressDelayMs;
95    private final Paint mPaint = new Paint();
96    private final Rect mSrcRect = new Rect();
97    private final Handler mHandler = new Handler();
98
99    public static final boolean DEBUG = false;
100    public static final String TAG = ExtendedBitmapDrawable.class.getSimpleName();
101
102    public ExtendedBitmapDrawable(final Resources res, final BitmapCache cache,
103            final DecodeAggregator decodeAggregator, final Drawable placeholder,
104            final Drawable progress) {
105        mDensity = res.getDisplayMetrics().density;
106        mCache = cache;
107        this.mDecodeAggregator = decodeAggregator;
108        mPaint.setFilterBitmap(true);
109
110        final int fadeOutDurationMs = res.getInteger(R.integer.bitmap_fade_animation_duration);
111        final int tileColor = res.getColor(R.color.bitmap_placeholder_background_color);
112        mProgressDelayMs = res.getInteger(R.integer.bitmap_progress_animation_delay);
113
114        int placeholderSize = res.getDimensionPixelSize(R.dimen.placeholder_size);
115        mPlaceholder = new Placeholder(placeholder.getConstantState().newDrawable(res), res,
116                placeholderSize, placeholderSize, fadeOutDurationMs, tileColor);
117        mPlaceholder.setCallback(this);
118
119        int progressBarSize = res.getDimensionPixelSize(R.dimen.progress_bar_size);
120        mProgress = new Progress(progress.getConstantState().newDrawable(res), res,
121                progressBarSize, progressBarSize, fadeOutDurationMs, tileColor);
122        mProgress.setCallback(this);
123    }
124
125    public DecodeTask.Request getKey() {
126        return mCurrKey;
127    }
128
129    /**
130     * Set the dimensions to which to decode into. For a parallax effect, ensure the height is
131     * larger than the destination of the bitmap.
132     * TODO: test parallax
133     */
134    public void setDecodeDimensions(int w, int h) {
135        mDecodeWidth = w;
136        mDecodeHeight = h;
137        decode();
138    }
139
140    public void setParallaxSpeedMultiplier(final float parallaxSpeedMultiplier) {
141        mParallaxSpeedMultiplier = parallaxSpeedMultiplier;
142    }
143
144    public void showStaticPlaceholder() {
145        setLoadState(LOAD_STATE_FAILED);
146    }
147
148    public void unbind() {
149        setImage(null);
150    }
151
152    public void bind(BitmapRequestKey key) {
153        setImage(key);
154    }
155
156    private void setImage(final BitmapRequestKey key) {
157        if (mCurrKey != null && mCurrKey.equals(key)) {
158            return;
159        }
160
161        Trace.beginSection("set image");
162        Trace.beginSection("release reference");
163        if (mBitmap != null) {
164            mBitmap.releaseReference();
165            mBitmap = null;
166        }
167        Trace.endSection();
168        if (mCurrKey != null && mDecodeAggregator != null) {
169            mDecodeAggregator.forget(mCurrKey);
170        }
171        mCurrKey = key;
172
173        if (mTask != null) {
174            mTask.cancel();
175            mTask = null;
176        }
177
178        mHandler.removeCallbacks(this);
179        // start from a clean slate on every bind
180        // this allows the initial transition to be specially instantaneous, so e.g. a cache hit
181        // doesn't unnecessarily trigger a fade-in
182        setLoadState(LOAD_STATE_UNINITIALIZED);
183
184        if (key == null) {
185            invalidateSelf();
186            Trace.endSection();
187            return;
188        }
189
190        // find cached entry here and skip decode if found.
191        final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */);
192        if (cached != null) {
193            setBitmap(cached);
194            if (DEBUG) {
195                Log.d(TAG, String.format("CACHE HIT key=%s", mCurrKey));
196            }
197        } else {
198            decode();
199            if (DEBUG) {
200                Log.d(TAG, String.format(
201                        "CACHE MISS key=%s\ncache=%s", mCurrKey, mCache.toDebugString()));
202            }
203        }
204        Trace.endSection();
205    }
206
207    @Override
208    public void setParallaxFraction(float fraction) {
209        mParallaxFraction = fraction;
210    }
211
212    @Override
213    public void draw(final Canvas canvas) {
214        final Rect bounds = getBounds();
215        if (bounds.isEmpty()) {
216            return;
217        }
218
219        if (mBitmap != null && mBitmap.bmp != null) {
220            BitmapUtils.calculateCroppedSrcRect(
221                    mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
222                    bounds.width(), bounds.height(),
223                    bounds.height(), Integer.MAX_VALUE,
224                    mParallaxFraction, false /* absoluteFraction */,
225                    mParallaxSpeedMultiplier, mSrcRect);
226
227            final int orientation = mBitmap.getOrientation();
228            // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
229            // been corrected. We need to decode the uncorrected source rectangle. Calculate true
230            // coordinates.
231            RectUtils.rotateRectForOrientation(orientation,
232                    new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()),
233                    mSrcRect);
234
235            // We may need to rotate the canvas, so we also have to rotate the bounds.
236            final Rect rotatedBounds = new Rect(bounds);
237            RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds);
238
239            // Rotate the canvas.
240            canvas.save();
241            canvas.rotate(orientation, bounds.centerX(), bounds.centerY());
242            canvas.drawBitmap(mBitmap.bmp, mSrcRect, rotatedBounds, mPaint);
243            canvas.restore();
244        }
245
246        // Draw the two possible overlay layers in reverse-priority order.
247        // (each layer will no-op the draw when appropriate)
248        // This ordering means cross-fade transitions are just fade-outs of each layer.
249        mProgress.draw(canvas);
250        mPlaceholder.draw(canvas);
251    }
252
253    @Override
254    public void setAlpha(int alpha) {
255        final int old = mPaint.getAlpha();
256        mPaint.setAlpha(alpha);
257        mPlaceholder.setAlpha(alpha);
258        mProgress.setAlpha(alpha);
259        if (alpha != old) {
260            invalidateSelf();
261        }
262    }
263
264    @Override
265    public void setColorFilter(ColorFilter cf) {
266        mPaint.setColorFilter(cf);
267        mPlaceholder.setColorFilter(cf);
268        mProgress.setColorFilter(cf);
269        invalidateSelf();
270    }
271
272    @Override
273    public int getOpacity() {
274        return (mBitmap != null && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ?
275                PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
276    }
277
278    @Override
279    protected void onBoundsChange(Rect bounds) {
280        super.onBoundsChange(bounds);
281
282        mPlaceholder.setBounds(bounds);
283        mProgress.setBounds(bounds);
284    }
285
286    @Override
287    public void onDecodeBegin(final Request key) {
288        if (mDecodeAggregator != null) {
289            mDecodeAggregator.expect(key, this);
290        } else {
291            onBecomeFirstExpected(key);
292        }
293    }
294
295    @Override
296    public void onBecomeFirstExpected(final Request key) {
297        if (!key.equals(mCurrKey)) {
298            return;
299        }
300        // normally, we'd transition to the LOADING state now, but we want to delay that a bit
301        // to minimize excess occurrences of the rotating spinner
302        mHandler.postDelayed(this, mProgressDelayMs);
303    }
304
305    @Override
306    public void run() {
307        if (mLoadState == LOAD_STATE_NOT_YET_LOADED) {
308            setLoadState(LOAD_STATE_LOADING);
309        }
310    }
311
312    @Override
313    public void onDecodeComplete(final Request key, final ReusableBitmap result) {
314        if (mDecodeAggregator != null) {
315            mDecodeAggregator.execute(key, new Runnable() {
316                @Override
317                public void run() {
318                    onDecodeCompleteImpl(key, result);
319                }
320
321                @Override
322                public String toString() {
323                    return "DONE";
324                }
325            });
326        } else {
327            onDecodeCompleteImpl(key, result);
328        }
329    }
330
331    private void onDecodeCompleteImpl(final Request key, final ReusableBitmap result) {
332        if (key.equals(mCurrKey)) {
333            setBitmap(result);
334        } else {
335            // if the requests don't match (i.e. this request is stale), decrement the
336            // ref count to allow the bitmap to be pooled
337            if (result != null) {
338                result.releaseReference();
339            }
340        }
341    }
342
343    @Override
344    public void onDecodeCancel(final Request key) {
345        if (mDecodeAggregator != null) {
346            mDecodeAggregator.forget(key);
347        }
348    }
349
350    private void setBitmap(ReusableBitmap bmp) {
351        if (mBitmap != null && mBitmap != bmp) {
352            mBitmap.releaseReference();
353        }
354        mBitmap = bmp;
355        setLoadState((bmp != null) ? LOAD_STATE_LOADED : LOAD_STATE_FAILED);
356        invalidateSelf();
357    }
358
359    private void decode() {
360        final int bufferW;
361        final int bufferH;
362
363        if (mCurrKey == null) {
364            return;
365        }
366
367        Trace.beginSection("decode");
368        if (LIMIT_BITMAP_DENSITY) {
369            final float scale =
370                    Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT
371                            / mDensity);
372            bufferW = (int) (mDecodeWidth * scale);
373            bufferH = (int) (mDecodeHeight * scale);
374        } else {
375            bufferW = mDecodeWidth;
376            bufferH = mDecodeHeight;
377        }
378
379        if (bufferW == 0 || bufferH == 0) {
380            Trace.endSection();
381            return;
382        }
383        if (mTask != null) {
384            mTask.cancel();
385        }
386        setLoadState(LOAD_STATE_NOT_YET_LOADED);
387        mTask = new DecodeTask(mCurrKey, bufferW, bufferH, this, mCache);
388        mTask.executeOnExecutor(EXECUTOR);
389        Trace.endSection();
390    }
391
392    private void setLoadState(int loadState) {
393        if (DEBUG) {
394            Log.v(TAG, String.format("IN setLoadState. old=%s new=%s key=%s this=%s",
395                    mLoadState, loadState, mCurrKey, this));
396        }
397        if (mLoadState == loadState) {
398            if (DEBUG) {
399                Log.v(TAG, "OUT no-op setLoadState");
400            }
401            return;
402        }
403
404        Trace.beginSection("set load state");
405        switch (loadState) {
406            // This state differs from LOADED in that the subsequent state transition away from
407            // UNINITIALIZED will not have a fancy transition. This allows list item binds to
408            // cached data to take immediate effect without unnecessary whizzery.
409            case LOAD_STATE_UNINITIALIZED:
410                mPlaceholder.reset();
411                mProgress.reset();
412                break;
413            case LOAD_STATE_NOT_YET_LOADED:
414                mPlaceholder.setPulseEnabled(true);
415                mPlaceholder.setVisible(true);
416                mProgress.setVisible(false);
417                break;
418            case LOAD_STATE_LOADING:
419                mPlaceholder.setVisible(false);
420                mProgress.setVisible(true);
421                break;
422            case LOAD_STATE_LOADED:
423                mPlaceholder.setVisible(false);
424                mProgress.setVisible(false);
425                break;
426            case LOAD_STATE_FAILED:
427                mPlaceholder.setPulseEnabled(false);
428                mPlaceholder.setVisible(true);
429                mProgress.setVisible(false);
430                break;
431        }
432        Trace.endSection();
433
434        mLoadState = loadState;
435        boolean placeholderVisible = mPlaceholder != null && mPlaceholder.isVisible();
436        boolean progressVisible = mProgress != null && mProgress.isVisible();
437
438        if (DEBUG) {
439            Log.v(TAG, String.format("OUT stateful setLoadState. new=%s placeholder=%s progress=%s",
440                    loadState, placeholderVisible, progressVisible));
441        }
442    }
443
444    @Override
445    public void invalidateDrawable(Drawable who) {
446        invalidateSelf();
447    }
448
449    @Override
450    public void scheduleDrawable(Drawable who, Runnable what, long when) {
451        scheduleSelf(what, when);
452    }
453
454    @Override
455    public void unscheduleDrawable(Drawable who, Runnable what) {
456        unscheduleSelf(what);
457    }
458
459    private static class Placeholder extends TileDrawable {
460
461        private final ValueAnimator mPulseAnimator;
462        private boolean mPulseEnabled = true;
463        private float mPulseAlphaFraction = 1f;
464
465        public Placeholder(Drawable placeholder, Resources res,
466                int placeholderWidth, int placeholderHeight, int fadeOutDurationMs,
467                int tileColor) {
468            super(placeholder, placeholderWidth, placeholderHeight, tileColor, fadeOutDurationMs);
469            mPulseAnimator = ValueAnimator.ofInt(55, 255)
470                    .setDuration(res.getInteger(R.integer.bitmap_placeholder_animation_duration));
471            mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
472            mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE);
473            mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() {
474                @Override
475                public void onAnimationUpdate(ValueAnimator animation) {
476                    mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f;
477                    setInnerAlpha(getCurrentAlpha());
478                }
479            });
480            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
481                @Override
482                public void onAnimationEnd(Animator animation) {
483                    stopPulsing();
484                }
485            });
486        }
487
488        @Override
489        public void setInnerAlpha(final int alpha) {
490            super.setInnerAlpha((int) (alpha * mPulseAlphaFraction));
491        }
492
493        public void setPulseEnabled(boolean enabled) {
494            mPulseEnabled = enabled;
495            if (!mPulseEnabled) {
496                stopPulsing();
497            }
498        }
499
500        private void stopPulsing() {
501            if (mPulseAnimator != null) {
502                mPulseAnimator.cancel();
503                mPulseAlphaFraction = 1f;
504                setInnerAlpha(getCurrentAlpha());
505            }
506        }
507
508        @Override
509        public boolean setVisible(boolean visible) {
510            final boolean changed = super.setVisible(visible);
511            if (changed) {
512                if (isVisible()) {
513                    // start
514                    if (mPulseAnimator != null && mPulseEnabled) {
515                        mPulseAnimator.start();
516                    }
517                } else {
518                    // can't cancel the pulsing yet-- wait for the fade-out animation to end
519                    // one exception: if alpha is already zero, there is no fade-out, so stop now
520                    if (getCurrentAlpha() == 0) {
521                        stopPulsing();
522                    }
523                }
524            }
525            return changed;
526        }
527
528    }
529
530    private static class Progress extends TileDrawable {
531
532        private final ValueAnimator mRotateAnimator;
533
534        public Progress(Drawable progress, Resources res,
535                int progressBarWidth, int progressBarHeight, int fadeOutDurationMs,
536                int tileColor) {
537            super(progress, progressBarWidth, progressBarHeight, tileColor, fadeOutDurationMs);
538
539            mRotateAnimator = ValueAnimator.ofInt(0, 10000)
540                    .setDuration(res.getInteger(R.integer.bitmap_progress_animation_duration));
541            mRotateAnimator.setInterpolator(new LinearInterpolator());
542            mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
543            mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() {
544                @Override
545                public void onAnimationUpdate(ValueAnimator animation) {
546                    setLevel((Integer) animation.getAnimatedValue());
547                }
548            });
549            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
550                @Override
551                public void onAnimationEnd(Animator animation) {
552                    if (mRotateAnimator != null) {
553                        mRotateAnimator.cancel();
554                    }
555                }
556            });
557        }
558
559        @Override
560        public boolean setVisible(boolean visible) {
561            final boolean changed = super.setVisible(visible);
562            if (changed) {
563                if (isVisible()) {
564                    if (mRotateAnimator != null) {
565                        mRotateAnimator.start();
566                    }
567                } else {
568                    // can't cancel the rotate yet-- wait for the fade-out animation to end
569                    // one exception: if alpha is already zero, there is no fade-out, so stop now
570                    if (getCurrentAlpha() == 0 && mRotateAnimator != null) {
571                        mRotateAnimator.cancel();
572                    }
573                }
574            }
575            return changed;
576        }
577
578    }
579}
580