ExtendedBitmapDrawable.java revision 41af50eb1ac488572b066629c3954b23c21dfa76
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.Rect;
27import android.graphics.drawable.Drawable;
28import android.os.Handler;
29import android.util.Log;
30import android.view.animation.LinearInterpolator;
31
32import com.android.bitmap.BitmapCache;
33import com.android.bitmap.DecodeAggregator;
34import com.android.bitmap.DecodeTask;
35import com.android.bitmap.R;
36import com.android.bitmap.RequestKey;
37import com.android.bitmap.ReusableBitmap;
38import com.android.bitmap.util.Trace;
39
40/**
41 * This class encapsulates all functionality needed to display a single image bitmap,
42 * including request creation/cancelling, data unbinding and re-binding, and fancy animations
43 * to draw upon state changes.
44 * <p>
45 * The actual bitmap decode work is handled by {@link DecodeTask}.
46 * TODO: have this class extend from BasicBitmapDrawable
47 */
48public class ExtendedBitmapDrawable extends BasicBitmapDrawable implements
49    Runnable, Parallaxable, DecodeAggregator.Callback {
50
51    private final ExtendedOptions mOpts;
52
53    // Parallax.
54    private static final float DECODE_VERTICAL_CENTER = 1f / 3;
55    private float mParallaxFraction = 1f / 2;
56
57    // State changes.
58    private static final int LOAD_STATE_UNINITIALIZED = 0;
59    private static final int LOAD_STATE_NOT_YET_LOADED = 1;
60    private static final int LOAD_STATE_LOADING = 2;
61    private static final int LOAD_STATE_LOADED = 3;
62    private static final int LOAD_STATE_FAILED = 4;
63    private int mLoadState = LOAD_STATE_UNINITIALIZED;
64    private Placeholder mPlaceholder;
65    private Progress mProgress;
66    private int mProgressDelayMs;
67    private final Handler mHandler = new Handler();
68
69    public static final boolean DEBUG = false;
70    public static final String TAG = ExtendedBitmapDrawable.class.getSimpleName();
71
72    public ExtendedBitmapDrawable(final Resources res, final BitmapCache cache,
73            final boolean limitDensity, final ExtendedOptions opts) {
74        super(res, cache, limitDensity);
75
76        opts.validate();
77        mOpts = opts;
78
79        // Placeholder and progress.
80        if ((opts.features & ExtendedOptions.FEATURE_STATE_CHANGES) != 0) {
81            final int fadeOutDurationMs = res.getInteger(R.integer.bitmap_fade_animation_duration);
82            mProgressDelayMs = res.getInteger(R.integer.bitmap_progress_animation_delay);
83
84            // Placeholder is not optional because of backgroundColor.
85            int placeholderSize = res.getDimensionPixelSize(R.dimen.placeholder_size);
86            mPlaceholder = new Placeholder(
87                    opts.placeholder != null ? opts.placeholder.getConstantState().newDrawable(res)
88                            : null, res, placeholderSize, placeholderSize, fadeOutDurationMs, opts);
89            mPlaceholder.setCallback(this);
90
91            // Progress bar is optional.
92            if (opts.progressBar != null) {
93                int progressBarSize = res.getDimensionPixelSize(R.dimen.progress_bar_size);
94                mProgress = new Progress(opts.progressBar.getConstantState().newDrawable(res), res,
95                        progressBarSize, progressBarSize, fadeOutDurationMs, opts);
96                mProgress.setCallback(this);
97            }
98        }
99    }
100
101    @Override
102    public void setParallaxFraction(float fraction) {
103        mParallaxFraction = fraction;
104        invalidateSelf();
105    }
106
107    /**
108     * Get the ExtendedOptions used to instantiate this ExtendedBitmapDrawable. Any changes made to
109     * the parameters inside the options will take effect immediately.
110     */
111    public ExtendedOptions getExtendedOptions() {
112        return mOpts;
113    }
114
115    /**
116     * This sets the drawable to the failed state, which remove all animations from the placeholder.
117     * This is different from unbinding to the uninitialized state, where we expect animations.
118     */
119    public void showStaticPlaceholder() {
120        setLoadState(LOAD_STATE_FAILED);
121    }
122
123    @Override
124    protected void setImage(final RequestKey key) {
125        if (mCurrKey != null && mCurrKey.equals(key)) {
126            return;
127        }
128
129        if (mCurrKey != null && getDecodeAggregator() != null) {
130            getDecodeAggregator().forget(mCurrKey);
131        }
132
133        mHandler.removeCallbacks(this);
134        // start from a clean slate on every bind
135        // this allows the initial transition to be specially instantaneous, so e.g. a cache hit
136        // doesn't unnecessarily trigger a fade-in
137        setLoadState(LOAD_STATE_UNINITIALIZED);
138        if (key == null) {
139            setLoadState(LOAD_STATE_FAILED);
140        }
141
142        super.setImage(key);
143    }
144
145    @Override
146    protected void setBitmap(ReusableBitmap bmp) {
147        setLoadState((bmp != null) ? LOAD_STATE_LOADED : LOAD_STATE_FAILED);
148
149        super.setBitmap(bmp);
150    }
151
152    @Override
153    protected void loadFileDescriptorFactory() {
154        boolean executeStateChange = shouldExecuteStateChange();
155        if (executeStateChange) {
156            if (mCurrKey == null || mDecodeWidth == 0 || mDecodeHeight == 0) {
157                setLoadState(LOAD_STATE_FAILED);
158            } else {
159                setLoadState(LOAD_STATE_NOT_YET_LOADED);
160            }
161        }
162
163        super.loadFileDescriptorFactory();
164    }
165
166    protected boolean shouldExecuteStateChange() {
167        // TODO: AttachmentDrawable should override this method to match prev and curr request keys.
168        return /* opts.stateChanges */ true;
169    }
170
171    @Override
172    public float getDrawVerticalCenter() {
173        return mParallaxFraction;
174    }
175
176    @Override
177    protected float getDrawVerticalOffsetMultiplier() {
178        return mOpts.parallaxSpeedMultiplier;
179    }
180
181    @Override
182    protected float getDecodeVerticalCenter() {
183        return DECODE_VERTICAL_CENTER;
184    }
185
186    private DecodeAggregator getDecodeAggregator() {
187        return mOpts.decodeAggregator;
188    }
189
190    /**
191     * Instead of overriding this method, subclasses should override {@link #onDraw(Canvas)}.
192     *
193     * The reason for this is that we need the placeholder and progress bar to be drawn over our
194     * content. Those two drawables fade out, giving the impression that our content is fading in.
195     */
196    @Override
197    public final void draw(final Canvas canvas) {
198        final Rect bounds = getBounds();
199        if (bounds.isEmpty()) {
200            return;
201        }
202
203        onDraw(canvas);
204
205        // Draw the two possible overlay layers in reverse-priority order.
206        // (each layer will no-op the draw when appropriate)
207        // This ordering means cross-fade transitions are just fade-outs of each layer.
208        if (mProgress != null) mProgress.draw(canvas);
209        if (mPlaceholder != null) mPlaceholder.draw(canvas);
210    }
211
212    /**
213     * Overriding this method to add your own custom drawing.
214     */
215    protected void onDraw(final Canvas canvas) {
216        super.draw(canvas);
217    }
218
219    @Override
220    public void setAlpha(int alpha) {
221        final int old = mPaint.getAlpha();
222        super.setAlpha(alpha);
223        if (mPlaceholder != null) mPlaceholder.setAlpha(alpha);
224        if (mProgress != null) mProgress.setAlpha(alpha);
225        if (alpha != old) {
226            invalidateSelf();
227        }
228    }
229
230    @Override
231    public void setColorFilter(ColorFilter cf) {
232        super.setColorFilter(cf);
233        if (mPlaceholder != null) mPlaceholder.setColorFilter(cf);
234        if (mProgress != null) mProgress.setColorFilter(cf);
235        invalidateSelf();
236    }
237
238    @Override
239    protected void onBoundsChange(Rect bounds) {
240        super.onBoundsChange(bounds);
241        if (mPlaceholder != null) mPlaceholder.setBounds(bounds);
242        if (mProgress != null) mProgress.setBounds(bounds);
243    }
244
245    @Override
246    public void onDecodeBegin(final RequestKey key) {
247        if (getDecodeAggregator() != null) {
248            getDecodeAggregator().expect(key, this);
249        } else {
250            onBecomeFirstExpected(key);
251        }
252        super.onDecodeBegin(key);
253    }
254
255    @Override
256    public void onBecomeFirstExpected(final RequestKey key) {
257        if (!key.equals(mCurrKey)) {
258            return;
259        }
260        // normally, we'd transition to the LOADING state now, but we want to delay that a bit
261        // to minimize excess occurrences of the rotating spinner
262        mHandler.postDelayed(this, mProgressDelayMs);
263    }
264
265    @Override
266    public void run() {
267        if (mLoadState == LOAD_STATE_NOT_YET_LOADED) {
268            setLoadState(LOAD_STATE_LOADING);
269        }
270    }
271
272    @Override
273    public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) {
274        if (getDecodeAggregator() != null) {
275            getDecodeAggregator().execute(key, new Runnable() {
276                @Override
277                public void run() {
278                    ExtendedBitmapDrawable.super.onDecodeComplete(key, result);
279                }
280
281                @Override
282                public String toString() {
283                    return "DONE";
284                }
285            });
286        } else {
287            super.onDecodeComplete(key, result);
288        }
289    }
290
291    @Override
292    public void onDecodeCancel(final RequestKey key) {
293        if (getDecodeAggregator() != null) {
294            getDecodeAggregator().forget(key);
295        }
296        super.onDecodeCancel(key);
297    }
298
299    /**
300     * Each attachment gets its own placeholder and progress indicator, to be shown, hidden,
301     * and animated based on Drawable#setVisible() changes, which are in turn driven by
302     * setLoadState().
303     */
304    private void setLoadState(int loadState) {
305        if (DEBUG) {
306            Log.v(TAG, String.format("IN setLoadState. old=%s new=%s key=%s this=%s",
307                    mLoadState, loadState, mCurrKey, this));
308        }
309        if (mLoadState == loadState) {
310            if (DEBUG) {
311                Log.v(TAG, "OUT no-op setLoadState");
312            }
313            return;
314        }
315
316        Trace.beginSection("set load state");
317        switch (loadState) {
318            // This state differs from LOADED in that the subsequent state transition away from
319            // UNINITIALIZED will not have a fancy transition. This allows list item binds to
320            // cached data to take immediate effect without unnecessary whizzery.
321            case LOAD_STATE_UNINITIALIZED:
322                if (mPlaceholder != null) mPlaceholder.reset();
323                if (mProgress != null) mProgress.reset();
324                break;
325            case LOAD_STATE_NOT_YET_LOADED:
326                if (mPlaceholder != null) {
327                    mPlaceholder.setPulseEnabled(true);
328                    mPlaceholder.setVisible(true);
329                }
330                if (mProgress != null) mProgress.setVisible(false);
331                break;
332            case LOAD_STATE_LOADING:
333                if (mProgress == null) {
334                    // Stay in same visual state as LOAD_STATE_NOT_YET_LOADED.
335                    break;
336                }
337                if (mPlaceholder != null) mPlaceholder.setVisible(false);
338                if (mProgress != null) mProgress.setVisible(true);
339                break;
340            case LOAD_STATE_LOADED:
341                if (mPlaceholder != null) mPlaceholder.setVisible(false);
342                if (mProgress != null) mProgress.setVisible(false);
343                break;
344            case LOAD_STATE_FAILED:
345                if (mPlaceholder != null) {
346                    mPlaceholder.setPulseEnabled(false);
347                    mPlaceholder.setVisible(true);
348                }
349                if (mProgress != null) mProgress.setVisible(false);
350                break;
351        }
352        Trace.endSection();
353
354        mLoadState = loadState;
355        boolean placeholderVisible = mPlaceholder != null && mPlaceholder.isVisible();
356        boolean progressVisible = mProgress != null && mProgress.isVisible();
357
358        if (DEBUG) {
359            Log.v(TAG, String.format("OUT stateful setLoadState. new=%s placeholder=%s progress=%s",
360                    loadState, placeholderVisible, progressVisible));
361        }
362    }
363
364    private static class Placeholder extends TileDrawable {
365
366        private final ValueAnimator mPulseAnimator;
367        private boolean mPulseEnabled = true;
368        private float mPulseAlphaFraction = 1f;
369
370        public Placeholder(Drawable placeholder, Resources res, int placeholderWidth,
371                int placeholderHeight, int fadeOutDurationMs, ExtendedOptions opts) {
372            super(placeholder, placeholderWidth, placeholderHeight, fadeOutDurationMs, opts);
373
374            mPulseAnimator = ValueAnimator.ofInt(55, 255)
375                    .setDuration(res.getInteger(R.integer.bitmap_placeholder_animation_duration));
376            mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
377            mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE);
378            mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() {
379                @Override
380                public void onAnimationUpdate(ValueAnimator animation) {
381                    mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f;
382                    setInnerAlpha(getCurrentAlpha());
383                }
384            });
385            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
386                @Override
387                public void onAnimationEnd(Animator animation) {
388                    stopPulsing();
389                }
390            });
391        }
392
393        @Override
394        public void setInnerAlpha(final int alpha) {
395            super.setInnerAlpha((int) (alpha * mPulseAlphaFraction));
396        }
397
398        public void setPulseEnabled(boolean enabled) {
399            mPulseEnabled = enabled;
400            if (!mPulseEnabled) {
401                stopPulsing();
402            }
403        }
404
405        private void stopPulsing() {
406            if (mPulseAnimator != null) {
407                mPulseAnimator.cancel();
408                mPulseAlphaFraction = 1f;
409                setInnerAlpha(getCurrentAlpha());
410            }
411        }
412
413        @Override
414        public boolean setVisible(boolean visible) {
415            final boolean changed = super.setVisible(visible);
416            if (changed) {
417                if (isVisible()) {
418                    // start
419                    if (mPulseAnimator != null && mPulseEnabled) {
420                        mPulseAnimator.start();
421                    }
422                } else {
423                    // can't cancel the pulsing yet-- wait for the fade-out animation to end
424                    // one exception: if alpha is already zero, there is no fade-out, so stop now
425                    if (getCurrentAlpha() == 0) {
426                        stopPulsing();
427                    }
428                }
429            }
430            return changed;
431        }
432
433    }
434
435    private static class Progress extends TileDrawable {
436
437        private final ValueAnimator mRotateAnimator;
438
439        public Progress(Drawable progress, Resources res,
440                int progressBarWidth, int progressBarHeight, int fadeOutDurationMs,
441                ExtendedOptions opts) {
442            super(progress, progressBarWidth, progressBarHeight, fadeOutDurationMs, opts);
443
444            mRotateAnimator = ValueAnimator.ofInt(0, 10000)
445                    .setDuration(res.getInteger(R.integer.bitmap_progress_animation_duration));
446            mRotateAnimator.setInterpolator(new LinearInterpolator());
447            mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
448            mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() {
449                @Override
450                public void onAnimationUpdate(ValueAnimator animation) {
451                    setLevel((Integer) animation.getAnimatedValue());
452                }
453            });
454            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
455                @Override
456                public void onAnimationEnd(Animator animation) {
457                    if (mRotateAnimator != null) {
458                        mRotateAnimator.cancel();
459                    }
460                }
461            });
462        }
463
464        @Override
465        public boolean setVisible(boolean visible) {
466            final boolean changed = super.setVisible(visible);
467            if (changed) {
468                if (isVisible()) {
469                    if (mRotateAnimator != null) {
470                        mRotateAnimator.start();
471                    }
472                } else {
473                    // can't cancel the rotate yet-- wait for the fade-out animation to end
474                    // one exception: if alpha is already zero, there is no fade-out, so stop now
475                    if (getCurrentAlpha() == 0 && mRotateAnimator != null) {
476                        mRotateAnimator.cancel();
477                    }
478                }
479            }
480            return changed;
481        }
482    }
483
484    /**
485     * This class contains the features a client can specify, and arguments to those features.
486     * Clients can later retrieve the ExtendedOptions from an ExtendedBitmapDrawable and change the
487     * parameters, which will be reflected immediately.
488     */
489    public static class ExtendedOptions {
490
491        /**
492         * Summary:
493         * This feature enables you to draw decoded bitmap in order on the screen, to give the
494         * visual effect of a single decode thread.
495         *
496         * <p/>
497         * Explanation:
498         * Since DecodeTasks are asynchronous, multiple tasks may finish decoding at different
499         * times. To have a smooth user experience, provide a shared {@link DecodeAggregator} to all
500         * the ExtendedBitmapDrawables, and the decode aggregator will hold finished decodes so they
501         * come back in order.
502         *
503         * <p/>
504         * Pros:
505         * Visual consistency. Images are not popping up randomly all over the place.
506         *
507         * <p/>
508         * Cons:
509         * Artificial delay. Images are not drawn as soon as they are decoded. They must wait
510         * for their turn.
511         *
512         * <p/>
513         * Requirements:
514         * Set {@link #decodeAggregator} to a shared {@link DecodeAggregator}.
515         */
516        public static final int FEATURE_ORDERED_DISPLAY = 1;
517
518        /**
519         * Summary:
520         * This feature enables the image to move in parallax as the user scrolls, to give visual
521         * flair to your images.
522         *
523         * <p/>
524         * Explanation:
525         * When the user scrolls D pixels in the vertical direction, this ExtendedBitmapDrawable
526         * shifts its Bitmap f(D) pixels in the vertical direction before drawing to the screen.
527         * Depending on the function f, the parallax effect can give varying interesting results.
528         *
529         * <p/>
530         * Pros:
531         * Visual pop and playfulness. Feeling of movement. Pleasantly surprise your users.
532         *
533         * <p/>
534         * Cons:
535         * Some users report motion sickness with certain speed multiplier values. Decode height
536         * must be greater than visual bounds to account for the parallax. This uses more memory and
537         * decoding time.
538         *
539         * <p/>
540         * Requirements:
541         * Set {@link #parallaxSpeedMultiplier} to the ratio between the decoded height and the
542         * visual bound height. Call {@link ExtendedBitmapDrawable#setDecodeDimensions(int, int)}
543         * with the height multiplied by {@link #parallaxSpeedMultiplier}.
544         * Call {@link ExtendedBitmapDrawable#setParallaxFraction(float)} when the user scrolls.
545         */
546        public static final int FEATURE_PARALLAX = 1 << 1;
547
548        /**
549         * Summary:
550         * This feature enables fading in between multiple decode states, to give smooth transitions
551         * to and from the placeholder, progress bars, and decoded image.
552         *
553         * <p/>
554         * Explanation:
555         * The states are: {@link ExtendedBitmapDrawable#LOAD_STATE_UNINITIALIZED},
556         * {@link ExtendedBitmapDrawable#LOAD_STATE_NOT_YET_LOADED},
557         * {@link ExtendedBitmapDrawable#LOAD_STATE_LOADING},
558         * {@link ExtendedBitmapDrawable#LOAD_STATE_LOADED}, and
559         * {@link ExtendedBitmapDrawable#LOAD_STATE_FAILED}. These states affect whether the
560         * placeholder and/or the progress bar is showing and animating. We first show the
561         * pulsating placeholder when an image begins decoding. After 2 seconds, we fade in a
562         * spinning progress bar. When the decode completes, we fade in the image.
563         *
564         * <p/>
565         * Pros:
566         * Smooth, beautiful transitions avoid perceived jank. Progress indicator informs users that
567         * work is being done and the app is not stalled.
568         *
569         * <p/>
570         * Cons:
571         * Very fast decodes' short decode time would be eclipsed by the animation duration. Static
572         * placeholder could be accomplished by {@link BasicBitmapDrawable} without the added
573         * complexity of states.
574         *
575         * <p/>
576         * Requirements:
577         * Set {@link #backgroundColor} to the color used for the background of the placeholder and
578         * progress bar. Use the alternative constructor to populate {@link #placeholder} and
579         * {@link #progressBar}.
580         */
581        public static final int FEATURE_STATE_CHANGES = 1 << 2;
582
583        /**
584         * Non-changeable bit field describing the features you want the
585         * {@link ExtendedBitmapDrawable} to support.
586         *
587         * <p/>
588         * Example:
589         * <code>
590         * opts.features = FEATURE_ORDERED_DISPLAY | FEATURE_PARALLAX | FEATURE_STATE_CHANGES;
591         * </code>
592         */
593        public final int features;
594
595        /**
596         * Required field if {@link #FEATURE_ORDERED_DISPLAY} is supported.
597         */
598        public DecodeAggregator decodeAggregator = null;
599
600        /**
601         * Required field if {@link #FEATURE_PARALLAX} is supported.
602         *
603         * A value of 1.5f gives a subtle parallax, and is a good value to
604         * start with. 2.0f gives a more obvious parallax, arguably exaggerated. Some users report
605         * motion sickness with 2.0f. A value of 1.0f is synonymous with no parallax. Be careful not
606         * to set too high a value, since we will start cropping the widths if the image's height is
607         * not sufficient.
608         */
609        public float parallaxSpeedMultiplier = 1;
610
611        /**
612         * Optional field if {@link #FEATURE_STATE_CHANGES} is supported.
613         *
614         * See {@link android.graphics.Color}.
615         */
616        public int backgroundColor = 0;
617
618        /**
619         * Optional non-changeable field if {@link #FEATURE_STATE_CHANGES} is supported.
620         */
621        public final Drawable placeholder;
622
623        /**
624         * Optional non-changeable field if {@link #FEATURE_STATE_CHANGES} is supported.
625         */
626        public final Drawable progressBar;
627
628        /**
629         * Use this constructor when all the feature parameters are changeable.
630         */
631        public ExtendedOptions(final int features) {
632            this(features, null, null);
633        }
634
635        /**
636         * Use this constructor when you have to specify non-changeable feature parameters.
637         */
638        public ExtendedOptions(final int features, final Drawable placeholder,
639                final Drawable progressBar) {
640            this.features = features;
641            this.placeholder = placeholder;
642            this.progressBar = progressBar;
643        }
644
645        /**
646         * Validate this ExtendedOptions instance to make sure that all the required fields are set
647         * for the requested features.
648         *
649         * This will throw an IllegalStateException if validation fails.
650         */
651        private void validate()
652                throws IllegalStateException {
653            if ((features & FEATURE_ORDERED_DISPLAY) != 0 && decodeAggregator == null) {
654                throw new IllegalStateException(
655                        "ExtendedOptions: To support FEATURE_ORDERED_DISPLAY, "
656                                + "decodeAggregator must be set.");
657            }
658            if ((features & FEATURE_PARALLAX) != 0 && parallaxSpeedMultiplier == 0) {
659                throw new IllegalStateException(
660                        "ExtendedOptions: To support FEATURE_PARALLAX, "
661                                + "parallaxSpeedMultiplier must be set.");
662            }
663            if ((features & FEATURE_STATE_CHANGES) != 0 && backgroundColor == 0
664                    && placeholder == null) {
665                throw new IllegalStateException(
666                        "ExtendedOptions: To support FEATURE_STATE_CHANGES, "
667                                + "either backgroundColor or placeholder must be set.");
668            }
669        }
670    }
671}
672