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