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