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