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