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