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