/** * Copyright (C) 2013 The Android Open Source Project * * 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.android.volley.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.ViewGroup.LayoutParams; import android.widget.ImageView; import com.android.volley.Response.Listener; import com.android.volley.cache.SimpleImageLoader; import com.android.volley.error.VolleyError; import com.android.volley.misc.Utils; import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.ImageLoader.ImageContainer; import com.android.volley.toolbox.ImageLoader.ImageListener; /** * Handles fetching an image from a URL as well as the life-cycle of the * associated request. */ public class NetworkImageView extends ImageView { private final int[] attrsArray = { android.R.attr.src, }; private static final ColorDrawable transparentDrawable = new ColorDrawable( android.R.color.transparent); private static final int HALF_FADE_IN_TIME = Utils.ANIMATION_FADE_IN_TIME / 2; /** The URL of the network image to load */ protected String mUrl; /** * Resource ID of the image to be used as a placeholder until the network image is loaded. */ int mDefaultImageId; /** * Resource ID of the image to be used if the network response fails. */ int mErrorImageId; /** Local copy of the ImageLoader. */ protected ImageLoader mImageLoader; /** Current ImageContainer. (either in-flight or finished) */ protected ImageContainer mImageContainer; private boolean mFadeInImage = true; private int mMaxImageHeight = 0; private int mMaxImageWidth = 0; private Listener<Bitmap> mListener; public NetworkImageView(Context context) { this(context, null); } public NetworkImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NetworkImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final TypedArray a = context.obtainStyledAttributes(attrs, attrsArray); setDefaultImageResId(a.getResourceId(0, 0)); a.recycle(); } /** * Sets URL of the image that should be loaded into this view. Note that calling this will * immediately either set the cached image (if available) or the default image specified by * {@link NetworkImageView#setDefaultImageResId(int)} on the view. * * NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and * {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling * this function. * * @param url The URL that should be loaded into this ImageView. * @param imageLoader ImageLoader that will be used to make the request. */ public void setImageUrl(String url, ImageLoader imageLoader) { mUrl = url; mImageLoader = imageLoader; // The URL has potentially changed. See if we need to load it. loadImageIfNecessary(false); } public void setResetImageUrl(String url, ImageLoader imageLoader) { mImageContainer = null; mUrl = url; mImageLoader = imageLoader; // The URL has potentially changed. See if we need to load it. loadImageIfNecessary(false); } /** * Sets the default image resource ID to be used for this view until the attempt to load it * completes. */ public void setDefaultImageResId(int defaultImage) { mDefaultImageId = defaultImage; setDefaultImageOrNull(); } /** * Sets the error image resource ID to be used for this view in the event that the image * requested fails to load. */ public void setErrorImageResId(int errorImage) { mErrorImageId = errorImage; } public void setMaxImageSize(int maxImageWidth, int maxImageHeight) { mMaxImageWidth = maxImageWidth; mMaxImageHeight = maxImageHeight; } public void setMaxImageSize(int maxImageSize) { setMaxImageSize(maxImageSize, maxImageSize); } public void setFadeInImage(boolean fadeInImage) { mFadeInImage = fadeInImage; } public void setImageListener(Listener<Bitmap> listener) { mListener = listener; } public Listener<Bitmap> getImageListener() { return mListener; } /** * Loads the image for the view if it isn't already loaded. * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise. */ void loadImageIfNecessary(final boolean isInLayoutPass) { int width = getWidth(); int height = getHeight(); ScaleType scaleType = getScaleType(); boolean wrapWidth = false, wrapHeight = false; if (getLayoutParams() != null) { wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT; wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT; } // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content // view, hold off on loading the image. boolean isFullyWrapContent = wrapWidth && wrapHeight; if (width == 0 && height == 0 && !isFullyWrapContent) { return; } // if the URL to be loaded in this view is empty, cancel any old requests and clear the // currently loaded image. if (TextUtils.isEmpty(mUrl)) { if (mImageContainer != null) { mImageContainer.cancelRequest(); mImageContainer = null; } setDefaultImageOrNull(); return; } // if there was an old request in this view, check if it needs to be canceled. if (mImageContainer != null && mImageContainer.getRequestUrl() != null) { if (mImageContainer.getRequestUrl().equals(mUrl)) { // if the request is from the same URL, return. return; } else { // if there is a pre-existing request, cancel it if it's fetching a different URL. mImageContainer.cancelRequest(); setDefaultImageOrNull(); } } int maxWidth; int maxHeight; if(mImageLoader instanceof SimpleImageLoader){ final SimpleImageLoader loader = (SimpleImageLoader) mImageLoader; maxWidth = mMaxImageWidth == 0 ? loader.getMaxImageWidth() : mMaxImageWidth; maxHeight = mMaxImageHeight == 0 ? loader.getMaxImageHeight() : mMaxImageHeight; } else{ // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens. maxWidth = wrapWidth ? 0 : width; maxHeight = wrapHeight ? 0 : height; } // The pre-existing content of this view didn't match the current URL. Load the new image // from the network. ImageContainer newContainer = mImageLoader.get(mUrl, new ImageListener() { @Override public void onErrorResponse(VolleyError error) { if (mErrorImageId != 0) { setImageResource(mErrorImageId); } } @Override public void onResponse(final ImageContainer response, boolean isImmediate) { // If this was an immediate response that was delivered inside of a layout // pass do not set the image immediately as it will trigger a requestLayout // inside of a layout. Instead, defer setting the image by posting back to // the main thread. if (isImmediate && isInLayoutPass) { post(new Runnable() { @Override public void run() { onResponse(response, false); } }); return; } if (response.getBitmap() != null) { //setImageBitmap(response.getBitmap(), mFadeInImage); setAnimateImageBitmap(response.getBitmap(), mFadeInImage && !isImmediate); if(null != mListener){ mListener.onResponse(response.getBitmap()); } } else if (mDefaultImageId != 0) { setImageResource(mDefaultImageId); } } }, maxWidth, maxHeight, scaleType); // update the ImageContainer to be the new bitmap container. mImageContainer = newContainer; } @SuppressLint("NewApi") private void setAnimateImageBitmap(final Bitmap bitmap, boolean fadeIn) { // If we're fading in and on HC MR1+ if (fadeIn && Utils.hasHoneycombMR1()) { // Use ViewPropertyAnimator to run a simple fade in + fade out animation to update the // ImageView animate() .scaleY(0.95f) .scaleX(0.95f) .alpha(0f) .setDuration(getDrawable() == null ? 0 : HALF_FADE_IN_TIME) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { setImageBitmap(bitmap); animate() .alpha(1f) .scaleY(1f) .scaleX(1f) .setDuration(HALF_FADE_IN_TIME) .setListener(null); } }); } else if (fadeIn) { // Otherwise use a TransitionDrawable to fade in Drawable initialDrawable; if (getDrawable() != null) { initialDrawable = getDrawable(); } else { initialDrawable = transparentDrawable; } BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), bitmap); // Use TransitionDrawable to fade in final TransitionDrawable td = new TransitionDrawable(new Drawable[] { initialDrawable, bitmapDrawable }); setImageDrawable(td); td.startTransition(Utils.ANIMATION_FADE_IN_TIME); } else { // No fade in, just set bitmap directly setImageBitmap(bitmap); } } protected void setDefaultImageOrNull() { if (mDefaultImageId != 0) { setImageResource(mDefaultImageId); } else { setImageBitmap(null); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mImageLoader != null) { loadImageIfNecessary(true); } } @Override protected void onDetachedFromWindow() { if (mImageContainer != null) { // If the view was bound to an image request, cancel it and clear // out the image from the view. mImageContainer.cancelRequest(); setImageBitmap(null); // also clear out the container so we can reload the image if necessary. mImageContainer = null; } super.onDetachedFromWindow(); } @Override protected void drawableStateChanged() { super.drawableStateChanged(); invalidate(); } }