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