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