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