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