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