ExtendedBitmapDrawable.java revision 9c6ac19d4a3d39b7c2992060957920118ff56a65
114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne/*
214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * Copyright (C) 2013 The Android Open Source Project
314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne *
414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * Licensed under the Apache License, Version 2.0 (the "License");
514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * you may not use this file except in compliance with the License.
614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * You may obtain a copy of the License at
714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne *
814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne *      http://www.apache.org/licenses/LICENSE-2.0
914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne *
1014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * Unless required by applicable law or agreed to in writing, software
1114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * distributed under the License is distributed on an "AS IS" BASIS,
1214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * See the License for the specific language governing permissions and
1414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * limitations under the License.
1514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne */
1614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
1714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbournepackage com.android.bitmap.drawable;
1814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
1914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.animation.Animator;
2014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.animation.AnimatorListenerAdapter;
2114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.animation.ValueAnimator;
2214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.animation.ValueAnimator.AnimatorUpdateListener;
2314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.content.res.Resources;
2414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.graphics.Canvas;
2514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.graphics.ColorFilter;
2614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.graphics.Paint;
2714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.graphics.PixelFormat;
2814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.graphics.Rect;
2914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.graphics.drawable.Drawable;
3014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.os.Handler;
3114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.util.DisplayMetrics;
3214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.util.Log;
3314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport android.view.animation.LinearInterpolator;
3414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
3514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport com.android.bitmap.BitmapCache;
3614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport com.android.bitmap.DecodeAggregator;
3714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport com.android.bitmap.DecodeTask;
3814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport com.android.bitmap.DecodeTask.DecodeOptions;
3914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport com.android.bitmap.R;
4014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport com.android.bitmap.RequestKey;
4114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport com.android.bitmap.ReusableBitmap;
42c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindolaimport com.android.bitmap.util.BitmapUtils;
4314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport com.android.bitmap.util.RectUtils;
4414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport com.android.bitmap.util.Trace;
4514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
4614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport java.util.concurrent.Executor;
4714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport java.util.concurrent.LinkedBlockingQueue;
4814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport java.util.concurrent.ThreadPoolExecutor;
4914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourneimport java.util.concurrent.TimeUnit;
5014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
5114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne/**
5214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * This class encapsulates all functionality needed to display a single image bitmap,
5314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * including request creation/cancelling, data unbinding and re-binding, and fancy animations
5414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * to draw upon state changes.
5514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * <p>
5614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * The actual bitmap decode work is handled by {@link DecodeTask}.
5714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne * TODO: have this class extend from BasicBitmapDrawable
58c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola */
5914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbournepublic class ExtendedBitmapDrawable extends Drawable implements DecodeTask.DecodeCallback,
6014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        Drawable.Callback, Runnable, Parallaxable, DecodeAggregator.Callback {
6114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
6214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private RequestKey mCurrKey;
6314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
64c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    private ReusableBitmap mBitmap;
6514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private final BitmapCache mCache;
6614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private final boolean mLimitDensity;
67c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    private DecodeAggregator mDecodeAggregator;
68c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    private DecodeTask mTask;
69c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    private int mDecodeWidth;
70c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    private int mDecodeHeight;
7114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private int mLoadState = LOAD_STATE_UNINITIALIZED;
7214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private float mParallaxFraction = 0.5f;
7314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private float mParallaxSpeedMultiplier;
7414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
75c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    // each attachment gets its own placeholder and progress indicator, to be shown, hidden,
7614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    // and animated based on Drawable#setVisible() changes, which are in turn driven by
7714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    // #setLoadState().
78c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    private Placeholder mPlaceholder;
79c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    private Progress mProgress;
80c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola
81c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(4, 4,
8214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne            1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
8314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
8414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
85c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola
8614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH;
8714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
8814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private static final float VERTICAL_CENTER = 1f / 3;
8914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
9014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private static final int LOAD_STATE_UNINITIALIZED = 0;
9114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private static final int LOAD_STATE_NOT_YET_LOADED = 1;
9214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private static final int LOAD_STATE_LOADING = 2;
9314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private static final int LOAD_STATE_LOADED = 3;
9414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private static final int LOAD_STATE_FAILED = 4;
9514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
9614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private final float mDensity;
9714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private int mProgressDelayMs;
9814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private final Paint mPaint = new Paint();
9914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private final Rect mSrcRect = new Rect();
10014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    private final Handler mHandler = new Handler();
10114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
102c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    public static final boolean DEBUG = false;
103c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    public static final String TAG = ExtendedBitmapDrawable.class.getSimpleName();
10414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
10514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    public ExtendedBitmapDrawable(final Resources res, final BitmapCache cache,
10614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne            final boolean limitDensity, final DecodeAggregator decodeAggregator,
107c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola            final Drawable placeholder, final Drawable progress) {
10814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        mDensity = res.getDisplayMetrics().density;
10914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        mCache = cache;
11014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        mLimitDensity = limitDensity;
11114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        this.mDecodeAggregator = decodeAggregator;
11214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        mPaint.setFilterBitmap(true);
11314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
11414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        final int fadeOutDurationMs = res.getInteger(R.integer.bitmap_fade_animation_duration);
11514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        final int tileColor = res.getColor(R.color.bitmap_placeholder_background_color);
11614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        mProgressDelayMs = res.getInteger(R.integer.bitmap_progress_animation_delay);
11714110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
11814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        int placeholderSize = res.getDimensionPixelSize(R.dimen.placeholder_size);
11914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        mPlaceholder = new Placeholder(placeholder.getConstantState().newDrawable(res), res,
12014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne                placeholderSize, placeholderSize, fadeOutDurationMs, tileColor);
12114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        mPlaceholder.setCallback(this);
12214110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
12314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        int progressBarSize = res.getDimensionPixelSize(R.dimen.progress_bar_size);
12414110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        mProgress = new Progress(progress.getConstantState().newDrawable(res), res,
12514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne                progressBarSize, progressBarSize, fadeOutDurationMs, tileColor);
12614110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        mProgress.setCallback(this);
127c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola    }
12814110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne
12914110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    public RequestKey getKey() {
13014110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne        return mCurrKey;
13114110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    }
132c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola
13314110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne    /**
134c4850c2aa4c281a352e228aafc51fb1e30dcad02Rafael Espindola     * Set the dimensions to which to decode into. For a parallax effect, ensure the height is
13514110477887e3dc168ffc6c191e72d705051f99ePeter Collingbourne     * larger than the destination of the bitmap.
136     * TODO: test parallax
137     */
138    public void setDecodeDimensions(int w, int h) {
139        mDecodeWidth = w;
140        mDecodeHeight = h;
141        decode();
142    }
143
144    public void setParallaxSpeedMultiplier(final float parallaxSpeedMultiplier) {
145        mParallaxSpeedMultiplier = parallaxSpeedMultiplier;
146    }
147
148    public void showStaticPlaceholder() {
149        setLoadState(LOAD_STATE_FAILED);
150    }
151
152    public void unbind() {
153        setImage(null);
154    }
155
156    public void bind(RequestKey key) {
157        setImage(key);
158    }
159
160    private void setImage(final RequestKey key) {
161        if (mCurrKey != null && mCurrKey.equals(key)) {
162            return;
163        }
164
165        Trace.beginSection("set image");
166        Trace.beginSection("release reference");
167        if (mBitmap != null) {
168            mBitmap.releaseReference();
169            mBitmap = null;
170        }
171        Trace.endSection();
172        if (mCurrKey != null && mDecodeAggregator != null) {
173            mDecodeAggregator.forget(mCurrKey);
174        }
175        mCurrKey = key;
176
177        if (mTask != null) {
178            mTask.cancel();
179            mTask = null;
180        }
181
182        mHandler.removeCallbacks(this);
183        // start from a clean slate on every bind
184        // this allows the initial transition to be specially instantaneous, so e.g. a cache hit
185        // doesn't unnecessarily trigger a fade-in
186        setLoadState(LOAD_STATE_UNINITIALIZED);
187
188        if (key == null) {
189            invalidateSelf();
190            Trace.endSection();
191            return;
192        }
193
194        // find cached entry here and skip decode if found.
195        final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */);
196        if (cached != null) {
197            setBitmap(cached);
198            if (DEBUG) {
199                Log.d(TAG, String.format("CACHE HIT key=%s", mCurrKey));
200            }
201        } else {
202            decode();
203            if (DEBUG) {
204                Log.d(TAG, String.format(
205                        "CACHE MISS key=%s\ncache=%s", mCurrKey, mCache.toDebugString()));
206            }
207        }
208        Trace.endSection();
209    }
210
211    @Override
212    public void setParallaxFraction(float fraction) {
213        mParallaxFraction = fraction;
214    }
215
216    @Override
217    public void draw(final Canvas canvas) {
218        final Rect bounds = getBounds();
219        if (bounds.isEmpty()) {
220            return;
221        }
222
223        if (mBitmap != null && mBitmap.bmp != null) {
224            BitmapUtils.calculateCroppedSrcRect(
225                    mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
226                    bounds.width(), bounds.height(),
227                    bounds.height(), Integer.MAX_VALUE,
228                    mParallaxFraction, false /* absoluteFraction */,
229                    mParallaxSpeedMultiplier, mSrcRect);
230
231            final int orientation = mBitmap.getOrientation();
232            // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
233            // been corrected. We need to decode the uncorrected source rectangle. Calculate true
234            // coordinates.
235            RectUtils.rotateRectForOrientation(orientation,
236                    new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()),
237                    mSrcRect);
238
239            // We may need to rotate the canvas, so we also have to rotate the bounds.
240            final Rect rotatedBounds = new Rect(bounds);
241            RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds);
242
243            // Rotate the canvas.
244            canvas.save();
245            canvas.rotate(orientation, bounds.centerX(), bounds.centerY());
246            canvas.drawBitmap(mBitmap.bmp, mSrcRect, rotatedBounds, mPaint);
247            canvas.restore();
248        }
249
250        // Draw the two possible overlay layers in reverse-priority order.
251        // (each layer will no-op the draw when appropriate)
252        // This ordering means cross-fade transitions are just fade-outs of each layer.
253        mProgress.draw(canvas);
254        mPlaceholder.draw(canvas);
255    }
256
257    @Override
258    public void setAlpha(int alpha) {
259        final int old = mPaint.getAlpha();
260        mPaint.setAlpha(alpha);
261        mPlaceholder.setAlpha(alpha);
262        mProgress.setAlpha(alpha);
263        if (alpha != old) {
264            invalidateSelf();
265        }
266    }
267
268    @Override
269    public void setColorFilter(ColorFilter cf) {
270        mPaint.setColorFilter(cf);
271        mPlaceholder.setColorFilter(cf);
272        mProgress.setColorFilter(cf);
273        invalidateSelf();
274    }
275
276    @Override
277    public int getOpacity() {
278        return (mBitmap != null && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ?
279                PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
280    }
281
282    @Override
283    protected void onBoundsChange(Rect bounds) {
284        super.onBoundsChange(bounds);
285
286        mPlaceholder.setBounds(bounds);
287        mProgress.setBounds(bounds);
288    }
289
290    @Override
291    public void onDecodeBegin(final RequestKey key) {
292        if (mDecodeAggregator != null) {
293            mDecodeAggregator.expect(key, this);
294        } else {
295            onBecomeFirstExpected(key);
296        }
297    }
298
299    @Override
300    public void onBecomeFirstExpected(final RequestKey key) {
301        if (!key.equals(mCurrKey)) {
302            return;
303        }
304        // normally, we'd transition to the LOADING state now, but we want to delay that a bit
305        // to minimize excess occurrences of the rotating spinner
306        mHandler.postDelayed(this, mProgressDelayMs);
307    }
308
309    @Override
310    public void run() {
311        if (mLoadState == LOAD_STATE_NOT_YET_LOADED) {
312            setLoadState(LOAD_STATE_LOADING);
313        }
314    }
315
316    @Override
317    public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) {
318        if (mDecodeAggregator != null) {
319            mDecodeAggregator.execute(key, new Runnable() {
320                @Override
321                public void run() {
322                    onDecodeCompleteImpl(key, result);
323                }
324
325                @Override
326                public String toString() {
327                    return "DONE";
328                }
329            });
330        } else {
331            onDecodeCompleteImpl(key, result);
332        }
333    }
334
335    private void onDecodeCompleteImpl(final RequestKey key, final ReusableBitmap result) {
336        if (key.equals(mCurrKey)) {
337            setBitmap(result);
338        } else {
339            // if the requests don't match (i.e. this request is stale), decrement the
340            // ref count to allow the bitmap to be pooled
341            if (result != null) {
342                result.releaseReference();
343            }
344        }
345    }
346
347    @Override
348    public void onDecodeCancel(final RequestKey key) {
349        if (mDecodeAggregator != null) {
350            mDecodeAggregator.forget(key);
351        }
352    }
353
354    private void setBitmap(ReusableBitmap bmp) {
355        if (mBitmap != null && mBitmap != bmp) {
356            mBitmap.releaseReference();
357        }
358        mBitmap = bmp;
359        setLoadState((bmp != null) ? LOAD_STATE_LOADED : LOAD_STATE_FAILED);
360        invalidateSelf();
361    }
362
363    private void decode() {
364        final int bufferW;
365        final int bufferH;
366
367        if (mCurrKey == null) {
368            return;
369        }
370
371        Trace.beginSection("decode");
372        if (mLimitDensity) {
373            final float scale =
374                    Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT
375                            / mDensity);
376            bufferW = (int) (mDecodeWidth * scale);
377            bufferH = (int) (mDecodeHeight * scale);
378        } else {
379            bufferW = mDecodeWidth;
380            bufferH = mDecodeHeight;
381        }
382
383        if (bufferW == 0 || bufferH == 0) {
384            Trace.endSection();
385            return;
386        }
387        if (mTask != null) {
388            mTask.cancel();
389        }
390        setLoadState(LOAD_STATE_NOT_YET_LOADED);
391        final DecodeOptions opts = new DecodeOptions(bufferW, bufferH, VERTICAL_CENTER,
392                DecodeOptions.STRATEGY_ROUND_NEAREST);
393        // TODO: file is null because we expect this class to extend BasicBitmapDrawable soon.
394        mTask = new DecodeTask(mCurrKey, opts, null /* file */, this, mCache);
395        mTask.executeOnExecutor(EXECUTOR);
396        Trace.endSection();
397    }
398
399    private void setLoadState(int loadState) {
400        if (DEBUG) {
401            Log.v(TAG, String.format("IN setLoadState. old=%s new=%s key=%s this=%s",
402                    mLoadState, loadState, mCurrKey, this));
403        }
404        if (mLoadState == loadState) {
405            if (DEBUG) {
406                Log.v(TAG, "OUT no-op setLoadState");
407            }
408            return;
409        }
410
411        Trace.beginSection("set load state");
412        switch (loadState) {
413            // This state differs from LOADED in that the subsequent state transition away from
414            // UNINITIALIZED will not have a fancy transition. This allows list item binds to
415            // cached data to take immediate effect without unnecessary whizzery.
416            case LOAD_STATE_UNINITIALIZED:
417                mPlaceholder.reset();
418                mProgress.reset();
419                break;
420            case LOAD_STATE_NOT_YET_LOADED:
421                mPlaceholder.setPulseEnabled(true);
422                mPlaceholder.setVisible(true);
423                mProgress.setVisible(false);
424                break;
425            case LOAD_STATE_LOADING:
426                mPlaceholder.setVisible(false);
427                mProgress.setVisible(true);
428                break;
429            case LOAD_STATE_LOADED:
430                mPlaceholder.setVisible(false);
431                mProgress.setVisible(false);
432                break;
433            case LOAD_STATE_FAILED:
434                mPlaceholder.setPulseEnabled(false);
435                mPlaceholder.setVisible(true);
436                mProgress.setVisible(false);
437                break;
438        }
439        Trace.endSection();
440
441        mLoadState = loadState;
442        boolean placeholderVisible = mPlaceholder != null && mPlaceholder.isVisible();
443        boolean progressVisible = mProgress != null && mProgress.isVisible();
444
445        if (DEBUG) {
446            Log.v(TAG, String.format("OUT stateful setLoadState. new=%s placeholder=%s progress=%s",
447                    loadState, placeholderVisible, progressVisible));
448        }
449    }
450
451    @Override
452    public void invalidateDrawable(Drawable who) {
453        invalidateSelf();
454    }
455
456    @Override
457    public void scheduleDrawable(Drawable who, Runnable what, long when) {
458        scheduleSelf(what, when);
459    }
460
461    @Override
462    public void unscheduleDrawable(Drawable who, Runnable what) {
463        unscheduleSelf(what);
464    }
465
466    private static class Placeholder extends TileDrawable {
467
468        private final ValueAnimator mPulseAnimator;
469        private boolean mPulseEnabled = true;
470        private float mPulseAlphaFraction = 1f;
471
472        public Placeholder(Drawable placeholder, Resources res,
473                int placeholderWidth, int placeholderHeight, int fadeOutDurationMs,
474                int tileColor) {
475            super(placeholder, placeholderWidth, placeholderHeight, tileColor, fadeOutDurationMs);
476            mPulseAnimator = ValueAnimator.ofInt(55, 255)
477                    .setDuration(res.getInteger(R.integer.bitmap_placeholder_animation_duration));
478            mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
479            mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE);
480            mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() {
481                @Override
482                public void onAnimationUpdate(ValueAnimator animation) {
483                    mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f;
484                    setInnerAlpha(getCurrentAlpha());
485                }
486            });
487            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
488                @Override
489                public void onAnimationEnd(Animator animation) {
490                    stopPulsing();
491                }
492            });
493        }
494
495        @Override
496        public void setInnerAlpha(final int alpha) {
497            super.setInnerAlpha((int) (alpha * mPulseAlphaFraction));
498        }
499
500        public void setPulseEnabled(boolean enabled) {
501            mPulseEnabled = enabled;
502            if (!mPulseEnabled) {
503                stopPulsing();
504            }
505        }
506
507        private void stopPulsing() {
508            if (mPulseAnimator != null) {
509                mPulseAnimator.cancel();
510                mPulseAlphaFraction = 1f;
511                setInnerAlpha(getCurrentAlpha());
512            }
513        }
514
515        @Override
516        public boolean setVisible(boolean visible) {
517            final boolean changed = super.setVisible(visible);
518            if (changed) {
519                if (isVisible()) {
520                    // start
521                    if (mPulseAnimator != null && mPulseEnabled) {
522                        mPulseAnimator.start();
523                    }
524                } else {
525                    // can't cancel the pulsing yet-- wait for the fade-out animation to end
526                    // one exception: if alpha is already zero, there is no fade-out, so stop now
527                    if (getCurrentAlpha() == 0) {
528                        stopPulsing();
529                    }
530                }
531            }
532            return changed;
533        }
534
535    }
536
537    private static class Progress extends TileDrawable {
538
539        private final ValueAnimator mRotateAnimator;
540
541        public Progress(Drawable progress, Resources res,
542                int progressBarWidth, int progressBarHeight, int fadeOutDurationMs,
543                int tileColor) {
544            super(progress, progressBarWidth, progressBarHeight, tileColor, fadeOutDurationMs);
545
546            mRotateAnimator = ValueAnimator.ofInt(0, 10000)
547                    .setDuration(res.getInteger(R.integer.bitmap_progress_animation_duration));
548            mRotateAnimator.setInterpolator(new LinearInterpolator());
549            mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
550            mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() {
551                @Override
552                public void onAnimationUpdate(ValueAnimator animation) {
553                    setLevel((Integer) animation.getAnimatedValue());
554                }
555            });
556            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
557                @Override
558                public void onAnimationEnd(Animator animation) {
559                    if (mRotateAnimator != null) {
560                        mRotateAnimator.cancel();
561                    }
562                }
563            });
564        }
565
566        @Override
567        public boolean setVisible(boolean visible) {
568            final boolean changed = super.setVisible(visible);
569            if (changed) {
570                if (isVisible()) {
571                    if (mRotateAnimator != null) {
572                        mRotateAnimator.start();
573                    }
574                } else {
575                    // can't cancel the rotate yet-- wait for the fade-out animation to end
576                    // one exception: if alpha is already zero, there is no fade-out, so stop now
577                    if (getCurrentAlpha() == 0 && mRotateAnimator != null) {
578                        mRotateAnimator.cancel();
579                    }
580                }
581            }
582            return changed;
583        }
584
585    }
586}
587