package external.GifImageViewEx.net.frakbot.imageviewex;
import java.io.InputStream;
import java.lang.reflect.Method;
import com.aiyou.R;
import com.aiyou.utils.logcat.Logcat;
import com.aiyou.view.DarkImageView;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.ImageView;
/**
* Extension of the ImageView that handles any kind of image already supported
* by ImageView, plus animated GIF images.
* <p/>
* <b>WARNING:</b> due to Android limitations, the android:adjustViewBounds
* attribute is ignored on API levels < 16 (Jelly Bean 4.1). Use our own
* adjustViewBounds attribute to obtain the same behaviour!
*
* @author Sebastiano Poggi, Francesco Pontillo
*/
@SuppressWarnings({
"deprecation"
})
public class ImageViewEx extends DarkImageView {
private static final String TAG = ImageViewEx.class.getSimpleName();
private static boolean mCanAlwaysAnimate = true;
private float mScale = -1;
private boolean mAdjustViewBounds = false;
private static final int IMAGE_SOURCE_UNKNOWN = -1;
private static final int IMAGE_SOURCE_RESOURCE = 0;
private static final int IMAGE_SOURCE_DRAWABLE = 1;
private static final int IMAGE_SOURCE_BITMAP = 2;
private static final int IMAGE_SOURCE_GIF = 2;
@SuppressWarnings("unused")
private int mImageSource;
// Used by the fixed size optimizations
private boolean mIsFixedSize = false;
private boolean mBlockLayout = false;
private BitmapFactory.Options mOptions;
private int mOverriddenDensity = -1;
private static int mOverriddenClassDensity = -1;
private int mMaxHeight, mMaxWidth;
private Movie mGif;
private double mGifStartTime;
private int mFrameDuration = 67;
private final Handler mHandler = new Handler();
private Thread mUpdater;
private ImageAlign mImageAlign = ImageAlign.NONE;
private final DisplayMetrics mDm;
private final SetDrawableRunnable mSetDrawableRunnable = new SetDrawableRunnable();
private final SetGifRunnable mSetGifRunnable = new SetGifRunnable();
private ScaleType mScaleType;
protected Drawable mEmptyDrawable = new ColorDrawable(0x00000000);
protected FillDirection mFillDirection = FillDirection.NONE;
// /////////////////////////////////////////////////////////
// / CONSTRUCTORS ///
// /////////////////////////////////////////////////////////
/**
* Creates an instance for the class.
*
* @param context The context to instantiate the object for.
*/
public ImageViewEx(Context context) {
super(context);
mDm = context.getResources().getDisplayMetrics();
}
/**
* Creates an instance for the class and initializes it with a given image.
*
* @param context The context to initialize the instance into.
* @param src InputStream containing the GIF to view.
*/
public ImageViewEx(Context context, InputStream src) {
super(context);
mGif = Movie.decodeStream(src);
mDm = context.getResources().getDisplayMetrics();
}
/**
* Creates an instance for the class.
*
* @param context The context to initialize the instance into.
* @param attrs The parameters to initialize the instance with.
*/
public ImageViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
mDm = context.getResources().getDisplayMetrics();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ImageViewEx, 0, 0);
if (a.hasValue(R.styleable.ImageViewEx_adjustViewBounds)) {
// Prioritize our own adjustViewBounds
setAdjustViewBounds(a.getBoolean(
R.styleable.ImageViewEx_adjustViewBounds, false));
} else {
// Fallback strategy: try to use ImageView's own adjustViewBounds
// attribute value
if (Build.VERSION.SDK_INT >= 16) {
// The ImageView#getAdjustViewBounds() method only exists from
// API Level 16+, for some reason.
try {
Method m = super.getClass()
.getMethod("getAdjustViewBounds");
mAdjustViewBounds = (Boolean) m.invoke(this);
} catch (Exception ignored) {
}
}
}
if (a.hasValue(R.styleable.ImageViewEx_fillDirection)) {
setFillDirection(a.getInt(R.styleable.ImageViewEx_fillDirection, 0));
}
if (a.hasValue(R.styleable.ImageViewEx_emptyDrawable)) {
setEmptyDrawable(a
.getDrawable(R.styleable.ImageViewEx_emptyDrawable));
}
a.recycle();
}
/**
* Creates an instance for the class and initializes it with a provided GIF.
*
* @param context The context to initialize the instance into.
* @param src The byte array containing the GIF to view.
*/
public ImageViewEx(Context context, byte[] src) {
super(context);
mGif = Movie.decodeByteArray(src, 0, src.length);
mDm = context.getResources().getDisplayMetrics();
}
/**
* Creates an instance for the class and initializes it with a provided GIF.
*
* @param context Il contesto in cui viene inizializzata l'istanza.
* @param src The path of the GIF file to view.
*/
public ImageViewEx(Context context, String src) {
super(context);
mGif = Movie.decodeFile(src);
mDm = context.getResources().getDisplayMetrics();
}
// /////////////////////////////////////////////////////////
// / PUBLIC SETTERS ///
// /////////////////////////////////////////////////////////
/**
* Initalizes the inner variable describing the kind of resource attached to
* the ImageViewEx.
*/
public void initializeDefaultValues() {
if (isPlaying())
stop();
mGif = null;
setTag(null);
mImageSource = IMAGE_SOURCE_UNKNOWN;
}
/**
* Sets the image from a byte array. The actual image-setting is called on a
* worker thread because it can be pretty CPU-consuming.
*
* @param src The byte array containing the image to set into the
* ImageViewEx.
*/
public void setSource(final byte[] src) {
if (src != null) {
final ImageViewEx thisImageView = this;
setImageDrawable(mEmptyDrawable);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
thisImageView.setSourceBlocking(src);
}
});
t.setPriority(Thread.MIN_PRIORITY);
t.setName("ImageSetter@" + hashCode());
t.run();
}
}
/**
* Sets the image from a byte array in a blocking, CPU-consuming way. Will
* handle itself referring back to the UI thread when needed.
*
* @param src The byte array containing the image to set into the
* ImageViewEx.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void setSourceBlocking(final byte[] src) {
if (src == null) {
try {
stop();
mGif = null;
setTag(null);
} catch (Throwable ignored) {
}
return;
}
Movie gif = null;
// If the animation is not requested
// decoding into a Movie is pointless (read: expensive)
if (internalCanAnimate()) {
gif = Movie.decodeByteArray(src, 0, src.length);
}
// If gif is null, it's probably not a gif
if (gif == null || !internalCanAnimate()) {
// If not a gif and if on Android 3+, enable HW acceleration
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
// Sets the image as a regular Drawable
setTag(null);
final Drawable d = Converters.byteArrayToDrawable(src, mOptions,
getContext());
// We need to run this on the UI thread
stopLoading();
mSetDrawableRunnable.setDrawable(d);
mHandler.post(mSetDrawableRunnable);
} else {
// Disables the HW acceleration when viewing a GIF on Android 3+
if (Build.VERSION.SDK_INT >= 11) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
// We need to run this on the UI thread
stopLoading();
mSetGifRunnable.setGif(gif);
mHandler.post(mSetGifRunnable);
}
}
/** {@inheritDoc} */
public void setImageResource(int resId) {
initializeDefaultValues();
stopLoading();
stop();
super.setImageResource(resId);
mImageSource = IMAGE_SOURCE_RESOURCE;
mGif = null;
}
/** {@inheritDoc} */
public void setImageDrawable(Drawable drawable) {
blockLayoutIfPossible();
initializeDefaultValues();
stopLoading();
stop();
super.setImageDrawable(drawable);
mBlockLayout = false;
mGif = null;
mImageSource = IMAGE_SOURCE_DRAWABLE;
}
/** {@inheritDoc} */
public void setImageBitmap(Bitmap bm) {
initializeDefaultValues();
stopLoading();
stop();
super.setImageBitmap(bm);
mImageSource = IMAGE_SOURCE_BITMAP;
mGif = null;
}
/** {@inheritDoc} */
@Override
public void setScaleType(ScaleType scaleType) {
super.setScaleType(scaleType);
}
/**
* Sets the fill direction for the image. This is used in conjunction with
* {@link #setAdjustViewBounds(boolean)}. If <code>adjustViewBounds</code>
* is not already enabled, it will be automatically enabled by setting the
* direction to anything other than {@link FillDirection#NONE}.
*
* @param direction The fill direction.
*/
public void setFillDirection(FillDirection direction) {
if (direction != mFillDirection) {
mFillDirection = direction;
if (mFillDirection != FillDirection.NONE && !mAdjustViewBounds) {
setAdjustViewBounds(true);
}
requestLayout();
}
}
/**
* Private helper for
* {@link #setFillDirection(net.frakbot.imageviewex.ImageViewEx.FillDirection)}
* .
*
* @param direction The direction integer. 0 = NONE, 1 = HORIZONTAL, 2 =
* VERTICAL.
*/
private void setFillDirection(int direction) {
FillDirection fd;
switch (direction) {
case 1:
fd = FillDirection.HORIZONTAL;
break;
case 2:
fd = FillDirection.VERTICAL;
break;
default:
fd = FillDirection.NONE;
}
setFillDirection(fd);
}
/**
* Sets the duration, in milliseconds, of each frame during the GIF
* animation. It is the refresh period.
*
* @param duration The duration, in milliseconds, of each frame.
*/
public void setFramesDuration(int duration) {
if (duration < 1) {
throw new IllegalArgumentException(
"Frame duration can't be less or equal than zero.");
}
mFrameDuration = duration;
}
/**
* Sets the number of frames per second during the GIF animation.
*
* @param fps The fps amount.
*/
public void setFPS(float fps) {
if (fps <= 0.0) {
throw new IllegalArgumentException(
"FPS can't be less or equal than zero.");
}
mFrameDuration = Math.round(1000f / fps);
}
/**
* Sets a density for every image set to any {@link ImageViewEx}. If a
* custom density level is set for a particular instance of
* {@link ImageViewEx}, this will be ignored.
*
* @param classLevelDensity the density to apply to every instance of
* {@link ImageViewEx}.
*/
public static void setClassLevelDensity(int classLevelDensity) {
mOverriddenClassDensity = classLevelDensity;
}
/**
* Assign an Options object to this {@link ImageViewEx}. Those options are
* used internally by the {@link ImageViewEx} when decoding the image. This
* may be used to prevent the default behavior that loads all images as mdpi
* density.
*
* @param options The BitmapFactory.Options used to decode the images.
*/
public void setOptions(BitmapFactory.Options options) {
mOptions = options;
}
/**
* Programmatically overrides this view's density. The new density will be
* set on the next {@link #onMeasure(int, int)}.
*
* @param fixedDensity the new density the view has to use.
*/
public void setDensity(int fixedDensity) {
mOverriddenDensity = fixedDensity;
}
/**
* Removes the class level density for {@link ImageViewEx}.
*
* @see ImageViewEx#setClassLevelDensity(int)
*/
public static void removeClassLevelDensity() {
setClassLevelDensity(-1);
}
/**
* Class method. Sets the mCanAlwaysAnimate value. If it is true,
* {@link #canAnimate()} will be triggered, determining if the animation can
* be played in that particular instance of {@link ImageViewEx}. If it is
* false, {@link #canAnimate()} will never be triggered and GIF animations
* will never start. {@link #mCanAlwaysAnimate} defaults to true.
*
* @param mCanAlwaysAnimate boolean, true to always animate for every
* instance of {@link ImageViewEx}, false if you want to perform
* the decision method {@link #canAnimate()} on every
* {@link #setSource(byte[])} call.
*/
public static void setCanAlwaysAnimate(boolean mCanAlwaysAnimate) {
ImageViewEx.mCanAlwaysAnimate = mCanAlwaysAnimate;
}
/**
* Sets a value indicating wether the image is considered as having a fixed
* size. This will enable an optimization when assigning images to the
* ImageViewEx, but has to be used sparingly or it may cause artifacts if
* the image isn't really fixed in size.
* <p/>
* An example of usage for this optimization is in ListViews, where items
* images are supposed to be fixed size, and this enables buttery
* smoothness.
* <p/>
* See: https://plus.google.com/u/0/113058165720861374515/posts/iTk4PjgeAWX
*/
public void setIsFixedSize(boolean fixedSize) {
mIsFixedSize = fixedSize;
}
/**
* Sets a new ImageAlign value and redraws the View. If the ImageViewEx has
* a ScaleType set too, this will override it!
*
* @param align The new ImageAlign value.
* @deprecated Use setScaleType(ScaleType.FIT_START) and
* setScaleType(ScaleType.FIT_END) instead.
*/
public void setImageAlign(ImageAlign align) {
if (align != mImageAlign) {
mImageAlign = align;
invalidate();
}
}
/**
* Sets the drawable used as "empty". Note that this is not automatically
* assigned by {@link ImageViewEx} but is used by descendants such as
* {@link ImageViewNext}.
*
* @param d The "empty" drawable
*/
public void setEmptyDrawable(Drawable d) {
mEmptyDrawable = d;
}
@Override
public void setAdjustViewBounds(boolean adjustViewBounds) {
if (mFillDirection != FillDirection.NONE) {
// Just in case, shouldn't be ever necessary
if (!mAdjustViewBounds) {
mAdjustViewBounds = true;
super.setAdjustViewBounds(true);
}
return;
}
mAdjustViewBounds = adjustViewBounds;
super.setAdjustViewBounds(adjustViewBounds);
}
// /////////////////////////////////////////////////////////
// / PUBLIC GETTERS ///
// /////////////////////////////////////////////////////////
/** Disables density ovverriding. */
public void dontOverrideDensity() {
mOverriddenDensity = -1;
}
/**
* Returns a boolean indicating if an animation is currently playing.
*
* @return true if animating, false otherwise.
*/
public boolean isPlaying() {
return mUpdater != null && mUpdater.isAlive();
}
/**
* Returns a boolean indicating if the instance was initialized and if it is
* ready for playing the animation.
*
* @return true if the instance is ready for playing, false otherwise.
*/
public boolean canPlay() {
return mGif != null;
}
/**
* Class method. Returns the mCanAlwaysAnimate value. If it is true,
* {@link #canAnimate()} will be triggered, determining if the animation can
* be played in that particular instance of {@link ImageViewEx}. If it is
* false, {@link #canAnimate()} will never be triggered and GIF animations
* will never start. {@link #mCanAlwaysAnimate} defaults to true.
*
* @return boolean, true to see if this instance can be animated by calling
* {@link #canAnimate()}, if false, animations will never be
* triggered and {@link #canAnimate()} will never be evaluated for
* this instance.
*/
public static boolean canAlwaysAnimate() {
return mCanAlwaysAnimate;
}
/**
* This method should be overridden with your custom implementation. By
* default, it always returns {@code <code>true</code>}.
* <p/>
* <p>
* This method decides whether animations can be started for this instance
* of {@link ImageViewEx}. Still, if {@link #canAlwaysAnimate()} equals
* {@code <code>false</code>} this method will never be called for all of
* the instances of {@link ImageViewEx}.
*
* @return {@code <code>true</code>} if it can animate the current instance
* of {@link ImageViewEx}, false otherwise.
* @see {@link #setCanAlwaysAnimate(boolean)} to set the predefined class
* behavior in regards to animations.
*/
public boolean canAnimate() {
return true;
}
/**
* Gets the frame duration, in milliseconds, of each frame during the GIF
* animation. It is the refresh period.
*
* @return The duration, in milliseconds, of each frame.
*/
public int getFramesDuration() {
return mFrameDuration;
}
/**
* Gets the number of frames per second during the GIF animation.
*
* @return The fps amount.
*/
public float getFPS() {
return 1000.0f / mFrameDuration;
}
/**
* Gets the current scale value.
*
* @return Returns the scale value for this ImageViewEx.
*/
public float getScale() {
float targetDensity = getContext().getResources().getDisplayMetrics().densityDpi;
float displayThisDensity = getDensity();
mScale = targetDensity / displayThisDensity;
if (mScale < 0.1f)
mScale = 0.1f;
if (mScale > 5.0f)
mScale = 5.0f;
return mScale;
}
/**
* Gets the fill direction for this ImageViewEx.
*
* @return Returns the fill direction.
*/
public FillDirection getFillDirection() {
return mFillDirection;
}
/**
* Gets the drawable used as "empty" state.
*
* @return Returns the drawable used ad "empty".
*/
public Drawable getEmptyDrawable() {
return mEmptyDrawable;
}
/**
* Checks whether the class level density has been set.
*
* @return true if it has been set, false otherwise.
* @see ImageViewEx#setClassLevelDensity(int)
*/
public static boolean isClassLevelDensitySet() {
return mOverriddenClassDensity != -1;
}
/**
* Gets the class level density has been set.
*
* @return int, the class level density
* @see ImageViewEx#setClassLevelDensity(int)
*/
public static int getClassLevelDensity() {
return mOverriddenClassDensity;
}
/**
* Gets the set density of the view, given by the screen density or by value
* overridden with {@link #setDensity(int)}. If the density was not
* overridden and it can't be retrieved by the context, it simply returns
* the DENSITY_HIGH constant.
*
* @return int representing the current set density of the view.
*/
public int getDensity() {
int density;
// If a custom instance density was set, set the image to this density
if (mOverriddenDensity > 0) {
density = mOverriddenDensity;
} else if (isClassLevelDensitySet()) {
// If a class level density has been set, set every image to that
// density
density = getClassLevelDensity();
} else {
// If the instance density was not overridden, get the one from the
// display
DisplayMetrics metrics = new DisplayMetrics();
if (!(getContext() instanceof Activity)) {
density = DisplayMetrics.DENSITY_HIGH;
} else {
Activity activity = (Activity) getContext();
activity.getWindowManager().getDefaultDisplay()
.getMetrics(metrics);
density = metrics.densityDpi;
}
}
return density;
}
/**
* Sets a value indicating wether the image is considered as having a fixed
* size. See {@link #setIsFixedSize(boolean)} for further details.
*/
public boolean getIsFixedSize() {
return mIsFixedSize;
}
/**
* Returns the current ImageAlign setting.
*
* @return Returns the current ImageAlign setting.
* @deprecated Use setScaleType(ScaleType.FIT_START) and
* setScaleType(ScaleType.FIT_END) instead.
*/
public ImageAlign getImageAlign() {
return mImageAlign;
}
// /////////////////////////////////////////////////////////
// / PUBLIC METHODS ///
// /////////////////////////////////////////////////////////
/**
* Starts playing the GIF, if it hasn't started yet. FPS defaults to 15..
*/
public void play() {
// Do something if the animation hasn't started yet
if (mUpdater == null || !mUpdater.isAlive()) {
// Check id the animation is ready
if (!canPlay()) {
throw new IllegalStateException(
"Animation can't start before a GIF is loaded.");
}
// Initialize the thread and start it
mUpdater = new Thread() {
@Override
public void run() {
// Infinite loop: invalidates the View.
// Stopped when the thread is stopped or interrupted.
while (mUpdater != null && !mUpdater.isInterrupted()) {
mHandler.post(new Runnable() {
public void run() {
invalidate();
}
});
// The thread sleeps until the next frame
try {
Thread.sleep(mFrameDuration);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
};
mUpdater.start();
}
}
/** Pause playing the GIF, if it has started. */
public void pause() {
// If the animation has started
if (mUpdater != null && mUpdater.isAlive()) {
mUpdater.suspend();
}
}
/** Stops playing the GIF, if it has started. */
public void stop() {
// If the animation has started
if (mUpdater != null && mUpdater.isAlive() && canPlay()) {
mUpdater.interrupt();
mGifStartTime = 0;
}
}
/** {@inheritDoc} */
@Override
public void requestLayout() {
if (!mBlockLayout) {
super.requestLayout();
}
}
@Override
public void setMaxHeight(int maxHeight) {
super.setMaxHeight(maxHeight);
mMaxHeight = maxHeight;
}
@Override
public void setMaxWidth(int maxWidth) {
super.setMaxWidth(maxWidth);
mMaxWidth = maxWidth;
}
// /////////////////////////////////////////////////////////
// / EVENT HANDLERS ///
// /////////////////////////////////////////////////////////
/**
* Draws the control
*
* @param canvas The canvas to drow onto.
*/
@Override
protected void onDraw(Canvas canvas) {
if (mGif != null) {
long now = android.os.SystemClock.uptimeMillis();
// first time
if (mGifStartTime == 0) {
mGifStartTime = now;
}
int dur = mGif.duration();
if (dur == 0) {
dur = 1000;
}
int relTime = (int) ((now - mGifStartTime) % dur);
mGif.setTime(relTime);
int saveCnt = canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(mScale, mScale);
float[] gifDrawParams = applyScaleType(canvas);
mGif.draw(canvas, gifDrawParams[0], gifDrawParams[1]);
if (mImageAlign != ImageAlign.NONE) {
// We have an alignment override.
// Note: at the moment we only have TOP as custom alignment,
// so the code here is simplified. Will need refactoring
// if other custom alignments are implemented further on.
// ImageAlign.TOP: align top edge with the View
canvas.translate(0.0f, calcTopAlignYDisplacement());
}
canvas.restoreToCount(saveCnt);
} else {
// Reset the original scale type
super.setScaleType(getScaleType());
if (mImageAlign == ImageAlign.NONE) {
// Everything is normal when there is no alignment override
super.onDraw(canvas);
} else {
// We have an alignment override.
// Note: at the moment we only have TOP as custom alignment,
// so the code here is simplified. Will need refactoring
// if other custom alignments are implemented further on.
// ImageAlign.TOP: scaling forced to CENTER_CROP, align top edge
// with the View
setScaleType(ScaleType.CENTER_CROP);
int saveCnt = canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.translate(0.0f, calcTopAlignYDisplacement());
super.onDraw(canvas);
canvas.restoreToCount(saveCnt);
}
}
}
/**
* Applies the scale type of the ImageViewEx to the GIF. Use the returned
* value to draw the GIF and calculate the right y-offset, if any has to be
* set.
*
* @param canvas The {@link Canvas} to apply the {@link ScaleType} to.
* @return A float array containing, for each position: - 0 The x position
* of the gif - 1 The y position of the gif - 2 The scaling applied
* to the y-axis
*/
private float[] applyScaleType(Canvas canvas) {
// Get the current dimensions of the view and the gif
float vWidth = getWidth();
float vHeight = getHeight();
float gWidth = mGif.width() * mScale;
float gHeight = mGif.height() * mScale;
// Disable the default scaling, it can mess things up
if (mScaleType == null) {
mScaleType = getScaleType();
setScaleType(ScaleType.MATRIX);
}
float x = 0;
float y = 0;
float s = 1;
switch (mScaleType) {
case CENTER:
/* Center the image in the view, but perform no scaling. */
x = (vWidth - gWidth) / 2 / mScale;
y = (vHeight - gHeight) / 2 / mScale;
break;
case CENTER_CROP:
/*
* Scale the image uniformly (maintain the image's aspect ratio)
* so that both dimensions (width and height) of the image will
* be equal to or larger than the corresponding dimension of the
* view (minus padding). The image is then centered in the view.
*/
float minDimensionCenterCrop = Math.min(gWidth, gHeight);
if (minDimensionCenterCrop == gWidth) {
s = vWidth / gWidth;
} else {
s = vHeight / gHeight;
}
x = (vWidth - gWidth * s) / 2 / (s * mScale);
y = (vHeight - gHeight * s) / 2 / (s * mScale);
canvas.scale(s, s);
break;
case CENTER_INSIDE:
/*
* Scale the image uniformly (maintain the image's aspect ratio)
* so that both dimensions (width and height) of the image will
* be equal to or less than the corresponding dimension of the
* view (minus padding). The image is then centered in the view.
*/
// Scaling only applies if the gif is larger than the container!
if (gWidth > vWidth || gHeight > vHeight) {
float maxDimensionCenterInside = Math.max(gWidth, gHeight);
if (maxDimensionCenterInside == gWidth) {
s = vWidth / gWidth;
} else {
s = vHeight / gHeight;
}
}
x = (vWidth - gWidth * s) / 2 / (s * mScale);
y = (vHeight - gHeight * s) / 2 / (s * mScale);
canvas.scale(s, s);
break;
case FIT_CENTER:
/*
* Compute a scale that will maintain the original src aspect
* ratio, but will also ensure that src fits entirely inside
* dst. At least one axis (X or Y) will fit exactly. The result
* is centered inside dst.
*/
// This scale type always scales the gif to the exact dimension
// of
// the View
float maxDimensionFitCenter = Math.max(gWidth, gHeight);
if (maxDimensionFitCenter == gWidth) {
s = vWidth / gWidth;
} else {
s = vHeight / gHeight;
}
x = (vWidth - gWidth * s) / 2 / (s * mScale);
y = (vHeight - gHeight * s) / 2 / (s * mScale);
canvas.scale(s, s);
break;
case FIT_START:
/*
* Compute a scale that will maintain the original src aspect
* ratio, but will also ensure that src fits entirely inside
* dst. At least one axis (X or Y) will fit exactly. The result
* is centered inside dst.
*/
// This scale type always scales the gif to the exact dimension
// of
// the View
float maxDimensionFitStart = Math.max(gWidth, gHeight);
if (maxDimensionFitStart == gWidth) {
s = vWidth / gWidth;
} else {
s = vHeight / gHeight;
}
x = 0;
y = 0;
canvas.scale(s, s);
break;
case FIT_END:
/*
* Compute a scale that will maintain the original src aspect
* ratio, but will also ensure that src fits entirely inside
* dst. At least one axis (X or Y) will fit exactly. END aligns
* the result to the right and bottom edges of dst.
*/
// This scale type always scales the gif to the exact dimension
// of
// the View
float maxDimensionFitEnd = Math.max(gWidth, gHeight);
if (maxDimensionFitEnd == gWidth) {
s = vWidth / gWidth;
} else {
s = vHeight / gHeight;
}
x = (vWidth - gWidth * s) / mScale / s;
y = (vHeight - gHeight * s) / mScale / s;
canvas.scale(s, s);
break;
case FIT_XY:
/*
* Scale in X and Y independently, so that src matches dst
* exactly. This may change the aspect ratio of the src.
*/
float sFitX = vWidth / gWidth;
s = vHeight / gHeight;
x = 0;
y = 0;
canvas.scale(sFitX, s);
break;
default:
break;
}
return new float[] {
x, y, s
};
}
/** @see android.view.View#measure(int, int) */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mScale = getScale();
int w;
int h;
// Desired aspect ratio of the view's contents (not including padding)
float desiredAspect = 0.0f;
// We are allowed to change the view's width
boolean resizeWidth = false;
// We are allowed to change the view's height
boolean resizeHeight = false;
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
final Drawable drawable = getDrawable();
if (drawable != null) {
w = drawable.getIntrinsicWidth();
h = drawable.getIntrinsicHeight();
if (w <= 0)
w = 1;
if (h <= 0)
h = 1;
} else if (mGif != null) {
w = mGif.width();
h = mGif.height();
if (w <= 0)
w = 1;
if (h <= 0)
h = 1;
} else {
// If no drawable, its intrinsic size is 0.
w = 0;
h = 0;
}
// We are supposed to adjust view bounds to match the aspect
// ratio of our drawable. See if that is possible.
if (w > 0 && h > 0) {
if (mAdjustViewBounds) {
resizeWidth = widthSpecMode != MeasureSpec.EXACTLY
&& mFillDirection != FillDirection.HORIZONTAL;
resizeHeight = heightSpecMode != MeasureSpec.EXACTLY
&& mFillDirection != FillDirection.VERTICAL;
desiredAspect = (float) w / (float) h;
}
}
int pleft = getPaddingLeft();
int pright = getPaddingRight();
int ptop = getPaddingTop();
int pbottom = getPaddingBottom();
int widthSize;
int heightSize;
if (resizeWidth || resizeHeight) {
// If we get here, it means we want to resize to match the
// drawables aspect ratio, and we have the freedom to change at
// least one dimension.
// Get the max possible width given our constraints
widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth,
widthMeasureSpec);
// Get the max possible height given our constraints
heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight,
heightMeasureSpec);
if (desiredAspect != 0.0f) {
// See what our actual aspect ratio is
float actualAspect = (float) (widthSize - pleft - pright)
/ (heightSize - ptop - pbottom);
if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
boolean done = false;
// Try adjusting width to be proportional to height
if (resizeWidth) {
int newWidth = (int) (desiredAspect * (heightSize
- ptop - pbottom))
+ pleft + pright;
if (newWidth <= widthSize
|| mFillDirection == FillDirection.VERTICAL) {
widthSize = newWidth;
done = true;
}
}
// Try adjusting height to be proportional to width
if (!done && resizeHeight) {
int newHeight = (int) ((widthSize - pleft - pright) / desiredAspect)
+ ptop + pbottom;
if (newHeight <= heightSize
|| mFillDirection == FillDirection.HORIZONTAL) {
heightSize = newHeight;
}
}
}
}
} else {
/*
* We either don't want to preserve the drawables aspect ratio, or
* we are not allowed to change view dimensions. Just measure in the
* normal way.
*/
w += pleft + pright;
h += ptop + pbottom;
w = Math.max(w, getSuggestedMinimumWidth());
h = Math.max(h, getSuggestedMinimumHeight());
widthSize = resolveSize(w, widthMeasureSpec);
heightSize = resolveSize(h, heightMeasureSpec);
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
return new SavedState(superState);
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
}
// /////////////////////////////////////////////////////////
// / PRIVATE HELPERS ///
// /////////////////////////////////////////////////////////
/** Copied from {@link ImageView}'s implementation. */
private int resolveAdjustedSize(int desiredSize, int maxSize,
int measureSpec) {
int result = desiredSize;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// Parent says we can be as big as we want. Just don't be larger
// than max size imposed on ourselves.
result = Math.min(desiredSize, maxSize);
break;
case MeasureSpec.AT_MOST:
// Parent says we can be as big as we want, up to specSize.
// Don't be larger than specSize, and don't be larger than
// the max size imposed on ourselves.
result = Math.min(Math.min(desiredSize, specSize), maxSize);
break;
case MeasureSpec.EXACTLY:
// No choice. Do what we are told.
result = specSize;
break;
}
return result;
}
/**
* Calculates the top displacement for the image to make sure it is aligned
* at the top of the ImageViewEx.
*/
private float calcTopAlignYDisplacement() {
int viewHeight = getHeight();
int imgHeight;
float displacement = 0f;
if (viewHeight <= 0) {
Logcat.v(TAG, "The ImageViewEx is still initializing...");
return displacement;
}
if (mGif == null) {
final Drawable tmpDrawable = getDrawable();
if (!(tmpDrawable instanceof BitmapDrawable) || mGif == null) {
return 0f; // Nothing to do here
}
// Retrieve the bitmap, its height and the ImageView height
Bitmap bmp = ((BitmapDrawable) tmpDrawable).getBitmap();
imgHeight = bmp.getScaledHeight(mDm);
} else {
// This is a GIF...
imgHeight = mGif.height();
}
// noinspection IfMayBeConditional
if (viewHeight > imgHeight) {
displacement = -1 * (viewHeight - imgHeight); // Just align to top
// edge
} else {
// Top displacement [px] = (image height / 2) - (view height / 2)
displacement = -1 * ((imgHeight - viewHeight) / 2); // This is in
// pixels...
}
return displacement;
}
/**
* Blocks layout recalculation if the image is set as fixed size to prevent
* unnecessary calculations and provide butteriness.
*/
private void blockLayoutIfPossible() {
if (mIsFixedSize) {
mBlockLayout = true;
}
}
/**
* Internal method, deciding whether to trigger the custom decision method
* {@link #canAnimate()} or to use the static class value of
* mCanAlwaysAnimate.
*
* @return true if the animation can be started, false otherwise.
*/
private boolean internalCanAnimate() {
return canAlwaysAnimate() ? canAnimate() : canAlwaysAnimate();
}
/**
* Stops any currently running async loading (deserialization and parsing of
* the image).
*/
public void stopLoading() {
// noinspection ConstantConditions
if (mHandler != null) {
mHandler.removeCallbacks(mSetDrawableRunnable);
mHandler.removeCallbacks(mSetGifRunnable);
}
}
/**
* Temporarily shows the empty drawable (or empties the view if none is
* defined). Note that this does not follow all procedures
* {@link #setImageDrawable(android.graphics.drawable.Drawable)} follows and
* is only intended for temporary assignments such as in
* {@link ImageViewNext.ImageLoadCompletionListener#onLoadStarted(ImageViewNext, ImageViewNext.CacheLevel)}
* .
*/
public void showEmptyDrawable() {
setScaleType(ScaleType.CENTER_CROP);
super.setImageDrawable(mEmptyDrawable);
}
// /////////////////////////////////////////////////////////
// / PRIVATE CLASSES ///
// /////////////////////////////////////////////////////////
/** Class that represents a saved state for the ImageViewEx. */
private static class SavedState extends BaseSavedState {
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
}
@SuppressWarnings("unused")
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
/** A Runnable that sets a specified Drawable on the ImageView. */
private class SetDrawableRunnable implements Runnable {
private Drawable mDrawable;
private final Object mDrawableLock = new Object();
private void setDrawable(Drawable drawable) {
synchronized (mDrawableLock) {
mDrawable = drawable;
}
}
@Override
public void run() {
synchronized (mDrawableLock) {
if (mDrawable == null) {
Logcat.v(TAG, "Loading the Drawable has been aborted");
return;
}
setImageDrawable(mDrawable);
measure(0, 0);
requestLayout();
try {
AnimationDrawable animationDrawable = (AnimationDrawable) getDrawable();
animationDrawable.start();
} catch (Exception ignored) {
}
}
}
}
/** A Runnable that sets a specified Movie on the ImageView. */
private class SetGifRunnable implements Runnable {
private Movie mGifMovie;
private final Object mGifMovieLock = new Object();
private void setGif(Movie drawable) {
synchronized (mGifMovieLock) {
mGifMovie = drawable;
}
}
@Override
public void run() {
synchronized (mGifMovieLock) {
if (mGifMovie == null) {
Logcat.v(TAG, "Loading the GIF has been aborted");
return;
}
initializeDefaultValues();
mImageSource = IMAGE_SOURCE_GIF;
setImageDrawable(null);
mGif = mGifMovie;
measure(0, 0);
requestLayout();
play();
}
}
}
/**
* The fill direction for the image. All values other than
* {@link FillDirection#NONE} imply having the <code>adjustViewBounds</code>
* function active on the {@link ImageViewEx}.
*/
public enum FillDirection {
/**
* No fill direction. Acts just like a common {@link ImageView} does.
*/
NONE,
/**
* If the width of the {@link ImageViewEx} is longer than the width of
* the image it contains, the image is scaled to fit the width of the
* view. The height of the view is then adjusted to fit the height of
* the scaled image.
*/
HORIZONTAL,
/**
* If the height of the {@link ImageViewEx} is longer than the height of
* the image it contains, the image is scaled to fit the height of the
* view. The width of the view is then adjusted to fit the width of the
* scaled image.
*/
VERTICAL
}
}