package net.frakbot.imageviewex;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
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.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.util.Log;
import android.view.View;
import android.widget.ImageView;
import java.io.InputStream;
/**
* Extension of the ImageView that handles any kind of image already supported
* by ImageView, plus animated GIF images.
*
* @author Sebastiano Poggi, Francesco Pontillo
*/
public class ImageViewEx extends ImageView {
private static final String TAG = ImageViewEx.class.getSimpleName();
private static boolean mCanAlwaysAnimate = true;
private float mScale = -1;
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 Movie mGif;
private double mGifStartTime;
private int mFrameDuration = 67;
private Handler mHandler = new Handler();
private Thread mUpdater;
private ImageAlign mImageAlign = ImageAlign.NONE;
private DisplayMetrics mDm;
///////////////////////////////////////////////////////////
/// 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();
}
/**
* 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;
Thread t = new Thread(new Runnable() {
@Override
public void run() {
thisImageView.setSourceBlocking(src);
}
});
t.setPriority(Thread.MIN_PRIORITY);
t.setName("ImageSetter@" + this.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
mHandler.post(new Runnable() {
@Override
public void run() {
setImageDrawable(d);
measure(0, 0);
requestLayout();
try {
AnimationDrawable animationDrawable = (AnimationDrawable) getDrawable();
animationDrawable.start();
}
catch (Exception ignored) {
}
}
});
}
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
mHandler.post(new MovieRunnable(gif));
}
}
/**
* Runnable that sets the animated gif.
* @author Francesco Pontillo, Sebastiano Poggi
*/
private class MovieRunnable implements Runnable {
private Movie gif;
public MovieRunnable(Movie gif) {
this.gif = gif;
}
@Override
public void run() {
measure(0, 0);
requestLayout();
initializeDefaultValues();
mImageSource = IMAGE_SOURCE_GIF;
mGif = gif;
play();
}
}
/** {@inheritDoc} */
public void setImageResource(int resId) {
initializeDefaultValues();
super.setImageResource(resId);
mImageSource = IMAGE_SOURCE_RESOURCE;
}
/** {@inheritDoc} */
public void setImageDrawable(Drawable drawable) {
blockLayoutIfPossible();
initializeDefaultValues();
super.setImageDrawable(drawable);
mBlockLayout = false;
mImageSource = IMAGE_SOURCE_DRAWABLE;
}
/** {@inheritDoc} */
public void setImageBitmap(Bitmap bm) {
initializeDefaultValues();
super.setImageBitmap(bm);
mImageSource = IMAGE_SOURCE_BITMAP;
}
/**
* 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.
*
* @param align The new ImageAlign value.
*/
public void setImageAlign(ImageAlign align) {
if (align != mImageAlign) {
mImageAlign = align;
invalidate();
}
}
///////////////////////////////////////////////////////////
/// 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;
}
/**
* 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.
*/
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() {
ImageViewEx.this.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. */
@SuppressWarnings("deprecation")
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();
}
}
///////////////////////////////////////////////////////////
/// 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);
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
setScaleType(ScaleType.CENTER_CROP);
canvas.translate(0.0f, calcTopAlignYDisplacement());
}
mGif.draw(canvas, this.getWidth() - (mGif.width() * mScale), this.getHeight() - (mGif.height() * mScale));
canvas.restoreToCount(saveCnt);
}
else {
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);
}
}
}
/** @see android.view.View#measure(int, int) */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mScale = getScale();
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
@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 ///
///////////////////////////////////////////////////////////
/**
* Determines the width of this View
*
* @param measureSpec A measureSpec packed into an int
*
* @return The width of the view, honoring constraints from measureSpec
*/
private int measureWidth(int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
}
else {
// Measure the mGif
if (mGif != null) {
result = Math.round(mGif.width() * mScale) + getPaddingLeft() + getPaddingRight();
}
else if (getDrawable() != null) // Measure the drawable
{
result = Math.round(getDrawable().getIntrinsicWidth() * mScale) + getPaddingLeft() + getPaddingRight();
}
else {
// Nothing to measure, use the requested value
result = specSize;
}
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
}
/**
* Determines the height of this view
*
* @param measureSpec A measureSpec packed into an int
*
* @return The height of the view, honoring constraints from measureSpec
*/
private int measureHeight(int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
}
else {
// Measure the mGif
if (mGif != null) {
result = Math.round(mGif.height() * mScale) + getPaddingTop() + getPaddingBottom();
}
else if (getDrawable() != null) // Measure the drawable
{
result = Math.round(getDrawable().getIntrinsicHeight() * mScale) + getPaddingTop() + getPaddingBottom();
}
else {
// Nothing to measure, use the requested value
result = specSize;
}
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
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 = 0;
float displacement = 0f;
if (viewHeight <= 0) {
Log.v(TAG, "The ImageViewEx is still initializing...");
return displacement;
}
if (mGif == null) {
final Drawable tmpDrawable = getDrawable();
if ((tmpDrawable == null || !(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();
}
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() {
if (canAlwaysAnimate()) {
return canAnimate();
}
else {
return canAlwaysAnimate();
}
}
///////////////////////////////////////////////////////////
/// 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];
}
};
}
}