ExtendedBitmapDrawable.java revision 10dddd8a24a80d1d539997d8eaa9763c62bd02ad
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.Rect; 27import android.graphics.drawable.Drawable; 28import android.os.Handler; 29import android.util.Log; 30import android.view.animation.LinearInterpolator; 31 32import com.android.bitmap.BitmapCache; 33import com.android.bitmap.DecodeAggregator; 34import com.android.bitmap.DecodeTask; 35import com.android.bitmap.R; 36import com.android.bitmap.RequestKey; 37import com.android.bitmap.RequestKey.FileDescriptorFactory; 38import com.android.bitmap.ReusableBitmap; 39import 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 * TODO: have this class extend from BasicBitmapDrawable 48 */ 49public class ExtendedBitmapDrawable extends BasicBitmapDrawable implements 50 Runnable, Parallaxable, DecodeAggregator.Callback { 51 52 private final ExtendedOptions mOpts; 53 54 // Parallax. 55 private static final float DECODE_VERTICAL_CENTER = 1f / 3; 56 private float mParallaxFraction = 1f / 2; 57 58 // State changes. 59 private static final int LOAD_STATE_UNINITIALIZED = 0; 60 private static final int LOAD_STATE_NOT_YET_LOADED = 1; 61 private static final int LOAD_STATE_LOADING = 2; 62 private static final int LOAD_STATE_LOADED = 3; 63 private static final int LOAD_STATE_FAILED = 4; 64 private int mLoadState = LOAD_STATE_UNINITIALIZED; 65 private Placeholder mPlaceholder; 66 private Progress mProgress; 67 private int mProgressDelayMs; 68 private final Handler mHandler = new Handler(); 69 70 public static final boolean DEBUG = false; 71 public static final String TAG = ExtendedBitmapDrawable.class.getSimpleName(); 72 73 public ExtendedBitmapDrawable(final Resources res, final BitmapCache cache, 74 final boolean limitDensity, final ExtendedOptions opts) { 75 super(res, cache, limitDensity); 76 77 opts.validate(); 78 mOpts = opts; 79 80 // Placeholder and progress. 81 if ((opts.features & ExtendedOptions.FEATURE_STATE_CHANGES) != 0) { 82 final int fadeOutDurationMs = res.getInteger(R.integer.bitmap_fade_animation_duration); 83 mProgressDelayMs = res.getInteger(R.integer.bitmap_progress_animation_delay); 84 85 // Placeholder is not optional because of backgroundColor. 86 int placeholderSize = res.getDimensionPixelSize(R.dimen.placeholder_size); 87 mPlaceholder = new Placeholder( 88 opts.placeholder != null ? opts.placeholder.getConstantState().newDrawable(res) 89 : null, res, placeholderSize, placeholderSize, fadeOutDurationMs, opts); 90 mPlaceholder.setCallback(this); 91 92 // Progress bar is optional. 93 if (opts.progressBar != null) { 94 int progressBarSize = res.getDimensionPixelSize(R.dimen.progress_bar_size); 95 mProgress = new Progress(opts.progressBar.getConstantState().newDrawable(res), res, 96 progressBarSize, progressBarSize, fadeOutDurationMs, opts); 97 mProgress.setCallback(this); 98 } 99 } 100 } 101 102 @Override 103 public void setParallaxFraction(float fraction) { 104 mParallaxFraction = fraction; 105 invalidateSelf(); 106 } 107 108 /** 109 * Get the ExtendedOptions used to instantiate this ExtendedBitmapDrawable. Any changes made to 110 * the parameters inside the options will take effect immediately. 111 */ 112 public ExtendedOptions getExtendedOptions() { 113 return mOpts; 114 } 115 116 /** 117 * This sets the drawable to the failed state, which remove all animations from the placeholder. 118 * This is different from unbinding to the uninitialized state, where we expect animations. 119 */ 120 public void showStaticPlaceholder() { 121 setLoadState(LOAD_STATE_FAILED); 122 } 123 124 @Override 125 protected void setImage(final RequestKey key) { 126 if (mCurrKey != null && mCurrKey.equals(key)) { 127 return; 128 } 129 130 if (mCurrKey != null && getDecodeAggregator() != null) { 131 getDecodeAggregator().forget(mCurrKey); 132 } 133 134 mHandler.removeCallbacks(this); 135 // start from a clean slate on every bind 136 // this allows the initial transition to be specially instantaneous, so e.g. a cache hit 137 // doesn't unnecessarily trigger a fade-in 138 setLoadState(LOAD_STATE_UNINITIALIZED); 139 if (key == null) { 140 setLoadState(LOAD_STATE_FAILED); 141 } 142 143 super.setImage(key); 144 } 145 146 @Override 147 protected void setBitmap(ReusableBitmap bmp) { 148 setLoadState((bmp != null) ? LOAD_STATE_LOADED : LOAD_STATE_FAILED); 149 150 super.setBitmap(bmp); 151 } 152 153 @Override 154 protected void decode(final FileDescriptorFactory factory) { 155 boolean executeStateChange = shouldExecuteStateChange(); 156 if (executeStateChange) { 157 setLoadState(LOAD_STATE_NOT_YET_LOADED); 158 } 159 160 super.decode(factory); 161 } 162 163 protected boolean shouldExecuteStateChange() { 164 // TODO: AttachmentDrawable should override this method to match prev and curr request keys. 165 return /* opts.stateChanges */ true; 166 } 167 168 @Override 169 public float getDrawVerticalCenter() { 170 return mParallaxFraction; 171 } 172 173 @Override 174 protected float getDrawVerticalOffsetMultiplier() { 175 return mOpts.parallaxSpeedMultiplier; 176 } 177 178 @Override 179 protected float getDecodeVerticalCenter() { 180 return DECODE_VERTICAL_CENTER; 181 } 182 183 private DecodeAggregator getDecodeAggregator() { 184 return mOpts.decodeAggregator; 185 } 186 187 /** 188 * Instead of overriding this method, subclasses should override {@link #onDraw(Canvas)}. 189 * 190 * The reason for this is that we need the placeholder and progress bar to be drawn over our 191 * content. Those two drawables fade out, giving the impression that our content is fading in. 192 */ 193 @Override 194 public final void draw(final Canvas canvas) { 195 final Rect bounds = getBounds(); 196 if (bounds.isEmpty()) { 197 return; 198 } 199 200 onDraw(canvas); 201 202 // Draw the two possible overlay layers in reverse-priority order. 203 // (each layer will no-op the draw when appropriate) 204 // This ordering means cross-fade transitions are just fade-outs of each layer. 205 if (mProgress != null) mProgress.draw(canvas); 206 if (mPlaceholder != null) mPlaceholder.draw(canvas); 207 } 208 209 /** 210 * Overriding this method to add your own custom drawing. 211 */ 212 protected void onDraw(final Canvas canvas) { 213 super.draw(canvas); 214 } 215 216 @Override 217 public void setAlpha(int alpha) { 218 final int old = mPaint.getAlpha(); 219 super.setAlpha(alpha); 220 if (mPlaceholder != null) mPlaceholder.setAlpha(alpha); 221 if (mProgress != null) mProgress.setAlpha(alpha); 222 if (alpha != old) { 223 invalidateSelf(); 224 } 225 } 226 227 @Override 228 public void setColorFilter(ColorFilter cf) { 229 super.setColorFilter(cf); 230 if (mPlaceholder != null) mPlaceholder.setColorFilter(cf); 231 if (mProgress != null) mProgress.setColorFilter(cf); 232 invalidateSelf(); 233 } 234 235 @Override 236 protected void onBoundsChange(Rect bounds) { 237 super.onBoundsChange(bounds); 238 if (mPlaceholder != null) mPlaceholder.setBounds(bounds); 239 if (mProgress != null) mProgress.setBounds(bounds); 240 } 241 242 @Override 243 public void onDecodeBegin(final RequestKey key) { 244 if (getDecodeAggregator() != null) { 245 getDecodeAggregator().expect(key, this); 246 } else { 247 onBecomeFirstExpected(key); 248 } 249 super.onDecodeBegin(key); 250 } 251 252 @Override 253 public void onBecomeFirstExpected(final RequestKey key) { 254 if (!key.equals(mCurrKey)) { 255 return; 256 } 257 // normally, we'd transition to the LOADING state now, but we want to delay that a bit 258 // to minimize excess occurrences of the rotating spinner 259 mHandler.postDelayed(this, mProgressDelayMs); 260 } 261 262 @Override 263 public void run() { 264 if (mLoadState == LOAD_STATE_NOT_YET_LOADED) { 265 setLoadState(LOAD_STATE_LOADING); 266 } 267 } 268 269 @Override 270 public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) { 271 if (getDecodeAggregator() != null) { 272 getDecodeAggregator().execute(key, new Runnable() { 273 @Override 274 public void run() { 275 ExtendedBitmapDrawable.super.onDecodeComplete(key, result); 276 } 277 278 @Override 279 public String toString() { 280 return "DONE"; 281 } 282 }); 283 } else { 284 super.onDecodeComplete(key, result); 285 } 286 } 287 288 @Override 289 public void onDecodeCancel(final RequestKey key) { 290 if (getDecodeAggregator() != null) { 291 getDecodeAggregator().forget(key); 292 } 293 super.onDecodeCancel(key); 294 } 295 296 /** 297 * Each attachment gets its own placeholder and progress indicator, to be shown, hidden, 298 * and animated based on Drawable#setVisible() changes, which are in turn driven by 299 * setLoadState(). 300 */ 301 private void setLoadState(int loadState) { 302 if (DEBUG) { 303 Log.v(TAG, String.format("IN setLoadState. old=%s new=%s key=%s this=%s", 304 mLoadState, loadState, mCurrKey, this)); 305 } 306 if (mLoadState == loadState) { 307 if (DEBUG) { 308 Log.v(TAG, "OUT no-op setLoadState"); 309 } 310 return; 311 } 312 313 Trace.beginSection("set load state"); 314 switch (loadState) { 315 // This state differs from LOADED in that the subsequent state transition away from 316 // UNINITIALIZED will not have a fancy transition. This allows list item binds to 317 // cached data to take immediate effect without unnecessary whizzery. 318 case LOAD_STATE_UNINITIALIZED: 319 if (mPlaceholder != null) mPlaceholder.reset(); 320 if (mProgress != null) mProgress.reset(); 321 break; 322 case LOAD_STATE_NOT_YET_LOADED: 323 if (mPlaceholder != null) { 324 mPlaceholder.setPulseEnabled(true); 325 mPlaceholder.setVisible(true); 326 } 327 if (mProgress != null) mProgress.setVisible(false); 328 break; 329 case LOAD_STATE_LOADING: 330 if (mProgress == null) { 331 // Stay in same visual state as LOAD_STATE_NOT_YET_LOADED. 332 break; 333 } 334 if (mPlaceholder != null) mPlaceholder.setVisible(false); 335 if (mProgress != null) mProgress.setVisible(true); 336 break; 337 case LOAD_STATE_LOADED: 338 if (mPlaceholder != null) mPlaceholder.setVisible(false); 339 if (mProgress != null) mProgress.setVisible(false); 340 break; 341 case LOAD_STATE_FAILED: 342 if (mPlaceholder != null) { 343 mPlaceholder.setPulseEnabled(false); 344 mPlaceholder.setVisible(true); 345 } 346 if (mProgress != null) mProgress.setVisible(false); 347 break; 348 } 349 Trace.endSection(); 350 351 mLoadState = loadState; 352 boolean placeholderVisible = mPlaceholder != null && mPlaceholder.isVisible(); 353 boolean progressVisible = mProgress != null && mProgress.isVisible(); 354 355 if (DEBUG) { 356 Log.v(TAG, String.format("OUT stateful setLoadState. new=%s placeholder=%s progress=%s", 357 loadState, placeholderVisible, progressVisible)); 358 } 359 } 360 361 private static class Placeholder extends TileDrawable { 362 363 private final ValueAnimator mPulseAnimator; 364 private boolean mPulseEnabled = true; 365 private float mPulseAlphaFraction = 1f; 366 367 public Placeholder(Drawable placeholder, Resources res, int placeholderWidth, 368 int placeholderHeight, int fadeOutDurationMs, ExtendedOptions opts) { 369 super(placeholder, placeholderWidth, placeholderHeight, fadeOutDurationMs, opts); 370 371 mPulseAnimator = ValueAnimator.ofInt(55, 255) 372 .setDuration(res.getInteger(R.integer.bitmap_placeholder_animation_duration)); 373 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE); 374 mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE); 375 mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() { 376 @Override 377 public void onAnimationUpdate(ValueAnimator animation) { 378 mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f; 379 setInnerAlpha(getCurrentAlpha()); 380 } 381 }); 382 mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 383 @Override 384 public void onAnimationEnd(Animator animation) { 385 stopPulsing(); 386 } 387 }); 388 } 389 390 @Override 391 public void setInnerAlpha(final int alpha) { 392 super.setInnerAlpha((int) (alpha * mPulseAlphaFraction)); 393 } 394 395 public void setPulseEnabled(boolean enabled) { 396 mPulseEnabled = enabled; 397 if (!mPulseEnabled) { 398 stopPulsing(); 399 } 400 } 401 402 private void stopPulsing() { 403 if (mPulseAnimator != null) { 404 mPulseAnimator.cancel(); 405 mPulseAlphaFraction = 1f; 406 setInnerAlpha(getCurrentAlpha()); 407 } 408 } 409 410 @Override 411 public boolean setVisible(boolean visible) { 412 final boolean changed = super.setVisible(visible); 413 if (changed) { 414 if (isVisible()) { 415 // start 416 if (mPulseAnimator != null && mPulseEnabled) { 417 mPulseAnimator.start(); 418 } 419 } else { 420 // can't cancel the pulsing yet-- wait for the fade-out animation to end 421 // one exception: if alpha is already zero, there is no fade-out, so stop now 422 if (getCurrentAlpha() == 0) { 423 stopPulsing(); 424 } 425 } 426 } 427 return changed; 428 } 429 430 } 431 432 private static class Progress extends TileDrawable { 433 434 private final ValueAnimator mRotateAnimator; 435 436 public Progress(Drawable progress, Resources res, 437 int progressBarWidth, int progressBarHeight, int fadeOutDurationMs, 438 ExtendedOptions opts) { 439 super(progress, progressBarWidth, progressBarHeight, fadeOutDurationMs, opts); 440 441 mRotateAnimator = ValueAnimator.ofInt(0, 10000) 442 .setDuration(res.getInteger(R.integer.bitmap_progress_animation_duration)); 443 mRotateAnimator.setInterpolator(new LinearInterpolator()); 444 mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE); 445 mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() { 446 @Override 447 public void onAnimationUpdate(ValueAnimator animation) { 448 setLevel((Integer) animation.getAnimatedValue()); 449 } 450 }); 451 mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 452 @Override 453 public void onAnimationEnd(Animator animation) { 454 if (mRotateAnimator != null) { 455 mRotateAnimator.cancel(); 456 } 457 } 458 }); 459 } 460 461 @Override 462 public boolean setVisible(boolean visible) { 463 final boolean changed = super.setVisible(visible); 464 if (changed) { 465 if (isVisible()) { 466 if (mRotateAnimator != null) { 467 mRotateAnimator.start(); 468 } 469 } else { 470 // can't cancel the rotate yet-- wait for the fade-out animation to end 471 // one exception: if alpha is already zero, there is no fade-out, so stop now 472 if (getCurrentAlpha() == 0 && mRotateAnimator != null) { 473 mRotateAnimator.cancel(); 474 } 475 } 476 } 477 return changed; 478 } 479 } 480 481 /** 482 * This class contains the features a client can specify, and arguments to those features. 483 * Clients can later retrieve the ExtendedOptions from an ExtendedBitmapDrawable and change the 484 * parameters, which will be reflected immediately. 485 */ 486 public static class ExtendedOptions { 487 488 /** 489 * Summary: 490 * This feature enables you to draw decoded bitmap in order on the screen, to give the 491 * visual effect of a single decode thread. 492 * 493 * <p/> 494 * Explanation: 495 * Since DecodeTasks are asynchronous, multiple tasks may finish decoding at different 496 * times. To have a smooth user experience, provide a shared {@link DecodeAggregator} to all 497 * the ExtendedBitmapDrawables, and the decode aggregator will hold finished decodes so they 498 * come back in order. 499 * 500 * <p/> 501 * Pros: 502 * Visual consistency. Images are not popping up randomly all over the place. 503 * 504 * <p/> 505 * Cons: 506 * Artificial delay. Images are not drawn as soon as they are decoded. They must wait 507 * for their turn. 508 * 509 * <p/> 510 * Requirements: 511 * Set {@link #decodeAggregator} to a shared {@link DecodeAggregator}. 512 */ 513 public static final int FEATURE_ORDERED_DISPLAY = 1; 514 515 /** 516 * Summary: 517 * This feature enables the image to move in parallax as the user scrolls, to give visual 518 * flair to your images. 519 * 520 * <p/> 521 * Explanation: 522 * When the user scrolls D pixels in the vertical direction, this ExtendedBitmapDrawable 523 * shifts its Bitmap f(D) pixels in the vertical direction before drawing to the screen. 524 * Depending on the function f, the parallax effect can give varying interesting results. 525 * 526 * <p/> 527 * Pros: 528 * Visual pop and playfulness. Feeling of movement. Pleasantly surprise your users. 529 * 530 * <p/> 531 * Cons: 532 * Some users report motion sickness with certain speed multiplier values. Decode height 533 * must be greater than visual bounds to account for the parallax. This uses more memory and 534 * decoding time. 535 * 536 * <p/> 537 * Requirements: 538 * Set {@link #parallaxSpeedMultiplier} to the ratio between the decoded height and the 539 * visual bound height. Call {@link ExtendedBitmapDrawable#setDecodeDimensions(int, int)} 540 * with the height multiplied by {@link #parallaxSpeedMultiplier}. 541 * Call {@link ExtendedBitmapDrawable#setParallaxFraction(float)} when the user scrolls. 542 */ 543 public static final int FEATURE_PARALLAX = 1 << 1; 544 545 /** 546 * Summary: 547 * This feature enables fading in between multiple decode states, to give smooth transitions 548 * to and from the placeholder, progress bars, and decoded image. 549 * 550 * <p/> 551 * Explanation: 552 * The states are: {@link ExtendedBitmapDrawable#LOAD_STATE_UNINITIALIZED}, 553 * {@link ExtendedBitmapDrawable#LOAD_STATE_NOT_YET_LOADED}, 554 * {@link ExtendedBitmapDrawable#LOAD_STATE_LOADING}, 555 * {@link ExtendedBitmapDrawable#LOAD_STATE_LOADED}, and 556 * {@link ExtendedBitmapDrawable#LOAD_STATE_FAILED}. These states affect whether the 557 * placeholder and/or the progress bar is showing and animating. We first show the 558 * pulsating placeholder when an image begins decoding. After 2 seconds, we fade in a 559 * spinning progress bar. When the decode completes, we fade in the image. 560 * 561 * <p/> 562 * Pros: 563 * Smooth, beautiful transitions avoid perceived jank. Progress indicator informs users that 564 * work is being done and the app is not stalled. 565 * 566 * <p/> 567 * Cons: 568 * Very fast decodes' short decode time would be eclipsed by the animation duration. Static 569 * placeholder could be accomplished by {@link BasicBitmapDrawable} without the added 570 * complexity of states. 571 * 572 * <p/> 573 * Requirements: 574 * Set {@link #backgroundColor} to the color used for the background of the placeholder and 575 * progress bar. Use the alternative constructor to populate {@link #placeholder} and 576 * {@link #progressBar}. 577 */ 578 public static final int FEATURE_STATE_CHANGES = 1 << 2; 579 580 /** 581 * Non-changeable bit field describing the features you want the 582 * {@link ExtendedBitmapDrawable} to support. 583 * 584 * <p/> 585 * Example: 586 * <code> 587 * opts.features = FEATURE_ORDERED_DISPLAY | FEATURE_PARALLAX | FEATURE_STATE_CHANGES; 588 * </code> 589 */ 590 public final int features; 591 592 /** 593 * Required field if {@link #FEATURE_ORDERED_DISPLAY} is supported. 594 */ 595 public DecodeAggregator decodeAggregator = null; 596 597 /** 598 * Required field if {@link #FEATURE_PARALLAX} is supported. 599 * 600 * A value of 1.5f gives a subtle parallax, and is a good value to 601 * start with. 2.0f gives a more obvious parallax, arguably exaggerated. Some users report 602 * motion sickness with 2.0f. A value of 1.0f is synonymous with no parallax. Be careful not 603 * to set too high a value, since we will start cropping the widths if the image's height is 604 * not sufficient. 605 */ 606 public float parallaxSpeedMultiplier = 1; 607 608 /** 609 * Optional field if {@link #FEATURE_STATE_CHANGES} is supported. 610 * 611 * See {@link android.graphics.Color}. 612 */ 613 public int backgroundColor = 0; 614 615 /** 616 * Optional non-changeable field if {@link #FEATURE_STATE_CHANGES} is supported. 617 */ 618 public final Drawable placeholder; 619 620 /** 621 * Optional non-changeable field if {@link #FEATURE_STATE_CHANGES} is supported. 622 */ 623 public final Drawable progressBar; 624 625 /** 626 * Use this constructor when all the feature parameters are changeable. 627 */ 628 public ExtendedOptions(final int features) { 629 this(features, null, null); 630 } 631 632 /** 633 * Use this constructor when you have to specify non-changeable feature parameters. 634 */ 635 public ExtendedOptions(final int features, final Drawable placeholder, 636 final Drawable progressBar) { 637 this.features = features; 638 this.placeholder = placeholder; 639 this.progressBar = progressBar; 640 } 641 642 /** 643 * Validate this ExtendedOptions instance to make sure that all the required fields are set 644 * for the requested features. 645 * 646 * This will throw an IllegalStateException if validation fails. 647 */ 648 private void validate() 649 throws IllegalStateException { 650 if ((features & FEATURE_ORDERED_DISPLAY) != 0 && decodeAggregator == null) { 651 throw new IllegalStateException( 652 "ExtendedOptions: To support FEATURE_ORDERED_DISPLAY, " 653 + "decodeAggregator must be set."); 654 } 655 if ((features & FEATURE_PARALLAX) != 0 && parallaxSpeedMultiplier == 0) { 656 throw new IllegalStateException( 657 "ExtendedOptions: To support FEATURE_PARALLAX, " 658 + "parallaxSpeedMultiplier must be set."); 659 } 660 if ((features & FEATURE_STATE_CHANGES) != 0 && backgroundColor == 0 661 && placeholder == null) { 662 throw new IllegalStateException( 663 "ExtendedOptions: To support FEATURE_STATE_CHANGES, " 664 + "either backgroundColor or placeholder must be set."); 665 } 666 } 667 } 668} 669