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