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