/*
* Copyright (C) 2010 Cyril Mottier (http://www.cyrilmottier.com)
*
* 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 greendroid.widget;
import greendroid.image.ImageProcessor;
import greendroid.image.ImageRequest;
import greendroid.image.ImageRequest.ImageRequestCallback;
import greendroid.util.Config;
import greendroid.util.GDUtils;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageView;
import android.widget.ListView;
import com.cyrilmottier.android.greendroid.R;
/**
* <p>
* A {@link AsyncImageView} is a network-aware {@link ImageView}. It may display
* images from the web according to a URL. {@link AsyncImageView} takes care of
* loading asynchronously images on the Internet. It also caches images in an
* application-wide cache to prevent loading images several times.
* </p>
* <p>
* Clients may listen the {@link OnImageViewLoadListener} to be notified of the
* current image loading state.
* </p>
* <p>
* {@link AsyncImageView} may be extremely useful in {@link ListView}'s row. To
* prevent your {@link AsyncImageView} from downloading while scrolling or
* flinging it is a good idea to pause it using {@link #setPaused(boolean)}
* method. Once the scrolling/flinging is over, <em>un-pause</em> your
* {@link AsyncImageView}s using <code>setPaused(false)</code>
* </p>
*
* @author Cyril Mottier
*/
public class AsyncImageView extends ImageView implements ImageRequestCallback {
private static final String LOG_TAG = AsyncImageView.class.getSimpleName();
/**
* Clients may listen to {@link AsyncImageView} changes using a
* {@link OnImageViewLoadListener}.
*
* @author Cyril Mottier
*/
public static interface OnImageViewLoadListener {
/**
* Called when the image started to load
*
* @param imageView
* The AsyncImageView that started loading
*/
void onLoadingStarted(AsyncImageView imageView);
/**
* Called when the image ended to load that is when the image has been
* downloaded and is ready to be displayed on screen
*
* @param imageView
* The AsyncImageView that ended loading
*/
void onLoadingEnded(AsyncImageView imageView, Bitmap image);
/**
* Called when the image loading failed
*
* @param imageView
* The AsyncImageView that failed to load
*/
void onLoadingFailed(AsyncImageView imageView, Throwable throwable);
}
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 int mImageSource;
private Bitmap mDefaultBitmap;
private Drawable mDefaultDrawable;
private int mDefaultResId;
private String mUrl;
private ImageRequest mRequest;
private boolean mPaused;
private Bitmap mBitmap;
private OnImageViewLoadListener mOnImageViewLoadListener;
private ImageProcessor mImageProcessor;
private BitmapFactory.Options mOptions;
public AsyncImageView(Context context) {
this(context, null);
}
public AsyncImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AsyncImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initializeDefaultValues();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.AsyncImageView);
setUrl(a.getString(R.styleable.AsyncImageView_url));
Drawable d = a.getDrawable(R.styleable.AsyncImageView_defaultSrc);
if (d != null) {
setDefaultImageDrawable(d);
}
final int inDensity = a.getInt(R.styleable.AsyncImageView_inDensity, -1);
if (inDensity != -1) {
setInDensity(inDensity);
}
a.recycle();
}
private void initializeDefaultValues() {
mImageSource = IMAGE_SOURCE_UNKNOWN;
mPaused = false;
}
/**
* Return true if this AsyncImageView is currently loading an image.
*
* @return true if this AsyncImageView is currently loading an image.
* Otherwise it returns false.
*/
public boolean isLoading() {
return mRequest != null;
}
/**
* Return true if the displayed image has been correctly loaded.
*
* @return true if this AsyncImageView succeed to load the image at the
* given url.
*/
public boolean isLoaded() {
return mRequest == null && mBitmap != null;
}
/**
* Pause this AsyncImageView preventing it from downloading the image. The
* download process will start back once setPaused(false) is called.
*
* @param paused
*/
public void setPaused(boolean paused) {
if (mPaused != paused) {
mPaused = paused;
if (!paused) {
reload();
}
}
}
/**
* Helper to {@link #setBitmapFactoryOptions(Options)} that simply
* sets the inDensity for loaded image.
*
* @param inDensity
* @see AsyncImageView#setBitmapFactoryOptions(Options)
*/
public void setInDensity(int inDensity) {
if (mOptions == null) {
mOptions = new BitmapFactory.Options();
mOptions.inDither = true;
mOptions.inScaled = true;
mOptions.inTargetDensity = getContext().getResources().getDisplayMetrics().densityDpi;
}
mOptions.inDensity = inDensity;
}
/**
* Assign a {@link Options} object to this {@link AsyncImageView}. Those
* options are used internally by the {@link AsyncImageView} when decoding
* the image. This may be used to prevent the default behavior that loads
* all images as mdpi density.
*
* @param options
*/
public void setOptions(BitmapFactory.Options options) {
mOptions = options;
}
/**
* Reload the image pointed by the given URL
*/
public void reload() {
reload(false);
}
/**
* Reload the image pointed by the given URL. You may want to force
* reloading by setting the force parameter to true.
*
* @param force
* if true the AsyncImageView won't look into the
* application-wide cache.
*/
public void reload(boolean force) {
if (mRequest == null && mUrl != null) {
// Prior downloading the image ... let's look in a cache !
// TODO cyril: This is a synchronous call ... make it asynchronous
mBitmap = null;
if (!force) {
mBitmap = GDUtils.getImageCache(getContext()).get(mUrl);
}
if (mBitmap != null) {
setImageBitmap(mBitmap);
return;
}
if (Config.GD_INFO_LOGS_ENABLED) {
Log.i(LOG_TAG,
"Cache miss. Starting to load the image at the given URL");
}
setDefaultImage();
mRequest = new ImageRequest(mUrl, this, mImageProcessor, mOptions);
mRequest.load(getContext());
}
}
/**
* Force the loading to be stopped.
*/
public void stopLoading() {
if (mRequest != null) {
mRequest.cancel();
mRequest = null;
}
}
/**
* Register a callback to be invoked when an event occured for this
* AsyncImageView.
*
* @param listener
* The listener that will be notified
*/
public void setOnImageViewLoadListener(OnImageViewLoadListener listener) {
mOnImageViewLoadListener = listener;
}
/**
* Set the url of the image that will be used as the content of this
* AsyncImageView. The given may be null in order to display the default
* image. Please note the url may be a local url. For instance, you can
* asynchronously load images from the disk memory is the url scheme is
* <code>file://</code>
*
* @param url
* The url of the image to set. Pass null to force the
* AsyncImageView to display the default image
*/
public void setUrl(String url) {
// Check the url has changed
if (mBitmap != null && url != null && url.equals(mUrl)) {
return;
}
stopLoading();
mUrl = url;
// Setting the url to an empty string force the displayed image to the
// default image
if (TextUtils.isEmpty(mUrl)) {
mBitmap = null;
setDefaultImage();
} else {
if (!mPaused) {
reload();
} else {
// We're paused: let's look in a synchronous and efficient cache
// prior using the default image.
mBitmap = GDUtils.getImageCache(getContext()).get(mUrl);
if (mBitmap != null) {
setImageBitmap(mBitmap);
return;
} else {
setDefaultImage();
}
}
}
}
/**
* Set the default bitmap as the content of this AsyncImageView
*
* @param bitmap
* The bitmap to set
*/
public void setDefaultImageBitmap(Bitmap bitmap) {
mImageSource = IMAGE_SOURCE_BITMAP;
mDefaultBitmap = bitmap;
setDefaultImage();
}
/**
* Set the default drawable as the content of this AsyncImageView
*
* @param drawable
* The drawable to set
*/
public void setDefaultImageDrawable(Drawable drawable) {
mImageSource = IMAGE_SOURCE_DRAWABLE;
mDefaultDrawable = drawable;
setDefaultImage();
}
/**
* Set the default resource as the content of this AsyncImageView
*
* @param resId
* The resource identifier to set
*/
public void setDefaultImageResource(int resId) {
mImageSource = IMAGE_SOURCE_RESOURCE;
mDefaultResId = resId;
setDefaultImage();
}
/**
* Set an image processor to this AsyncImageView. An ImageProcessor may be
* used in order to work on the retrieved Bitmap prior displaying it on
* screen.
*
* @param imageProcessor
* The {@link ImageProcessor} to set
* @see ImageProcessor
*/
public void setImageProcessor(ImageProcessor imageProcessor) {
mImageProcessor = imageProcessor;
}
private void setDefaultImage() {
if (mBitmap == null) {
switch (mImageSource) {
case IMAGE_SOURCE_BITMAP:
setImageBitmap(mDefaultBitmap);
break;
case IMAGE_SOURCE_DRAWABLE:
setImageDrawable(mDefaultDrawable);
break;
case IMAGE_SOURCE_RESOURCE:
setImageResource(mDefaultResId);
break;
default:
setImageDrawable(null);
break;
}
}
}
static class SavedState extends BaseSavedState {
String url;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
url = in.readString();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeString(url);
}
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];
}
};
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.url = mUrl;
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setUrl(ss.url);
}
public void onImageRequestStarted(ImageRequest request) {
if (mOnImageViewLoadListener != null) {
mOnImageViewLoadListener.onLoadingStarted(this);
}
}
public void onImageRequestFailed(ImageRequest request, Throwable throwable) {
mRequest = null;
if (mOnImageViewLoadListener != null) {
mOnImageViewLoadListener.onLoadingFailed(this, throwable);
}
}
public void onImageRequestEnded(ImageRequest request, Bitmap image) {
mBitmap = image;
setImageBitmap(image);
if (mOnImageViewLoadListener != null) {
mOnImageViewLoadListener.onLoadingEnded(this, image);
}
mRequest = null;
}
public void onImageRequestCancelled(ImageRequest request) {
mRequest = null;
if (mOnImageViewLoadListener != null) {
mOnImageViewLoadListener.onLoadingFailed(this, null);
}
}
}