package com.crossbow.volley;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.AlphaAnimation;
import android.webkit.URLUtil;
import android.widget.ImageView;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.ImageLoader;
import com.crossbow.volley.toolbox.Crossbow;
import com.crossbow.volley.toolbox.FileImageLoader;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.WeakHashMap;
/*
* Copyright (C) 2014 Patrick Doyle
*
* 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.
*/
/**
* Wraps the functionality of the image loader to provide a rich image loading api. Use a {@link com.crossbow.volley.CrossbowImage.Builder Builder} returned
* from {@link Crossbow#loadImage()} to create an instance of this class
*/
public class CrossbowImage implements ViewTreeObserver.OnPreDrawListener, ImageLoader.ImageListener, FileImageLoader.Listener {
private static Rect dirtyRect = new Rect(0, 0, 1, 1);
private static final int DEFAULT = -1;
private int fade = DEFAULT;
private String url = null;
private String file = null;
private int drawable = DEFAULT;
private Drawable defaultDrawable = null;
private Drawable errorDrawable = null;
private boolean dontClear = false;
private ImageView.ScaleType scaleType = ImageView.ScaleType.CENTER;
private ImageView.ScaleType errorScaleType = ImageView.ScaleType.CENTER;
private ImageView.ScaleType preScaleType = ImageView.ScaleType.CENTER;
private boolean dontScale = false;
private boolean debug = false;
private WeakReference<ImageView> imageView;
private Listener listener;
private PendingImage pendingImage;
private ImageLoader imageLoader;
private FileImageLoader fileImageLoader;
private int width = DEFAULT;
private int height = DEFAULT;
private static WeakHashMap<ImageView, CrossbowImage> inProgressLoads = new WeakHashMap<>();
private CrossbowImage(ImageLoader imageLoader, FileImageLoader fileImageLoader) {
this.imageLoader = imageLoader;
this.fileImageLoader = fileImageLoader;
}
public void cancelRequest() {
if (pendingImage != null) {
pendingImage.cancel();
}
}
public void load() {
ImageView imageView = this.imageView.get();
if (imageView != null) {
if(inProgressLoads.containsKey(imageView)) {
inProgressLoads.get(imageView).cancelRequest();
inProgressLoads.remove(imageView);
}
inProgressLoads.put(imageView, this);
imageView.getViewTreeObserver().addOnPreDrawListener(this);
imageView.invalidate(dirtyRect);
}
}
public void cancel(ImageView imageView) {
if(inProgressLoads.containsKey(imageView)) {
inProgressLoads.get(imageView).cancelRequest();
inProgressLoads.remove(imageView);
}
}
@Override
public boolean onPreDraw() {
ImageView view = this.imageView.get();
if (view == null) {
return true;
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
if (!this.hasSource()) {
setError(new VolleyError("No image source Is empty"));
return true;
}
if (!this.dontClear) {
view.setImageBitmap(null);
}
if (this.defaultDrawable != null) {
ScaleTypeDrawable defaultScale = new ScaleTypeDrawable(defaultDrawable, this.preScaleType);
view.setImageDrawable(defaultScale);
}
int width = 0;
int height = 0;
if(this.width != DEFAULT && this.height != DEFAULT) {
//Use provided width and height if provided
width = this.width;
height = this.height;
}
else if (!this.dontScale) {
//Use the raw image
width = view.getWidth();
height = view.getHeight();
}
loadImage(view, this.drawable, this.file, this.url, width, height);
return true;
}
private void loadImage(ImageView imageView, int drawableID, String file, String url, int width, int height) {
if(pendingImage != null) {
pendingImage.cancel();
}
pendingImage = new PendingImage();
if(url != null && file == null && drawableID == DEFAULT) {
pendingImage.imageContainer = imageLoader.get(url, this, width, height, scaleType);
}
else if(url == null && file != null && drawableID == DEFAULT) {
pendingImage.fileImageContainer = fileImageLoader.get(file, width, height, this);
}
else if(url == null && file == null && drawableID != DEFAULT) {
ScaleTypeDrawable defaultScale = new ScaleTypeDrawable(defaultDrawable, this.preScaleType);
imageView.setImageDrawable(defaultScale);
}
else {
onErrorResponse(new VolleyError("No image source defined"));
}
}
@VisibleForTesting
void setBitmap(Bitmap bitmap, boolean fromCache) {
ImageView imageView = this.imageView.get();
if (bitmap != null && imageView != null) {
Resources resources = imageView.getResources();
BitmapDrawable bitmapDrawable = new BitmapDrawable(resources, bitmap);
ScaleTypeDrawable bitmapScale = new ScaleTypeDrawable(bitmapDrawable, this.scaleType, this.debug && fromCache);
if (this.fade != DEFAULT && !fromCache) {
//Do a fade with an animation drawable
if (defaultDrawable != null) {
ScaleTypeDrawable defaultScale = new ScaleTypeDrawable(defaultDrawable, this.preScaleType);
Drawable[] layers = new Drawable[2];
layers[0] = defaultScale;
layers[1] = bitmapScale;
TransitionDrawable animationDrawable = new TransitionDrawable(layers);
imageView.setImageDrawable(animationDrawable);
animationDrawable.setCrossFadeEnabled(true);
animationDrawable.startTransition(this.fade);
}
else {
AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(this.fade);
imageView.setImageDrawable(bitmapScale);
imageView.startAnimation(alphaAnimation);
}
}
else {
//just set the bitmap
imageView.setImageDrawable(bitmapScale);
}
if (this.listener != null) {
this.listener.onLoad(true, fromCache, bitmap, imageView);
}
}
}
@VisibleForTesting
void setError(VolleyError volleyError) {
ImageView imageView = this.imageView.get();
if(imageView == null) {
return;
}
if (errorDrawable != null) {
ScaleTypeDrawable errorScaleDrawable = new ScaleTypeDrawable(errorDrawable, this.errorScaleType);
if(this.defaultDrawable != null && this.fade != DEFAULT) {
//have a default and a fade, do a cross fade;
ScaleTypeDrawable defaultScale = new ScaleTypeDrawable(defaultDrawable, this.preScaleType);
Drawable[] layers = new Drawable[2];
layers[0] = defaultScale;
layers[1] = errorScaleDrawable;
TransitionDrawable animationDrawable = new TransitionDrawable(layers);
animationDrawable.setCrossFadeEnabled(true);
imageView.setImageDrawable(animationDrawable);
animationDrawable.startTransition(this.fade);
}
else {
imageView.setImageDrawable(errorScaleDrawable);
}
}
else if (defaultDrawable != null) {
ScaleTypeDrawable defaultScale = new ScaleTypeDrawable(defaultDrawable, this.preScaleType);
imageView.setImageDrawable(defaultScale);
}
else {
imageView.setImageBitmap(null);
}
if (this.listener != null) {
this.listener.onLoad(false, false, null, imageView);
}
}
/**
* called when the network image loader returns
*/
@Override
public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
ImageView imageView = this.imageView.get();
if(imageView != null && response.getBitmap() != null) {
setBitmap(response.getBitmap(), isImmediate);
}
}
@Override
public void onErrorResponse(VolleyError error) {
setError(error);
}
/**
* Called when the file request returns
*/
@Override
public void onImageLoad(Bitmap bitmap, boolean fromCache) {
setBitmap(bitmap, fromCache);
}
@Override
public void onImageError(VolleyError error) {
setError(error);
}
private boolean hasSource() {
return !TextUtils.isEmpty(url) || !TextUtils.isEmpty(file) || drawable != DEFAULT;
}
public interface Listener {
public void onLoad(boolean success, boolean fromCache, Bitmap bitmap, ImageView imageView);
}
/**
* Builder used for the Imageload. This will be useful for when we have different load types when the file loaders are added.
*/
public static class Builder {
private final Context context;
private CrossbowImage crossbowImage;
private int defaultRes = DEFAULT;
private int errorRes = DEFAULT;
public Builder(Context context, ImageLoader imageLoader, FileImageLoader fileImageLoader) {
this.context = context.getApplicationContext();
crossbowImage = new CrossbowImage(imageLoader, fileImageLoader);
}
/**
* Sets a listener to allow you to see when the image loads
*/
public Builder listen(Listener listener) {
this.crossbowImage.listener = listener;
return this;
}
/**
* Sets the fade in duration for when the image loads.
*
* @param duration the duration to fade
*/
public Builder fade(int duration) {
this.crossbowImage.fade = duration;
return this;
}
/**
* Sets a pixel size for the image to be decoded to ignoring the size of the imageview
*/
public Builder pixelSize(int width, int height) {
this.crossbowImage.width = width;
this.crossbowImage.height = height;
return this;
}
/**
* Sets a density pixel size size for the image to be decoded to ignoring the size of the imageview
*/
public Builder dpSize(int width, int height) {
this.crossbowImage.width = width;
this.crossbowImage.height = height;
return this;
}
/**
* The drawable res to use when the image is loading
*
* @param defaultRes the drawable res to use
*/
public Builder placeholder(@DrawableRes int defaultRes) {
this.crossbowImage.defaultDrawable = context.getResources().getDrawable(defaultRes);
return this;
}
/**
* The drawable res to use when the image fails to load
*
* @param errorRes the drawable res to use
*/
public Builder error(@DrawableRes int errorRes) {
this.crossbowImage.errorDrawable = context.getResources().getDrawable(errorRes);
return this;
}
/**
* The drawable to use when the image is loading
*
* @param defaultDrawable the drawable res to use
*/
public Builder placeholder(Drawable defaultDrawable) {
this.crossbowImage.defaultDrawable = defaultDrawable;
return this;
}
/**
* The drawable to use when the image fails to load
*
* @param error the drawable res to use
*/
public Builder error(Drawable error) {
this.crossbowImage.errorDrawable = error;
return this;
}
/**
* Sets the image {@link ImageView.ScaleType Scaletype} to center crop when the image loads
*/
public Builder centerCrop() {
return scale(ImageView.ScaleType.CENTER_CROP);
}
/**
* Sets the image {@link ImageView.ScaleType Scaletype} to use when the image loads
*/
public Builder scale(ImageView.ScaleType scaleType) {
this.crossbowImage.scaleType = scaleType;
return this;
}
/**
* Sets the scale type for the default/placeholder image.
*
* @param preScaleType {@link ImageView.ScaleType Scaletype} to set.
*/
public Builder placeholderScale(ImageView.ScaleType preScaleType) {
this.crossbowImage.preScaleType = preScaleType;
return this;
}
/**
* Sets the scale type for the error image image.
*
* @param errorScaleType {@link ImageView.ScaleType Scaletype} to set.
*/
public Builder errorScale(ImageView.ScaleType errorScaleType) {
this.crossbowImage.errorScaleType = errorScaleType;
return this;
}
/**
* Dont remove the old image before setting the new image
*/
public Builder dontClear() {
this.crossbowImage.dontClear = true;
return this;
}
/**
* This will draw an extra green marker on the image if it was from the memory cache
*/
public Builder debug() {
this.crossbowImage.debug = true;
return this;
}
/**
* Loads the raw bitmap <b>Will cause the image to load the raw FULL SIZE image</b>
*/
public Builder dontScale() {
this.crossbowImage.dontScale = true;
return this;
}
/**
* Sets the image scaletype to fit center when the image loads
*/
public Builder fitInto() {
return scale(ImageView.ScaleType.FIT_CENTER);
}
/**
* @see #into(View, int)
* @param root root view to look in
* @param imageViewId id of the imageview
*/
public Builder into(View root, @IdRes int imageViewId) {
ImageView imageView = (ImageView) root.findViewById(imageViewId);
return into(imageView);
}
/**
* @see #into(View, int)
*/
public Builder into(Activity activty, @IdRes int imageViewId) {
ImageView imageView = (ImageView) activty.findViewById(imageViewId);
return into(imageView);
}
/**
* Sets the string source to load
*
* @param source the source for the image (file path or http/https url)
*/
public Builder source(String source) {
if(URLUtil.isNetworkUrl(source)) {
crossbowImage.url = source;
}
else {
crossbowImage.file = source;
}
return this;
}
/**
* Sets the file uri source to load
*
* @param source the source for the image (File Uri)
*/
public Builder source(Uri source) {
crossbowImage.file = new File(source.getPath()).getAbsolutePath();
return this;
}
/**
* Sets the drawable resource to load (This runs on the main thread)
*
* @param drawable the resource id of thr drawable to load
*/
public Builder source(int drawable) {
this.crossbowImage.drawable = drawable;
return this;
}
/**
* Starts the image load
*/
public CrossbowImage load() {
if(crossbowImage.imageView == null) {
throw new IllegalArgumentException("Image view must not by null");
}
crossbowImage.load();
return crossbowImage;
}
public Builder into(@NonNull ImageView imageView) {
this.crossbowImage.imageView = new WeakReference<ImageView>(imageView);
return this;
}
}
private static class PendingImage {
private ImageLoader.ImageContainer imageContainer;
private FileImageLoader.FileImageContainer fileImageContainer;
public void cancel() {
if(imageContainer != null) {
imageContainer.cancelRequest();
}
else if(fileImageContainer != null) {
fileImageContainer.cancel();
}
}
}
}