1/**
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.volley.toolbox;
17
18import android.content.Context;
19import android.text.TextUtils;
20import android.util.AttributeSet;
21import android.view.ViewGroup.LayoutParams;
22import android.widget.ImageView;
23
24import com.android.volley.VolleyError;
25import com.android.volley.toolbox.ImageLoader.ImageContainer;
26import com.android.volley.toolbox.ImageLoader.ImageListener;
27
28/**
29 * Handles fetching an image from a URL as well as the life-cycle of the
30 * associated request.
31 */
32public class NetworkImageView extends ImageView {
33    /** The URL of the network image to load */
34    private String mUrl;
35
36    /**
37     * Resource ID of the image to be used as a placeholder until the network image is loaded.
38     */
39    private int mDefaultImageId;
40
41    /**
42     * Resource ID of the image to be used if the network response fails.
43     */
44    private int mErrorImageId;
45
46    /** Local copy of the ImageLoader. */
47    private ImageLoader mImageLoader;
48
49    /** Current ImageContainer. (either in-flight or finished) */
50    private ImageContainer mImageContainer;
51
52    public NetworkImageView(Context context) {
53        this(context, null);
54    }
55
56    public NetworkImageView(Context context, AttributeSet attrs) {
57        this(context, attrs, 0);
58    }
59
60    public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
61        super(context, attrs, defStyle);
62    }
63
64    /**
65     * Sets URL of the image that should be loaded into this view. Note that calling this will
66     * immediately either set the cached image (if available) or the default image specified by
67     * {@link NetworkImageView#setDefaultImageResId(int)} on the view.
68     *
69     * NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and
70     * {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling
71     * this function.
72     *
73     * @param url The URL that should be loaded into this ImageView.
74     * @param imageLoader ImageLoader that will be used to make the request.
75     */
76    public void setImageUrl(String url, ImageLoader imageLoader) {
77        mUrl = url;
78        mImageLoader = imageLoader;
79        // The URL has potentially changed. See if we need to load it.
80        loadImageIfNecessary(false);
81    }
82
83    /**
84     * Sets the default image resource ID to be used for this view until the attempt to load it
85     * completes.
86     */
87    public void setDefaultImageResId(int defaultImage) {
88        mDefaultImageId = defaultImage;
89    }
90
91    /**
92     * Sets the error image resource ID to be used for this view in the event that the image
93     * requested fails to load.
94     */
95    public void setErrorImageResId(int errorImage) {
96        mErrorImageId = errorImage;
97    }
98
99    /**
100     * Loads the image for the view if it isn't already loaded.
101     * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
102     */
103    void loadImageIfNecessary(final boolean isInLayoutPass) {
104        int width = getWidth();
105        int height = getHeight();
106        ScaleType scaleType = getScaleType();
107
108        boolean wrapWidth = false, wrapHeight = false;
109        if (getLayoutParams() != null) {
110            wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
111            wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
112        }
113
114        // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
115        // view, hold off on loading the image.
116        boolean isFullyWrapContent = wrapWidth && wrapHeight;
117        if (width == 0 && height == 0 && !isFullyWrapContent) {
118            return;
119        }
120
121        // if the URL to be loaded in this view is empty, cancel any old requests and clear the
122        // currently loaded image.
123        if (TextUtils.isEmpty(mUrl)) {
124            if (mImageContainer != null) {
125                mImageContainer.cancelRequest();
126                mImageContainer = null;
127            }
128            setDefaultImageOrNull();
129            return;
130        }
131
132        // if there was an old request in this view, check if it needs to be canceled.
133        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
134            if (mImageContainer.getRequestUrl().equals(mUrl)) {
135                // if the request is from the same URL, return.
136                return;
137            } else {
138                // if there is a pre-existing request, cancel it if it's fetching a different URL.
139                mImageContainer.cancelRequest();
140                setDefaultImageOrNull();
141            }
142        }
143
144        // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
145        int maxWidth = wrapWidth ? 0 : width;
146        int maxHeight = wrapHeight ? 0 : height;
147
148        // The pre-existing content of this view didn't match the current URL. Load the new image
149        // from the network.
150        ImageContainer newContainer = mImageLoader.get(mUrl,
151                new ImageListener() {
152                    @Override
153                    public void onErrorResponse(VolleyError error) {
154                        if (mErrorImageId != 0) {
155                            setImageResource(mErrorImageId);
156                        }
157                    }
158
159                    @Override
160                    public void onResponse(final ImageContainer response, boolean isImmediate) {
161                        // If this was an immediate response that was delivered inside of a layout
162                        // pass do not set the image immediately as it will trigger a requestLayout
163                        // inside of a layout. Instead, defer setting the image by posting back to
164                        // the main thread.
165                        if (isImmediate && isInLayoutPass) {
166                            post(new Runnable() {
167                                @Override
168                                public void run() {
169                                    onResponse(response, false);
170                                }
171                            });
172                            return;
173                        }
174
175                        if (response.getBitmap() != null) {
176                            setImageBitmap(response.getBitmap());
177                        } else if (mDefaultImageId != 0) {
178                            setImageResource(mDefaultImageId);
179                        }
180                    }
181                }, maxWidth, maxHeight, scaleType);
182
183        // update the ImageContainer to be the new bitmap container.
184        mImageContainer = newContainer;
185    }
186
187    private void setDefaultImageOrNull() {
188        if(mDefaultImageId != 0) {
189            setImageResource(mDefaultImageId);
190        }
191        else {
192            setImageBitmap(null);
193        }
194    }
195
196    @Override
197    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
198        super.onLayout(changed, left, top, right, bottom);
199        loadImageIfNecessary(true);
200    }
201
202    @Override
203    protected void onDetachedFromWindow() {
204        if (mImageContainer != null) {
205            // If the view was bound to an image request, cancel it and clear
206            // out the image from the view.
207            mImageContainer.cancelRequest();
208            setImageBitmap(null);
209            // also clear out the container so we can reload the image if necessary.
210            mImageContainer = null;
211        }
212        super.onDetachedFromWindow();
213    }
214
215    @Override
216    protected void drawableStateChanged() {
217        super.drawableStateChanged();
218        invalidate();
219    }
220}
221