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