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