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