ExtendedBitmapDrawable.java revision cea0c012d538f11b3ee97d4b7e78f4c1ea73d5be
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.DecodeTask.DecodeOptions; 36import com.android.bitmap.R; 37import com.android.bitmap.BitmapCache; 38import com.android.bitmap.DecodeAggregator; 39import com.android.bitmap.DecodeTask; 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 mTask = new DecodeTask(mCurrKey, opts, this, mCache); 394 mTask.executeOnExecutor(EXECUTOR); 395 Trace.endSection(); 396 } 397 398 private void setLoadState(int loadState) { 399 if (DEBUG) { 400 Log.v(TAG, String.format("IN setLoadState. old=%s new=%s key=%s this=%s", 401 mLoadState, loadState, mCurrKey, this)); 402 } 403 if (mLoadState == loadState) { 404 if (DEBUG) { 405 Log.v(TAG, "OUT no-op setLoadState"); 406 } 407 return; 408 } 409 410 Trace.beginSection("set load state"); 411 switch (loadState) { 412 // This state differs from LOADED in that the subsequent state transition away from 413 // UNINITIALIZED will not have a fancy transition. This allows list item binds to 414 // cached data to take immediate effect without unnecessary whizzery. 415 case LOAD_STATE_UNINITIALIZED: 416 mPlaceholder.reset(); 417 mProgress.reset(); 418 break; 419 case LOAD_STATE_NOT_YET_LOADED: 420 mPlaceholder.setPulseEnabled(true); 421 mPlaceholder.setVisible(true); 422 mProgress.setVisible(false); 423 break; 424 case LOAD_STATE_LOADING: 425 mPlaceholder.setVisible(false); 426 mProgress.setVisible(true); 427 break; 428 case LOAD_STATE_LOADED: 429 mPlaceholder.setVisible(false); 430 mProgress.setVisible(false); 431 break; 432 case LOAD_STATE_FAILED: 433 mPlaceholder.setPulseEnabled(false); 434 mPlaceholder.setVisible(true); 435 mProgress.setVisible(false); 436 break; 437 } 438 Trace.endSection(); 439 440 mLoadState = loadState; 441 boolean placeholderVisible = mPlaceholder != null && mPlaceholder.isVisible(); 442 boolean progressVisible = mProgress != null && mProgress.isVisible(); 443 444 if (DEBUG) { 445 Log.v(TAG, String.format("OUT stateful setLoadState. new=%s placeholder=%s progress=%s", 446 loadState, placeholderVisible, progressVisible)); 447 } 448 } 449 450 @Override 451 public void invalidateDrawable(Drawable who) { 452 invalidateSelf(); 453 } 454 455 @Override 456 public void scheduleDrawable(Drawable who, Runnable what, long when) { 457 scheduleSelf(what, when); 458 } 459 460 @Override 461 public void unscheduleDrawable(Drawable who, Runnable what) { 462 unscheduleSelf(what); 463 } 464 465 private static class Placeholder extends TileDrawable { 466 467 private final ValueAnimator mPulseAnimator; 468 private boolean mPulseEnabled = true; 469 private float mPulseAlphaFraction = 1f; 470 471 public Placeholder(Drawable placeholder, Resources res, 472 int placeholderWidth, int placeholderHeight, int fadeOutDurationMs, 473 int tileColor) { 474 super(placeholder, placeholderWidth, placeholderHeight, tileColor, fadeOutDurationMs); 475 mPulseAnimator = ValueAnimator.ofInt(55, 255) 476 .setDuration(res.getInteger(R.integer.bitmap_placeholder_animation_duration)); 477 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE); 478 mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE); 479 mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() { 480 @Override 481 public void onAnimationUpdate(ValueAnimator animation) { 482 mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f; 483 setInnerAlpha(getCurrentAlpha()); 484 } 485 }); 486 mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 487 @Override 488 public void onAnimationEnd(Animator animation) { 489 stopPulsing(); 490 } 491 }); 492 } 493 494 @Override 495 public void setInnerAlpha(final int alpha) { 496 super.setInnerAlpha((int) (alpha * mPulseAlphaFraction)); 497 } 498 499 public void setPulseEnabled(boolean enabled) { 500 mPulseEnabled = enabled; 501 if (!mPulseEnabled) { 502 stopPulsing(); 503 } 504 } 505 506 private void stopPulsing() { 507 if (mPulseAnimator != null) { 508 mPulseAnimator.cancel(); 509 mPulseAlphaFraction = 1f; 510 setInnerAlpha(getCurrentAlpha()); 511 } 512 } 513 514 @Override 515 public boolean setVisible(boolean visible) { 516 final boolean changed = super.setVisible(visible); 517 if (changed) { 518 if (isVisible()) { 519 // start 520 if (mPulseAnimator != null && mPulseEnabled) { 521 mPulseAnimator.start(); 522 } 523 } else { 524 // can't cancel the pulsing yet-- wait for the fade-out animation to end 525 // one exception: if alpha is already zero, there is no fade-out, so stop now 526 if (getCurrentAlpha() == 0) { 527 stopPulsing(); 528 } 529 } 530 } 531 return changed; 532 } 533 534 } 535 536 private static class Progress extends TileDrawable { 537 538 private final ValueAnimator mRotateAnimator; 539 540 public Progress(Drawable progress, Resources res, 541 int progressBarWidth, int progressBarHeight, int fadeOutDurationMs, 542 int tileColor) { 543 super(progress, progressBarWidth, progressBarHeight, tileColor, fadeOutDurationMs); 544 545 mRotateAnimator = ValueAnimator.ofInt(0, 10000) 546 .setDuration(res.getInteger(R.integer.bitmap_progress_animation_duration)); 547 mRotateAnimator.setInterpolator(new LinearInterpolator()); 548 mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE); 549 mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() { 550 @Override 551 public void onAnimationUpdate(ValueAnimator animation) { 552 setLevel((Integer) animation.getAnimatedValue()); 553 } 554 }); 555 mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 556 @Override 557 public void onAnimationEnd(Animator animation) { 558 if (mRotateAnimator != null) { 559 mRotateAnimator.cancel(); 560 } 561 } 562 }); 563 } 564 565 @Override 566 public boolean setVisible(boolean visible) { 567 final boolean changed = super.setVisible(visible); 568 if (changed) { 569 if (isVisible()) { 570 if (mRotateAnimator != null) { 571 mRotateAnimator.start(); 572 } 573 } else { 574 // can't cancel the rotate yet-- wait for the fade-out animation to end 575 // one exception: if alpha is already zero, there is no fade-out, so stop now 576 if (getCurrentAlpha() == 0 && mRotateAnimator != null) { 577 mRotateAnimator.cancel(); 578 } 579 } 580 } 581 return changed; 582 } 583 584 } 585} 586