Home | History | Annotate | Download | only in drawable
      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 
     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      */
     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
    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      */
    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      */
    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      */
    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
    178     public void setDecodeDimensions(int width, int height) {
    179         super.setDecodeDimensions(width, (int) (height * mOpts.parallaxSpeedMultiplier));
    180     }
    181 
    182     @Override
    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
    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
    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
    227     protected void onDecodeFailed() {
    228         super.onDecodeFailed();
    229 
    230         setLoadState(LOAD_STATE_FAILED);
    231     }
    232 
    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
    239     public float getDrawVerticalCenter() {
    240         return mParallaxFraction;
    241     }
    242 
    243     @Override
    244     protected final float getDrawVerticalOffsetMultiplier() {
    245         return mOpts.parallaxSpeedMultiplier;
    246     }
    247 
    248     @Override
    249     protected float getDecodeHorizontalCenter() {
    250         return mOpts.decodeHorizontalCenter;
    251     }
    252 
    253     @Override
    254     protected float getDecodeVerticalCenter() {
    255         return mOpts.decodeVerticalCenter;
    256     }
    257 
    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
    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      */
    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      */
    296     protected void onDrawPlaceholderOrProgress(final Canvas canvas, final TileDrawable drawable) {
    297         drawable.draw(canvas);
    298     }
    299 
    300     @Override
    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
    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
    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
    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
    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
    347     public void run() {
    348         if (mLoadState == LOAD_STATE_NOT_YET_LOADED) {
    349             setLoadState(LOAD_STATE_LOADING);
    350         }
    351     }
    352 
    353     @Override
    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
    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      */
    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      */
    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 
    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
    485         public void setInnerAlpha(final int alpha) {
    486             super.setInnerAlpha((int) (alpha * mPulseAlphaFraction));
    487         }
    488 
    489         public void setPulseEnabled(boolean enabled) {
    490             mPulseEnabled = enabled;
    491             if (!mPulseEnabled) {
    492                 stopPulsing();
    493             } else {
    494                 startPulsing();
    495             }
    496         }
    497 
    498         private void stopPulsing() {
    499             if (mPulseAnimator != null) {
    500                 mPulseAnimator.cancel();
    501                 mPulseAlphaFraction = 1f;
    502                 setInnerAlpha(getCurrentAlpha());
    503             }
    504         }
    505 
    506         private void startPulsing() {
    507             if (mPulseAnimator != null && !mPulseAnimator.isStarted()) {
    508                 mPulseAnimator.start();
    509             }
    510         }
    511 
    512         @Override
    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 
    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
    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          */
    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          */
    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          */
    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