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