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