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