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
107        boolean wrapWidth = false, wrapHeight = false;
108        if (getLayoutParams() != null) {
109            wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
110            wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
111        }
112
113        // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
114        // view, hold off on loading the image.
115        boolean isFullyWrapContent = wrapWidth && wrapHeight;
116        if (width == 0 && height == 0 && !isFullyWrapContent) {
117            return;
118        }
119
120        // if the URL to be loaded in this view is empty, cancel any old requests and clear the
121        // currently loaded image.
122        if (TextUtils.isEmpty(mUrl)) {
123            if (mImageContainer != null) {
124                mImageContainer.cancelRequest();
125                mImageContainer = null;
126            }
127            setDefaultImageOrNull();
128            return;
129        }
130
131        // if there was an old request in this view, check if it needs to be canceled.
132        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
133            if (mImageContainer.getRequestUrl().equals(mUrl)) {
134                // if the request is from the same URL, return.
135                return;
136            } else {
137                // if there is a pre-existing request, cancel it if it's fetching a different URL.
138                mImageContainer.cancelRequest();
139                setDefaultImageOrNull();
140            }
141        }
142
143        // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
144        int maxWidth = wrapWidth ? 0 : width;
145        int maxHeight = wrapHeight ? 0 : height;
146
147        // The pre-existing content of this view didn't match the current URL. Load the new image
148        // from the network.
149        ImageContainer newContainer = mImageLoader.get(mUrl,
150                new ImageListener() {
151                    @Override
152                    public void onErrorResponse(VolleyError error) {
153                        if (mErrorImageId != 0) {
154                            setImageResource(mErrorImageId);
155                        }
156                    }
157
158                    @Override
159                    public void onResponse(final ImageContainer response, boolean isImmediate) {
160                        // If this was an immediate response that was delivered inside of a layout
161                        // pass do not set the image immediately as it will trigger a requestLayout
162                        // inside of a layout. Instead, defer setting the image by posting back to
163                        // the main thread.
164                        if (isImmediate && isInLayoutPass) {
165                            post(new Runnable() {
166                                @Override
167                                public void run() {
168                                    onResponse(response, false);
169                                }
170                            });
171                            return;
172                        }
173
174                        if (response.getBitmap() != null) {
175                            setImageBitmap(response.getBitmap());
176                        } else if (mDefaultImageId != 0) {
177                            setImageResource(mDefaultImageId);
178                        }
179                    }
180                }, maxWidth, maxHeight);
181
182        // update the ImageContainer to be the new bitmap container.
183        mImageContainer = newContainer;
184    }
185
186    private void setDefaultImageOrNull() {
187        if(mDefaultImageId != 0) {
188            setImageResource(mDefaultImageId);
189        }
190        else {
191            setImageBitmap(null);
192        }
193    }
194
195    @Override
196    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
197        super.onLayout(changed, left, top, right, bottom);
198        loadImageIfNecessary(true);
199    }
200
201    @Override
202    protected void onDetachedFromWindow() {
203        if (mImageContainer != null) {
204            // If the view was bound to an image request, cancel it and clear
205            // out the image from the view.
206            mImageContainer.cancelRequest();
207            setImageBitmap(null);
208            // also clear out the container so we can reload the image if necessary.
209            mImageContainer = null;
210        }
211        super.onDetachedFromWindow();
212    }
213
214    @Override
215    protected void drawableStateChanged() {
216        super.drawableStateChanged();
217        invalidate();
218    }
219}
220