Home | History | Annotate | Download | only in bitmap
      1 package com.android.mail.bitmap;
      2 
      3 import android.animation.Animator;
      4 import android.animation.AnimatorListenerAdapter;
      5 import android.animation.ValueAnimator;
      6 import android.animation.ValueAnimator.AnimatorUpdateListener;
      7 import android.content.Context;
      8 import android.content.res.Resources;
      9 import android.graphics.Canvas;
     10 import android.graphics.ColorFilter;
     11 import android.graphics.Paint;
     12 import android.graphics.PixelFormat;
     13 import android.graphics.Rect;
     14 import android.graphics.drawable.Drawable;
     15 import android.os.Handler;
     16 import android.util.DisplayMetrics;
     17 import android.view.animation.LinearInterpolator;
     18 
     19 import com.android.bitmap.BitmapCache;
     20 import com.android.bitmap.BitmapUtils;
     21 import com.android.bitmap.DecodeAggregator;
     22 import com.android.bitmap.DecodeTask;
     23 import com.android.bitmap.DecodeTask.Request;
     24 import com.android.bitmap.ReusableBitmap;
     25 import com.android.bitmap.Trace;
     26 import com.android.mail.R;
     27 import com.android.mail.browse.ConversationItemViewCoordinates;
     28 import com.android.mail.ui.SwipeableListView;
     29 import com.android.mail.utils.LogUtils;
     30 import com.android.mail.utils.RectUtils;
     31 
     32 import java.util.concurrent.Executor;
     33 import java.util.concurrent.LinkedBlockingQueue;
     34 import java.util.concurrent.ThreadPoolExecutor;
     35 import java.util.concurrent.TimeUnit;
     36 
     37 /**
     38  * This class encapsulates all functionality needed to display a single image attachment thumbnail,
     39  * including request creation/cancelling, data unbinding and re-binding, and fancy animations
     40  * to draw upon state changes.
     41  * <p>
     42  * The actual bitmap decode work is handled by {@link DecodeTask}.
     43  */
     44 public class AttachmentDrawable extends Drawable implements DecodeTask.BitmapView,
     45         Drawable.Callback, Runnable, Parallaxable, DecodeAggregator.Callback {
     46 
     47     private ImageAttachmentRequest mCurrKey;
     48     private ReusableBitmap mBitmap;
     49     private final BitmapCache mCache;
     50     private final DecodeAggregator mDecodeAggregator;
     51     private DecodeTask mTask;
     52     private int mDecodeWidth;
     53     private int mDecodeHeight;
     54     private int mLoadState = LOAD_STATE_UNINITIALIZED;
     55     private float mParallaxFraction = 0.5f;
     56     private float mParallaxSpeedMultiplier;
     57 
     58     // each attachment gets its own placeholder and progress indicator, to be shown, hidden,
     59     // and animated based on Drawable#setVisible() changes, which are in turn driven by
     60     // #setLoadState().
     61     private Placeholder mPlaceholder;
     62     private Progress mProgress;
     63 
     64     private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(4, 4,
     65             1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
     66 
     67     private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
     68 
     69     private static final boolean LIMIT_BITMAP_DENSITY = true;
     70 
     71     private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH;
     72 
     73     private static final int LOAD_STATE_UNINITIALIZED = 0;
     74     private static final int LOAD_STATE_NOT_YET_LOADED = 1;
     75     private static final int LOAD_STATE_LOADING = 2;
     76     private static final int LOAD_STATE_LOADED = 3;
     77     private static final int LOAD_STATE_FAILED = 4;
     78 
     79     private final ConversationItemViewCoordinates mCoordinates;
     80     private final float mDensity;
     81     private final int mProgressDelayMs;
     82     private final Paint mPaint = new Paint();
     83     private final Rect mSrcRect = new Rect();
     84     private final Handler mHandler = new Handler();
     85 
     86     public final String LOG_TAG = "AttachPreview";
     87 
     88     public AttachmentDrawable(final Resources res, final BitmapCache cache,
     89             final DecodeAggregator decodeAggregator,
     90             final ConversationItemViewCoordinates coordinates, final Drawable placeholder,
     91             final Drawable progress) {
     92         mCoordinates = coordinates;
     93         mDensity = res.getDisplayMetrics().density;
     94         mCache = cache;
     95         this.mDecodeAggregator = decodeAggregator;
     96         mPaint.setFilterBitmap(true);
     97 
     98         final int fadeOutDurationMs = res.getInteger(R.integer.ap_fade_animation_duration);
     99         final int tileColor = res.getColor(R.color.ap_background_color);
    100         mProgressDelayMs = res.getInteger(R.integer.ap_progress_animation_delay);
    101 
    102         mPlaceholder = new Placeholder(placeholder.getConstantState().newDrawable(res), res,
    103                 coordinates, fadeOutDurationMs, tileColor);
    104         mPlaceholder.setCallback(this);
    105 
    106         mProgress = new Progress(progress.getConstantState().newDrawable(res), res,
    107                 coordinates, fadeOutDurationMs, tileColor);
    108         mProgress.setCallback(this);
    109     }
    110 
    111     public DecodeTask.Request getKey() {
    112         return mCurrKey;
    113     }
    114 
    115     public void setDecodeDimensions(int w, int h) {
    116         mDecodeWidth = w;
    117         mDecodeHeight = h;
    118     }
    119 
    120     public void setParallaxSpeedMultiplier(final float parallaxSpeedMultiplier) {
    121         mParallaxSpeedMultiplier = parallaxSpeedMultiplier;
    122     }
    123 
    124     public void showStaticPlaceholder() {
    125         setLoadState(LOAD_STATE_FAILED);
    126     }
    127 
    128     public void unbind() {
    129         setImage(null);
    130     }
    131 
    132     public void bind(Context context, String lookupUri, int rendition) {
    133         final Rect bounds = getBounds();
    134         if (bounds.isEmpty()) {
    135             throw new IllegalStateException("AttachmentDrawable must have bounds set before bind");
    136         }
    137         setImage(new ImageAttachmentRequest(context, lookupUri, rendition, bounds.width()));
    138     }
    139 
    140     private void setImage(final ImageAttachmentRequest key) {
    141         if (mCurrKey != null && mCurrKey.equals(key)) {
    142             return;
    143         }
    144 
    145         Trace.beginSection("set image");
    146         // avoid visual state transitions when the existing request and the new one are just
    147         // requests for different renditions of the same attachment
    148         final boolean onlyRenditionChange = (mCurrKey != null && mCurrKey.matches(key));
    149 
    150         if (mBitmap != null && !onlyRenditionChange) {
    151             mBitmap.releaseReference();
    152 //            System.out.println("view.bind() decremented ref to old bitmap: " + mBitmap);
    153             mBitmap = null;
    154         }
    155         if (mCurrKey != null && SwipeableListView.ENABLE_ATTACHMENT_DECODE_AGGREGATOR) {
    156             mDecodeAggregator.forget(mCurrKey);
    157         }
    158         mCurrKey = key;
    159 
    160         if (mTask != null) {
    161             mTask.cancel();
    162             mTask = null;
    163         }
    164 
    165         mHandler.removeCallbacks(this);
    166         // start from a clean slate on every bind
    167         // this allows the initial transition to be specially instantaneous, so e.g. a cache hit
    168         // doesn't unnecessarily trigger a fade-in
    169         setLoadState(LOAD_STATE_UNINITIALIZED);
    170 
    171         if (key == null) {
    172             Trace.endSection();
    173             return;
    174         }
    175 
    176         // find cached entry here and skip decode if found.
    177         final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */);
    178         if (cached != null) {
    179             setBitmap(cached);
    180             LogUtils.d(LOG_TAG, "CACHE HIT key=%s", mCurrKey);
    181         } else {
    182             decode(!onlyRenditionChange);
    183             if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
    184                 LogUtils.d(LOG_TAG, "CACHE MISS key=%s\ncache=%s",
    185                         mCurrKey, mCache.toDebugString());
    186             }
    187         }
    188         Trace.endSection();
    189     }
    190 
    191     @Override
    192     public void setParallaxFraction(float fraction) {
    193         mParallaxFraction = fraction;
    194     }
    195 
    196     @Override
    197     public void draw(final Canvas canvas) {
    198         final Rect bounds = getBounds();
    199         if (bounds.isEmpty()) {
    200             return;
    201         }
    202 
    203         if (mBitmap != null) {
    204             BitmapUtils
    205                     .calculateCroppedSrcRect(mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
    206                             bounds.width(), bounds.height(),
    207                             mCoordinates.attachmentPreviewsDecodeHeight, Integer.MAX_VALUE,
    208                             mParallaxFraction, false /* absoluteFraction */,
    209                             mParallaxSpeedMultiplier, mSrcRect);
    210 
    211             final int orientation = mBitmap.getOrientation();
    212             // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
    213             // been corrected. We need to decode the uncorrected source rectangle. Calculate true
    214             // coordinates.
    215             RectUtils.rotateRectForOrientation(orientation,
    216                     new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()),
    217                     mSrcRect);
    218 
    219             // We may need to rotate the canvas, so we also have to rotate the bounds.
    220             final Rect rotatedBounds = new Rect(bounds);
    221             RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds);
    222 
    223             // Rotate the canvas.
    224             canvas.save();
    225             canvas.rotate(orientation, bounds.centerX(), bounds.centerY());
    226             canvas.drawBitmap(mBitmap.bmp, mSrcRect, rotatedBounds, mPaint);
    227             canvas.restore();
    228         }
    229 
    230         // Draw the two possible overlay layers in reverse-priority order.
    231         // (each layer will no-op the draw when appropriate)
    232         // This ordering means cross-fade transitions are just fade-outs of each layer.
    233         mProgress.draw(canvas);
    234         mPlaceholder.draw(canvas);
    235     }
    236 
    237     @Override
    238     public void setAlpha(int alpha) {
    239         final int old = mPaint.getAlpha();
    240         mPaint.setAlpha(alpha);
    241         mPlaceholder.setAlpha(alpha);
    242         mProgress.setAlpha(alpha);
    243         if (alpha != old) {
    244             invalidateSelf();
    245         }
    246     }
    247 
    248     @Override
    249     public void setColorFilter(ColorFilter cf) {
    250         mPaint.setColorFilter(cf);
    251         mPlaceholder.setColorFilter(cf);
    252         mProgress.setColorFilter(cf);
    253         invalidateSelf();
    254     }
    255 
    256     @Override
    257     public int getOpacity() {
    258         return (mBitmap != null && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ?
    259                 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
    260     }
    261 
    262     @Override
    263     protected void onBoundsChange(Rect bounds) {
    264         super.onBoundsChange(bounds);
    265 
    266         mPlaceholder.setBounds(bounds);
    267         mProgress.setBounds(bounds);
    268     }
    269 
    270     @Override
    271     public void onDecodeBegin(final Request key) {
    272         if (SwipeableListView.ENABLE_ATTACHMENT_DECODE_AGGREGATOR) {
    273             mDecodeAggregator.expect(key, this);
    274         } else {
    275             onBecomeFirstExpected(key);
    276         }
    277     }
    278 
    279     @Override
    280     public void onBecomeFirstExpected(final Request key) {
    281         if (!key.equals(mCurrKey)) {
    282             return;
    283         }
    284         // normally, we'd transition to the LOADING state now, but we want to delay that a bit
    285         // to minimize excess occurrences of the rotating spinner
    286         mHandler.postDelayed(this, mProgressDelayMs);
    287     }
    288 
    289     @Override
    290     public void run() {
    291         if (mLoadState == LOAD_STATE_NOT_YET_LOADED) {
    292             setLoadState(LOAD_STATE_LOADING);
    293         }
    294     }
    295 
    296     @Override
    297     public void onDecodeComplete(final Request key, final ReusableBitmap result) {
    298         if (SwipeableListView.ENABLE_ATTACHMENT_DECODE_AGGREGATOR) {
    299             mDecodeAggregator.execute(key, new Runnable() {
    300                 @Override
    301                 public void run() {
    302                     onDecodeCompleteImpl(key, result);
    303                 }
    304 
    305                 @Override
    306                 public String toString() {
    307                     return "DONE";
    308                 }
    309             });
    310         } else {
    311             onDecodeCompleteImpl(key, result);
    312         }
    313     }
    314 
    315     private void onDecodeCompleteImpl(final Request key, final ReusableBitmap result) {
    316         if (key.equals(mCurrKey)) {
    317             setBitmap(result);
    318         } else {
    319             // if the requests don't match (i.e. this request is stale), decrement the
    320             // ref count to allow the bitmap to be pooled
    321             if (result != null) {
    322                 result.releaseReference();
    323             }
    324         }
    325     }
    326 
    327     @Override
    328     public void onDecodeCancel(final Request key) {
    329         if (SwipeableListView.ENABLE_ATTACHMENT_DECODE_AGGREGATOR) {
    330             mDecodeAggregator.forget(key);
    331         }
    332     }
    333 
    334     private void setBitmap(ReusableBitmap bmp) {
    335         if (mBitmap != null && mBitmap != bmp) {
    336             mBitmap.releaseReference();
    337         }
    338         mBitmap = bmp;
    339         setLoadState((bmp != null) ? LOAD_STATE_LOADED : LOAD_STATE_FAILED);
    340         invalidateSelf();
    341     }
    342 
    343     private void decode(boolean executeStateChange) {
    344         final int w;
    345         final int bufferW;
    346         final int bufferH;
    347 
    348         if (mCurrKey == null) {
    349             return;
    350         }
    351 
    352         Trace.beginSection("decode");
    353         if (LIMIT_BITMAP_DENSITY) {
    354             final float scale =
    355                     Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT
    356                             / mDensity);
    357             w = (int) (mCurrKey.mDestW * scale);
    358             bufferW = (int) (mDecodeWidth * scale);
    359             bufferH = (int) (mDecodeHeight * scale);
    360         } else {
    361             w = mCurrKey.mDestW;
    362             bufferW = mDecodeWidth;
    363             bufferH = mDecodeHeight;
    364         }
    365 
    366         if (w == 0 || bufferH == 0) {
    367             Trace.endSection();
    368             return;
    369         }
    370 //        System.out.println("ITEM " + this + " w=" + w + " h=" + bufferH + " key=" + mCurrKey);
    371         if (mTask != null) {
    372             mTask.cancel();
    373         }
    374         if (executeStateChange) {
    375             setLoadState(LOAD_STATE_NOT_YET_LOADED);
    376         }
    377         mTask = new DecodeTask(mCurrKey, w, bufferH, bufferW, bufferH, this, mCache);
    378         mTask.executeOnExecutor(EXECUTOR);
    379         Trace.endSection();
    380     }
    381 
    382     private void setLoadState(int loadState) {
    383         LogUtils.v(LOG_TAG, "IN AD.setState. old=%s new=%s key=%s this=%s", mLoadState, loadState,
    384                 mCurrKey, this);
    385         if (mLoadState == loadState) {
    386             LogUtils.v(LOG_TAG, "OUT no-op AD.setState");
    387             return;
    388         }
    389 
    390         Trace.beginSection("set load state");
    391         switch (loadState) {
    392             // This state differs from LOADED in that the subsequent state transition away from
    393             // UNINITIALIZED will not have a fancy transition. This allows list item binds to
    394             // cached data to take immediate effect without unnecessary whizzery.
    395             case LOAD_STATE_UNINITIALIZED:
    396                 mPlaceholder.reset();
    397                 mProgress.reset();
    398                 break;
    399             case LOAD_STATE_NOT_YET_LOADED:
    400                 mPlaceholder.setPulseEnabled(true);
    401                 mPlaceholder.setVisible(true);
    402                 mProgress.setVisible(false);
    403                 break;
    404             case LOAD_STATE_LOADING:
    405                 mPlaceholder.setVisible(false);
    406                 mProgress.setVisible(true);
    407                 break;
    408             case LOAD_STATE_LOADED:
    409                 mPlaceholder.setVisible(false);
    410                 mProgress.setVisible(false);
    411                 break;
    412             case LOAD_STATE_FAILED:
    413                 mPlaceholder.setPulseEnabled(false);
    414                 mPlaceholder.setVisible(true);
    415                 mProgress.setVisible(false);
    416                 break;
    417         }
    418         Trace.endSection();
    419 
    420         mLoadState = loadState;
    421         LogUtils.v(LOG_TAG, "OUT stateful AD.setState. new=%s placeholder=%s progress=%s",
    422                 loadState, mPlaceholder.isVisible(), mProgress.isVisible());
    423     }
    424 
    425     @Override
    426     public void invalidateDrawable(Drawable who) {
    427         invalidateSelf();
    428     }
    429 
    430     @Override
    431     public void scheduleDrawable(Drawable who, Runnable what, long when) {
    432         scheduleSelf(what, when);
    433     }
    434 
    435     @Override
    436     public void unscheduleDrawable(Drawable who, Runnable what) {
    437         unscheduleSelf(what);
    438     }
    439 
    440     private static class Placeholder extends TileDrawable {
    441 
    442         private final ValueAnimator mPulseAnimator;
    443         private boolean mPulseEnabled = true;
    444         private float mPulseAlphaFraction = 1f;
    445 
    446         public Placeholder(Drawable placeholder, Resources res,
    447                 ConversationItemViewCoordinates coordinates, int fadeOutDurationMs,
    448                 int tileColor) {
    449             super(placeholder, coordinates.placeholderWidth, coordinates.placeholderHeight,
    450                     tileColor, fadeOutDurationMs);
    451             mPulseAnimator = ValueAnimator.ofInt(55, 255)
    452                     .setDuration(res.getInteger(R.integer.ap_placeholder_animation_duration));
    453             mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
    454             mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE);
    455             mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() {
    456                 @Override
    457                 public void onAnimationUpdate(ValueAnimator animation) {
    458                     mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f;
    459                     setInnerAlpha(getCurrentAlpha());
    460                 }
    461             });
    462             mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
    463                 @Override
    464                 public void onAnimationEnd(Animator animation) {
    465                     stopPulsing();
    466                 }
    467             });
    468         }
    469 
    470         @Override
    471         public void setInnerAlpha(final int alpha) {
    472             super.setInnerAlpha((int) (alpha * mPulseAlphaFraction));
    473         }
    474 
    475         public void setPulseEnabled(boolean enabled) {
    476             mPulseEnabled = enabled;
    477             if (!mPulseEnabled) {
    478                 stopPulsing();
    479             }
    480         }
    481 
    482         private void stopPulsing() {
    483             if (mPulseAnimator != null) {
    484                 mPulseAnimator.cancel();
    485                 mPulseAlphaFraction = 1f;
    486                 setInnerAlpha(getCurrentAlpha());
    487             }
    488         }
    489 
    490         @Override
    491         public boolean setVisible(boolean visible) {
    492             final boolean changed = super.setVisible(visible);
    493             if (changed) {
    494                 if (isVisible()) {
    495                     // start
    496                     if (mPulseAnimator != null && mPulseEnabled) {
    497                         mPulseAnimator.start();
    498                     }
    499                 } else {
    500                     // can't cancel the pulsing yet-- wait for the fade-out animation to end
    501                     // one exception: if alpha is already zero, there is no fade-out, so stop now
    502                     if (getCurrentAlpha() == 0) {
    503                         stopPulsing();
    504                     }
    505                 }
    506             }
    507             return changed;
    508         }
    509 
    510     }
    511 
    512     private static class Progress extends TileDrawable {
    513 
    514         private final ValueAnimator mRotateAnimator;
    515 
    516         public Progress(Drawable progress, Resources res,
    517                 ConversationItemViewCoordinates coordinates, int fadeOutDurationMs,
    518                 int tileColor) {
    519             super(progress, coordinates.progressBarWidth, coordinates.progressBarHeight,
    520                     tileColor, fadeOutDurationMs);
    521 
    522             mRotateAnimator = ValueAnimator.ofInt(0, 10000)
    523                     .setDuration(res.getInteger(R.integer.ap_progress_animation_duration));
    524             mRotateAnimator.setInterpolator(new LinearInterpolator());
    525             mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
    526             mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() {
    527                 @Override
    528                 public void onAnimationUpdate(ValueAnimator animation) {
    529                     setLevel((Integer) animation.getAnimatedValue());
    530                 }
    531             });
    532             mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
    533                 @Override
    534                 public void onAnimationEnd(Animator animation) {
    535                     if (mRotateAnimator != null) {
    536                         mRotateAnimator.cancel();
    537                     }
    538                 }
    539             });
    540         }
    541 
    542         @Override
    543         public boolean setVisible(boolean visible) {
    544             final boolean changed = super.setVisible(visible);
    545             if (changed) {
    546                 if (isVisible()) {
    547                     if (mRotateAnimator != null) {
    548                         mRotateAnimator.start();
    549                     }
    550                 } else {
    551                     // can't cancel the rotate yet-- wait for the fade-out animation to end
    552                     // one exception: if alpha is already zero, there is no fade-out, so stop now
    553                     if (getCurrentAlpha() == 0 && mRotateAnimator != null) {
    554                         mRotateAnimator.cancel();
    555                     }
    556                 }
    557             }
    558             return changed;
    559         }
    560 
    561     }
    562 }
    563