/* Copyright (c) 2009-2012 Matthias Kaeppler
*
* 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.github.ignition.core.widgets;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.os.Message;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.ViewSwitcher;
import com.github.ignition.core.Ignition;
import com.github.ignition.core.R;
import com.github.ignition.support.images.remote.RemoteImageLoader;
import com.github.ignition.support.images.remote.RemoteImageLoaderHandler;
/**
* An {@link ImageView} that fetches its image off the web from the supplied
* URL. While the image is being downloaded, a progress indicator will be shown.
* The following attributes are supported:
* <ul>
* <li>android:src (Drawable) -- The default/placeholder image that is shown if
* no image can be downloaded, or before the image download starts (see
* {@link android.R.attr#src})
* <li>android:indeterminateDrawable (Drawable) -- The progress drawable to use
* while the image is being downloaded (see
* {@link android.R.attr#indeterminateDrawable})</li>
* <li>ignition:imageUrl (String) -- The URL at which the image is found online</li>
* <li>ignition:autoLoad (Boolean) -- Whether the download should start
* immediately after view inflation</li>
* <li>ignition:errorDrawable (Drawable) -- The drawable to display if the image
* download fails</li>
* </ul>
* Being an ImageView itself, all other attributes of Android's own ImageView
* are supported, too.
* <p>
* This functionality is realized by the view dynamically re-attaching itself in
* the Activity's view tree under a {@link ViewSwitcher}. You can retrieve a
* reference to the switcher by a call to {@link View#getParent()}. In order for
* layouting to work properly in Android's {@link RelativeLayout}, the view
* itself and the switcher (its parent view) share the same view IDs (a
* circumstance that's not optimal but allowed by Android). If you find that
* calls to findViewById return the switcher instead of the view itself, please
* use the provided {@link #findViewById(Activity, int))} helper method instead,
* which guarantees to return instances of this class.
* </p>
*
* @author Matthias Kaeppler
*/
public class RemoteImageView extends ImageView {
public static final int DEFAULT_ERROR_DRAWABLE_RES_ID = android.R.drawable.ic_dialog_alert;
private static final String ATTR_AUTO_LOAD = "autoLoad";
private static final String ATTR_IMAGE_URL = "imageUrl";
private static final String ATTR_ERROR_DRAWABLE = "errorDrawable";
private static final int STATE_DEFAULT = 0;
private static final int STATE_LOADED = 1;
private static final int STATE_LOADING = 2;
private int state = STATE_DEFAULT;
private String imageUrl;
private boolean autoLoad;
private ViewGroup progressViewContainer;
private Drawable progressDrawable, errorDrawable;
private RemoteImageLoader imageLoader;
private static RemoteImageLoader sharedImageLoader;
private RemoteImageViewListener listener;
/**
* Use this method to inject an image loader that will be shared across all
* instances of this class. If the shared reference is null, a new
* {@link RemoteImageLoader} will be instantiated for every instance of this
* class.
*
* @param imageLoader
* the shared image loader
*/
public static void setSharedImageLoader(RemoteImageLoader imageLoader) {
sharedImageLoader = imageLoader;
}
/**
* @param context
* the view's current context
* @param imageUrl
* the URL of the image to download and show
* @param autoLoad
* Whether the download should start immediately after creating
* the view. If set to false, use {@link #loadImage()} to
* manually trigger the image download.
*/
public RemoteImageView(Context context, String imageUrl, boolean autoLoad) {
super(context);
initialize(context, imageUrl, null, null, autoLoad, null);
}
/**
* @param context
* the view's current context
* @param imageUrl
* the URL of the image to download and show
* @param progressDrawable
* the drawable to be used for the {@link ProgressBar} which is
* displayed while the image is loading
* @param errorDrawable
* the drawable to be used if a download error occurs
* @param autoLoad
* Whether the download should start immediately after creating
* the view. If set to false, use {@link #loadImage()} to
* manually trigger the image download.
*/
public RemoteImageView(Context context, String imageUrl,
Drawable progressDrawable, Drawable errorDrawable, boolean autoLoad) {
super(context);
initialize(context, imageUrl, progressDrawable, errorDrawable,
autoLoad, null);
}
public RemoteImageView(Context context, AttributeSet attributes) {
super(context, attributes);
// Read all Android specific view attributes into a typed array first.
// These are attributes that are specific to RemoteImageView, but which
// are not in the
// ignition XML namespace.
TypedArray imageViewAttrs = context.getTheme().obtainStyledAttributes(
attributes, R.styleable.RemoteImageView, 0, 0);
int progressDrawableId = imageViewAttrs.getResourceId(
R.styleable.RemoteImageView_android_indeterminateDrawable, 0);
imageViewAttrs.recycle();
int errorDrawableId = attributes.getAttributeResourceValue(
Ignition.XMLNS, ATTR_ERROR_DRAWABLE,
DEFAULT_ERROR_DRAWABLE_RES_ID);
Drawable errorDrawable = context.getResources().getDrawable(
errorDrawableId);
Drawable progressDrawable = null;
if (progressDrawableId > 0) {
progressDrawable = context.getResources().getDrawable(
progressDrawableId);
}
String imageUrl = attributes.getAttributeValue(Ignition.XMLNS,
ATTR_IMAGE_URL);
boolean autoLoad = attributes.getAttributeBooleanValue(Ignition.XMLNS,
ATTR_AUTO_LOAD, true);
initialize(context, imageUrl, progressDrawable, errorDrawable,
autoLoad, attributes);
}
private void initialize(Context context, String imageUrl,
Drawable progressDrawable, Drawable errorDrawable,
boolean autoLoad, AttributeSet attributes) {
this.imageUrl = imageUrl;
this.autoLoad = autoLoad;
this.progressDrawable = progressDrawable;
this.errorDrawable = errorDrawable;
if (sharedImageLoader == null) {
this.imageLoader = new RemoteImageLoader(context);
} else {
this.imageLoader = sharedImageLoader;
}
progressViewContainer = new FrameLayout(getContext());
progressViewContainer.addView(buildProgressSpinnerView(getContext()));
if (autoLoad) {
loadImage();
} else {
showProgressView(false);
}
}
protected View buildProgressSpinnerView(Context context) {
ProgressBar loadingSpinner = new ProgressBar(context);
loadingSpinner.setIndeterminate(true);
if (this.progressDrawable == null) {
this.progressDrawable = loadingSpinner.getIndeterminateDrawable();
} else {
loadingSpinner.setIndeterminateDrawable(progressDrawable);
if (progressDrawable instanceof AnimationDrawable) {
((AnimationDrawable) progressDrawable).start();
}
}
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
progressDrawable.getIntrinsicWidth(),
progressDrawable.getIntrinsicHeight(), Gravity.CENTER);
loadingSpinner.setLayoutParams(lp);
return loadingSpinner;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// insert the progress view and its container as a sibling to this image
// view
if (progressViewContainer.getParent() == null) {
// the container for the progress view must behave identically to
// this view during
// layouting, otherwise e.g. relative positioning via IDs will break
progressViewContainer.setLayoutParams(getLayoutParams());
ViewGroup parent = (ViewGroup) getParent();
int index = parent.indexOfChild(this);
parent.addView(progressViewContainer, index + 1);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* Use this method to trigger the image download if you had previously set
* autoLoad to false.
*/
public void loadImage() {
if (state != STATE_LOADING) {
if (imageUrl == null) {
throw new IllegalStateException(
"image URL is null; did you forget to set it for this view?");
}
showProgressView(true);
imageLoader.loadImage(imageUrl, this,
new DefaultImageLoaderHandler());
}
}
public boolean isLoaded() {
return state == STATE_LOADED;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
/**
* Often you have resources which usually have an image, but some don't. For
* these cases, use this method to supply a placeholder drawable which will
* be loaded instead of a web image.
*
* @param imageResourceId
* the resource of the placeholder image drawable
*/
public void setNoImageDrawable(int imageResourceId) {
setImageDrawable(getContext().getResources().getDrawable(
imageResourceId));
showProgressView(false);
}
/**
* Reset to view to its initial state, i.e. hide the progress item and show
* the image.
*/
public void reset() {
showProgressView(false);
}
private void showProgressView(boolean show) {
if (show) {
state = STATE_LOADING;
progressViewContainer.setVisibility(View.VISIBLE);
setVisibility(View.INVISIBLE);
} else {
state = STATE_DEFAULT;
progressViewContainer.setVisibility(View.INVISIBLE);
setVisibility(View.VISIBLE);
}
}
private class DefaultImageLoaderHandler extends RemoteImageLoaderHandler {
public DefaultImageLoaderHandler() {
super(RemoteImageView.this, imageUrl, errorDrawable);
}
@Override
protected boolean handleImageLoaded(Bitmap bitmap, Message msg) {
boolean wasUpdated = super.handleImageLoaded(bitmap, msg);
if (wasUpdated) {
state = STATE_LOADED;
if (listener != null) {
bitmap = resizeBitmap(bitmap, getLayoutParams().width, getLayoutParams().height);
listener.onImageLoaded(bitmap);
}
showProgressView(false);
}
return wasUpdated;
}
}
/**
* 保持长宽比缩小Bitmap
*
* @param bitmap
* @param maxWidth
* @param maxHeight
* @return
*/
public Bitmap resizeBitmap(Bitmap bitmap, int maxWidth, int maxHeight) {
int originWidth = bitmap.getWidth();
int originHeight = bitmap.getHeight();
// no need to resize
if (originWidth < maxWidth && originHeight < maxHeight) {
return bitmap;
}
int width = originWidth;
int height = originHeight;
// 若图片过宽, 则保持长宽比缩放图片
if (originWidth > maxWidth) {
width = maxWidth;
double i = originWidth * 1.0 / maxWidth;
height = (int) Math.floor(originHeight / i);
bitmap = Bitmap.createScaledBitmap(bitmap, width, height, false);
}
// 若图片过长, 则从上端截取
if (height > maxHeight) {
height = maxHeight;
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
}
// Log.i(TAG, width + " width");
// Log.i(TAG, height + " height");
return bitmap;
}
/**
* Returns the URL of the image to show. Corresponds to the view attribute
* ignition:imageUrl.
*
* @return the image URL
*/
public String getImageUrl() {
return imageUrl;
}
/**
* Whether or not the image should be downloaded immediately after view
* inflation. Corresponds to the view attribute ignition:autoLoad (default:
* true).
*
* @return true if auto downloading of the image is enabled
*/
public boolean isAutoLoad() {
return autoLoad;
}
/**
* The drawable that should be used to indicate progress while downloading
* the image. Corresponds to the view attribute
* android:indeterminateDrawable. If left blank, the platform's standard
* indeterminate progress drawable will be used.
*
* @return the progress drawable
*/
public Drawable getProgressDrawable() {
return progressDrawable;
}
/**
* The drawable that will be shown when the image download fails.
* Corresponds to the view attribute ignition:errorDrawable. If left blank,
* a stock alert icon from the Android platform will be used.
*
* @return the error drawable
*/
public Drawable getErrorDrawable() {
return errorDrawable;
}
/**
* The progress view that is shown while the image is loaded.
*
* @return the progress view, default is a {@link ProgressBar}
*/
public View getProgressView() {
return progressViewContainer.getChildAt(0);
}
public static interface RemoteImageViewListener {
public void onImageLoaded(Bitmap bm);
}
/**
* Use this method to set a listener for events raised by the remote image
* view such as image loaded.
*
* @param listener
* an implementation of the {@link RemoteImageViewListener}
* interface
*/
public void setRemoteImageViewListener(RemoteImageViewListener listener) {
this.listener = listener;
}
}