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(); } } } }