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