blob: fc544196cddbc7f1e0b0b35ff1f7bb11fa8430e7 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bitmap.drawable;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.Log;
import android.view.animation.LinearInterpolator;
import com.android.bitmap.BitmapCache;
import com.android.bitmap.DecodeAggregator;
import com.android.bitmap.DecodeTask;
import com.android.bitmap.R;
import com.android.bitmap.RequestKey;
import com.android.bitmap.ReusableBitmap;
import com.android.bitmap.util.Trace;
/**
* This class encapsulates all functionality needed to display a single image bitmap,
* including request creation/cancelling, data unbinding and re-binding, and fancy animations
* to draw upon state changes.
* <p>
* The actual bitmap decode work is handled by {@link DecodeTask}.
*/
public class ExtendedBitmapDrawable extends BasicBitmapDrawable implements
Runnable, Parallaxable, DecodeAggregator.Callback {
public static final int LOAD_STATE_UNINITIALIZED = 0;
public static final int LOAD_STATE_NOT_YET_LOADED = 1;
public static final int LOAD_STATE_LOADING = 2;
public static final int LOAD_STATE_LOADED = 3;
public static final int LOAD_STATE_FAILED = 4;
public static final boolean DEBUG = false;
private static final String TAG = ExtendedBitmapDrawable.class.getSimpleName();
private final Resources mResources;
private final ExtendedOptions mOpts;
// Parallax.
private float mParallaxFraction = 1f / 2;
// State changes.
private int mLoadState = LOAD_STATE_UNINITIALIZED;
private Placeholder mPlaceholder;
private Progress mProgress;
private int mProgressDelayMs;
private final Handler mHandler = new Handler();
public ExtendedBitmapDrawable(final Resources res, final BitmapCache cache,
final boolean limitDensity, ExtendedOptions opts) {
super(res, cache, limitDensity);
mResources = res;
if (opts == null) {
opts = new ExtendedOptions(0);
}
mOpts = opts;
onOptsChanged();
}
/**
* Called after a field is changed in an {@link ExtendedOptions}, if that field requests this
* method to be called.
*/
public void onOptsChanged() {
mOpts.validate();
// Placeholder and progress.
if ((mOpts.features & ExtendedOptions.FEATURE_STATE_CHANGES) != 0) {
final int fadeOutDurationMs = mResources.getInteger(R.integer.bitmap_fade_animation_duration);
mProgressDelayMs = mResources.getInteger(R.integer.bitmap_progress_animation_delay);
// Placeholder is not optional because backgroundColor is part of it.
Drawable placeholder = null;
int placeholderWidth = mResources.getDimensionPixelSize(R.dimen.placeholder_size);
int placeholderHeight = mResources.getDimensionPixelSize(R.dimen.placeholder_size);
if (mOpts.placeholder != null) {
ConstantState constantState = mOpts.placeholder.getConstantState();
if (constantState != null) {
placeholder = constantState.newDrawable(mResources);
} else {
placeholder = mOpts.placeholder;
}
Rect bounds = mOpts.placeholder.getBounds();
if (bounds.width() != 0) {
placeholderWidth = bounds.width();
} else if (placeholder.getIntrinsicWidth() != -1) {
placeholderWidth = placeholder.getIntrinsicWidth();
}
if (bounds.height() != 0) {
placeholderHeight = bounds.height();
} else if (placeholder.getIntrinsicHeight() != -1) {
placeholderHeight = placeholder.getIntrinsicHeight();
}
}
mPlaceholder = new Placeholder(placeholder, mResources, placeholderWidth, placeholderHeight,
fadeOutDurationMs, mOpts);
mPlaceholder.setCallback(this);
mPlaceholder.setBounds(getBounds());
// Progress bar is optional.
if (mOpts.progressBar != null) {
int progressBarSize = mResources.getDimensionPixelSize(R.dimen.progress_bar_size);
mProgress = new Progress(mOpts.progressBar.getConstantState().newDrawable(mResources), mResources,
progressBarSize, progressBarSize, fadeOutDurationMs, mOpts);
mProgress.setCallback(this);
mProgress.setBounds(getBounds());
} else {
mProgress = null;
}
}
setLoadState(mLoadState);
}
@Override
public void setParallaxFraction(float fraction) {
mParallaxFraction = fraction;
invalidateSelf();
}
/**
* Get the ExtendedOptions used to instantiate this ExtendedBitmapDrawable. Any changes made to
* the parameters inside the options will take effect immediately.
*/
public ExtendedOptions getExtendedOptions() {
return mOpts;
}
/**
* This sets the drawable to the failed state, which remove all animations from the placeholder.
* This is different from unbinding to the uninitialized state, where we expect animations.
*/
public void showStaticPlaceholder() {
setLoadState(LOAD_STATE_FAILED);
}
/**
* Directly sets the decode width and height. The given height should already have had the
* parallaxSpeedMultiplier applied to it.
*/
public void setExactDecodeDimensions(int width, int height) {
super.setDecodeDimensions(width, height);
}
/**
* {@inheritDoc}
*
* The given height should not have had the parallaxSpeedMultiplier applied to it.
*/
@Override
public void setDecodeDimensions(int width, int height) {
super.setDecodeDimensions(width, (int) (height * mOpts.parallaxSpeedMultiplier));
}
@Override
protected void setImage(final RequestKey key) {
if (mCurrKey != null && getDecodeAggregator() != null) {
getDecodeAggregator().forget(mCurrKey);
}
mHandler.removeCallbacks(this);
// start from a clean slate on every bind
// this allows the initial transition to be specially instantaneous, so e.g. a cache hit
// doesn't unnecessarily trigger a fade-in
setLoadState(LOAD_STATE_UNINITIALIZED);
super.setImage(key);
if (key == null) {
showStaticPlaceholder();
}
}
@Override
protected void setBitmap(ReusableBitmap bmp) {
if (bmp != null) {
setLoadState(LOAD_STATE_LOADED);
} else {
onDecodeFailed();
}
super.setBitmap(bmp);
}
@Override
protected void loadFileDescriptorFactory() {
boolean executeStateChange = shouldExecuteStateChange();
if (mCurrKey == null || mDecodeWidth == 0 || mDecodeHeight == 0) {
return;
}
if (executeStateChange) {
setLoadState(LOAD_STATE_NOT_YET_LOADED);
}
super.loadFileDescriptorFactory();
}
@Override
protected void onDecodeFailed() {
super.onDecodeFailed();
setLoadState(LOAD_STATE_FAILED);
}
protected boolean shouldExecuteStateChange() {
// TODO: AttachmentDrawable should override this method to match prev and curr request keys.
return /* opts.stateChanges */ true;
}
@Override
public float getDrawVerticalCenter() {
return mParallaxFraction;
}
@Override
protected final float getDrawVerticalOffsetMultiplier() {
return mOpts.parallaxSpeedMultiplier;
}
@Override
protected float getDecodeHorizontalCenter() {
return mOpts.decodeHorizontalCenter;
}
@Override
protected float getDecodeVerticalCenter() {
return mOpts.decodeVerticalCenter;
}
private DecodeAggregator getDecodeAggregator() {
return mOpts.decodeAggregator;
}
/**
* Instead of overriding this method, subclasses should override {@link #onDraw(Canvas)}.
*
* The reason for this is that we need the placeholder and progress bar to be drawn over our
* content. Those two drawables fade out, giving the impression that our content is fading in.
*
* Only override this method for custom drawings on top of all the drawable layers.
*/
@Override
public void draw(final Canvas canvas) {
final Rect bounds = getBounds();
if (bounds.isEmpty()) {
return;
}
onDraw(canvas);
// Draw the two possible overlay layers in reverse-priority order.
// (each layer will no-op the draw when appropriate)
// This ordering means cross-fade transitions are just fade-outs of each layer.
if (mProgress != null) onDrawPlaceholderOrProgress(canvas, mProgress);
if (mPlaceholder != null) onDrawPlaceholderOrProgress(canvas, mPlaceholder);
}
/**
* Overriding this method to add your own custom drawing.
*/
protected void onDraw(final Canvas canvas) {
super.draw(canvas);
}
/**
* Overriding this method to add your own custom placeholder or progress drawing.
*/
protected void onDrawPlaceholderOrProgress(final Canvas canvas, final TileDrawable drawable) {
drawable.draw(canvas);
}
@Override
public void setAlpha(int alpha) {
final int old = mPaint.getAlpha();
super.setAlpha(alpha);
if (mPlaceholder != null) mPlaceholder.setAlpha(alpha);
if (mProgress != null) mProgress.setAlpha(alpha);
if (alpha != old) {
invalidateSelf();
}
}
@Override
public void setColorFilter(ColorFilter cf) {
super.setColorFilter(cf);
if (mPlaceholder != null) mPlaceholder.setColorFilter(cf);
if (mProgress != null) mProgress.setColorFilter(cf);
invalidateSelf();
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
if (mPlaceholder != null) mPlaceholder.setBounds(bounds);
if (mProgress != null) mProgress.setBounds(bounds);
}
@Override
public void onDecodeBegin(final RequestKey key) {
if (getDecodeAggregator() != null) {
getDecodeAggregator().expect(key, this);
} else {
onBecomeFirstExpected(key);
}
super.onDecodeBegin(key);
}
@Override
public void onBecomeFirstExpected(final RequestKey key) {
if (!key.equals(mCurrKey)) {
return;
}
// normally, we'd transition to the LOADING state now, but we want to delay that a bit
// to minimize excess occurrences of the rotating spinner
mHandler.postDelayed(this, mProgressDelayMs);
}
@Override
public void run() {
if (mLoadState == LOAD_STATE_NOT_YET_LOADED) {
setLoadState(LOAD_STATE_LOADING);
}
}
@Override
public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) {
if (getDecodeAggregator() != null) {
getDecodeAggregator().execute(key, new Runnable() {
@Override
public void run() {
ExtendedBitmapDrawable.super.onDecodeComplete(key, result);
}
@Override
public String toString() {
return "DONE";
}
});
} else {
super.onDecodeComplete(key, result);
}
}
@Override
public void onDecodeCancel(final RequestKey key) {
if (getDecodeAggregator() != null) {
getDecodeAggregator().forget(key);
}
super.onDecodeCancel(key);
}
/**
* Get the load state of this drawable. Return one of the LOAD_STATE constants.
*/
public int getLoadState() {
return mLoadState;
}
/**
* Each attachment gets its own placeholder and progress indicator, to be shown, hidden,
* and animated based on Drawable#setVisible() changes, which are in turn driven by
* setLoadState().
*/
private void setLoadState(int loadState) {
if (DEBUG) {
Log.v(TAG, String.format("IN setLoadState. old=%s new=%s key=%s this=%s",
mLoadState, loadState, mCurrKey, this));
}
Trace.beginSection("set load state");
switch (loadState) {
// This state differs from LOADED in that the subsequent state transition away from
// UNINITIALIZED will not have a fancy transition. This allows list item binds to
// cached data to take immediate effect without unnecessary whizzery.
case LOAD_STATE_UNINITIALIZED:
if (mPlaceholder != null) mPlaceholder.reset();
if (mProgress != null) mProgress.reset();
break;
case LOAD_STATE_NOT_YET_LOADED:
if (mPlaceholder != null) {
mPlaceholder.setPulseEnabled(true);
mPlaceholder.setVisible(true);
}
if (mProgress != null) mProgress.setVisible(false);
break;
case LOAD_STATE_LOADING:
if (mProgress == null) {
// Stay in same visual state as LOAD_STATE_NOT_YET_LOADED.
break;
}
if (mPlaceholder != null) mPlaceholder.setVisible(false);
if (mProgress != null) mProgress.setVisible(true);
break;
case LOAD_STATE_LOADED:
if (mPlaceholder != null) mPlaceholder.setVisible(false);
if (mProgress != null) mProgress.setVisible(false);
break;
case LOAD_STATE_FAILED:
if (mPlaceholder != null) {
mPlaceholder.setPulseEnabled(false);
mPlaceholder.setVisible(true);
}
if (mProgress != null) mProgress.setVisible(false);
break;
}
Trace.endSection();
mLoadState = loadState;
boolean placeholderVisible = mPlaceholder != null && mPlaceholder.isVisible();
boolean progressVisible = mProgress != null && mProgress.isVisible();
if (DEBUG) {
Log.v(TAG, String.format("OUT stateful setLoadState. new=%s placeholder=%s progress=%s",
loadState, placeholderVisible, progressVisible));
}
}
private static class Placeholder extends TileDrawable {
private final ValueAnimator mPulseAnimator;
private boolean mPulseEnabled = true;
private float mPulseAlphaFraction = 1f;
public Placeholder(Drawable placeholder, Resources res, int placeholderWidth,
int placeholderHeight, int fadeOutDurationMs, ExtendedOptions opts) {
super(placeholder, placeholderWidth, placeholderHeight, fadeOutDurationMs, opts);
if (opts.placeholderAnimationDuration == -1) {
mPulseAnimator = null;
} else {
final long pulseDuration;
if (opts.placeholderAnimationDuration == 0) {
pulseDuration = res.getInteger(R.integer.bitmap_placeholder_animation_duration);
} else {
pulseDuration = opts.placeholderAnimationDuration;
}
mPulseAnimator = ValueAnimator.ofInt(55, 255).setDuration(pulseDuration);
mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE);
mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f;
setInnerAlpha(getCurrentAlpha());
}
});
}
mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
stopPulsing();
}
});
}
@Override
public void setInnerAlpha(final int alpha) {
super.setInnerAlpha((int) (alpha * mPulseAlphaFraction));
}
public void setPulseEnabled(boolean enabled) {
mPulseEnabled = enabled;
if (!mPulseEnabled) {
stopPulsing();
} else {
startPulsing();
}
}
private void stopPulsing() {
if (mPulseAnimator != null) {
mPulseAnimator.cancel();
mPulseAlphaFraction = 1f;
setInnerAlpha(getCurrentAlpha());
}
}
private void startPulsing() {
if (mPulseAnimator != null && !mPulseAnimator.isStarted()) {
mPulseAnimator.start();
}
}
@Override
public boolean setVisible(boolean visible) {
final boolean changed = super.setVisible(visible);
if (changed) {
if (isVisible()) {
// start
if (mPulseAnimator != null && mPulseEnabled && !mPulseAnimator.isStarted()) {
mPulseAnimator.start();
}
} else {
// can't cancel the pulsing yet-- wait for the fade-out animation to end
// one exception: if alpha is already zero, there is no fade-out, so stop now
if (getCurrentAlpha() == 0) {
stopPulsing();
}
}
}
return changed;
}
}
private static class Progress extends TileDrawable {
private final ValueAnimator mRotateAnimator;
public Progress(Drawable progress, Resources res,
int progressBarWidth, int progressBarHeight, int fadeOutDurationMs,
ExtendedOptions opts) {
super(progress, progressBarWidth, progressBarHeight, fadeOutDurationMs, opts);
mRotateAnimator = ValueAnimator.ofInt(0, 10000)
.setDuration(res.getInteger(R.integer.bitmap_progress_animation_duration));
mRotateAnimator.setInterpolator(new LinearInterpolator());
mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setLevel((Integer) animation.getAnimatedValue());
}
});
mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mRotateAnimator != null) {
mRotateAnimator.cancel();
}
}
});
}
@Override
public boolean setVisible(boolean visible) {
final boolean changed = super.setVisible(visible);
if (changed) {
if (isVisible()) {
if (mRotateAnimator != null) {
mRotateAnimator.start();
}
} else {
// can't cancel the rotate yet-- wait for the fade-out animation to end
// one exception: if alpha is already zero, there is no fade-out, so stop now
if (getCurrentAlpha() == 0 && mRotateAnimator != null) {
mRotateAnimator.cancel();
}
}
}
return changed;
}
}
/**
* This class contains the features a client can specify, and arguments to those features.
* Clients can later retrieve the ExtendedOptions from an ExtendedBitmapDrawable and change the
* parameters, which will be reflected immediately.
*/
public static class ExtendedOptions {
/**
* Summary:
* This feature enables you to draw decoded bitmap in order on the screen, to give the
* visual effect of a single decode thread.
*
* <p/>
* Explanation:
* Since DecodeTasks are asynchronous, multiple tasks may finish decoding at different
* times. To have a smooth user experience, provide a shared {@link DecodeAggregator} to all
* the ExtendedBitmapDrawables, and the decode aggregator will hold finished decodes so they
* come back in order.
*
* <p/>
* Pros:
* Visual consistency. Images are not popping up randomly all over the place.
*
* <p/>
* Cons:
* Artificial delay. Images are not drawn as soon as they are decoded. They must wait
* for their turn.
*
* <p/>
* Requirements:
* Set {@link #decodeAggregator} to a shared {@link DecodeAggregator}.
*/
public static final int FEATURE_ORDERED_DISPLAY = 1;
/**
* Summary:
* This feature enables the image to move in parallax as the user scrolls, to give visual
* flair to your images.
*
* <p/>
* Explanation:
* When the user scrolls D pixels in the vertical direction, this ExtendedBitmapDrawable
* shifts its Bitmap f(D) pixels in the vertical direction before drawing to the screen.
* Depending on the function f, the parallax effect can give varying interesting results.
*
* <p/>
* Pros:
* Visual pop and playfulness. Feeling of movement. Pleasantly surprise your users.
*
* <p/>
* Cons:
* Some users report motion sickness with certain speed multiplier values. Decode height
* must be greater than visual bounds to account for the parallax. This uses more memory and
* decoding time.
*
* <p/>
* Requirements:
* Set {@link #parallaxSpeedMultiplier} to the ratio between the decoded height and the
* visual bound height. Call {@link ExtendedBitmapDrawable#setDecodeDimensions(int, int)}
* with the height multiplied by {@link #parallaxSpeedMultiplier}.
* Call {@link ExtendedBitmapDrawable#setParallaxFraction(float)} when the user scrolls.
*/
public static final int FEATURE_PARALLAX = 1 << 1;
/**
* Summary:
* This feature enables fading in between multiple decode states, to give smooth transitions
* to and from the placeholder, progress bars, and decoded image.
*
* <p/>
* Explanation:
* The states are: {@link ExtendedBitmapDrawable#LOAD_STATE_UNINITIALIZED},
* {@link ExtendedBitmapDrawable#LOAD_STATE_NOT_YET_LOADED},
* {@link ExtendedBitmapDrawable#LOAD_STATE_LOADING},
* {@link ExtendedBitmapDrawable#LOAD_STATE_LOADED}, and
* {@link ExtendedBitmapDrawable#LOAD_STATE_FAILED}. These states affect whether the
* placeholder and/or the progress bar is showing and animating. We first show the
* pulsating placeholder when an image begins decoding. After 2 seconds, we fade in a
* spinning progress bar. When the decode completes, we fade in the image.
*
* <p/>
* Pros:
* Smooth, beautiful transitions avoid perceived jank. Progress indicator informs users that
* work is being done and the app is not stalled.
*
* <p/>
* Cons:
* Very fast decodes' short decode time would be eclipsed by the animation duration. Static
* placeholder could be accomplished by {@link BasicBitmapDrawable} without the added
* complexity of states.
*
* <p/>
* Requirements:
* Set {@link #backgroundColor} to the color used for the background of the placeholder and
* progress bar. Use the alternative constructor to populate {@link #placeholder} and
* {@link #progressBar}. Optionally set {@link #placeholderAnimationDuration}.
*/
public static final int FEATURE_STATE_CHANGES = 1 << 2;
/**
* Non-changeable bit field describing the features you want the
* {@link ExtendedBitmapDrawable} to support.
*
* <p/>
* Example:
* <code>
* opts.features = FEATURE_ORDERED_DISPLAY | FEATURE_PARALLAX | FEATURE_STATE_CHANGES;
* </code>
*/
public final int features;
/**
* Optional field for general decoding.
*
* This field determines which section of the source image to decode from. A value of 0
* indicates a preference for the far left of the source, while a value of 1 indicates a
* preference for the far right of the source. A value of .5 will result in the center
* of the source being decoded.
*/
public float decodeHorizontalCenter = 1f / 2;
/**
* Optional field for general decoding.
*
* This field determines which section of the source image to decode from. A value of 0
* indicates a preference for the very top of the source, while a value of 1 indicates a
* preference for the very bottom of the source. A value of .5 will result in the center
* of the source being decoded.
*
* This should not be confused with {@link #setParallaxFraction(float)}. This field
* determines the general section for decode. The parallax fraction then determines the
* slice from within that section for display.
*
* The default value of 1f / 3 provides a good heuristic for the subject's face in a
* portrait photo.
*/
public float decodeVerticalCenter = 1f / 3;
/**
* Required field if {@link #FEATURE_ORDERED_DISPLAY} is supported.
*/
public DecodeAggregator decodeAggregator = null;
/**
* Required field if {@link #FEATURE_PARALLAX} is supported.
*
* A value of 1.5f gives a subtle parallax, and is a good value to
* start with. 2.0f gives a more obvious parallax, arguably exaggerated. Some users report
* motion sickness with 2.0f. A value of 1.0f is synonymous with no parallax. Be careful not
* to set too high a value, since we will start cropping the widths if the image's height is
* not sufficient.
*/
public float parallaxSpeedMultiplier = 1;
/**
* Optional field if {@link #FEATURE_STATE_CHANGES} is supported. Must be an opaque color.
*
* See {@link android.graphics.Color}.
*/
public int backgroundColor = 0;
/**
* Optional field if {@link #FEATURE_STATE_CHANGES} is supported.
*
* If you modify this field you must call
* {@link ExtendedBitmapDrawable#onOptsChanged(Resources, ExtendedOptions)} on the
* appropriate ExtendedBitmapDrawable.
*/
public Drawable placeholder;
/**
* Optional field if {@link #FEATURE_STATE_CHANGES} is supported.
*
* Special value 0 means default animation duration. Special value -1 means disable the
* animation (placeholder will be at maximum alpha always). Any value > 0 defines the
* duration in milliseconds.
*/
public int placeholderAnimationDuration = 0;
/**
* Optional field if {@link #FEATURE_STATE_CHANGES} is supported.
*
* If you modify this field you must call
* {@link ExtendedBitmapDrawable#onOptsChanged(Resources, ExtendedOptions)} on the
* appropriate ExtendedBitmapDrawable.
*/
public Drawable progressBar;
/**
* Use this constructor when all the feature parameters are changeable.
*/
public ExtendedOptions(final int features) {
this(features, null, null);
}
/**
* Use this constructor when you have to specify non-changeable feature parameters.
*/
public ExtendedOptions(final int features, final Drawable placeholder,
final Drawable progressBar) {
this.features = features;
this.placeholder = placeholder;
this.progressBar = progressBar;
}
/**
* Validate this ExtendedOptions instance to make sure that all the required fields are set
* for the requested features.
*
* This will throw an IllegalStateException if validation fails.
*/
private void validate()
throws IllegalStateException {
if (decodeHorizontalCenter < 0 || decodeHorizontalCenter > 1) {
throw new IllegalStateException(
"ExtendedOptions: decodeHorizontalCenter must be within 0 and 1, " +
"inclusive");
}
if (decodeVerticalCenter < 0 || decodeVerticalCenter > 1) {
throw new IllegalStateException(
"ExtendedOptions: decodeVerticalCenter must be within 0 and 1, inclusive");
}
if ((features & FEATURE_ORDERED_DISPLAY) != 0 && decodeAggregator == null) {
throw new IllegalStateException(
"ExtendedOptions: To support FEATURE_ORDERED_DISPLAY, "
+ "decodeAggregator must be set.");
}
if ((features & FEATURE_PARALLAX) != 0 && parallaxSpeedMultiplier <= 1) {
throw new IllegalStateException(
"ExtendedOptions: To support FEATURE_PARALLAX, "
+ "parallaxSpeedMultiplier must be greater than 1.");
}
if ((features & FEATURE_STATE_CHANGES) != 0) {
if (backgroundColor == 0
&& placeholder == null) {
throw new IllegalStateException(
"ExtendedOptions: To support FEATURE_STATE_CHANGES, "
+ "either backgroundColor or placeholder must be set.");
}
if (placeholderAnimationDuration < -1) {
throw new IllegalStateException(
"ExtendedOptions: To support FEATURE_STATE_CHANGES, "
+ "placeholderAnimationDuration must be set correctly.");
}
if (backgroundColor != 0 && Color.alpha(backgroundColor) != 255) {
throw new IllegalStateException(
"ExtendedOptions: To support FEATURE_STATE_CHANGES, "
+ "backgroundColor must be set to an opaque color.");
}
}
}
}
}