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