ViewTarget.java revision 5ba19a0e69ad3a651b8f13ba45de48a56b56ce36
1package com.bumptech.glide.request.target;
2
3import android.content.Context;
4import android.util.Log;
5import android.view.Display;
6import android.view.View;
7import android.view.ViewGroup;
8import android.view.ViewTreeObserver;
9import android.view.WindowManager;
10import com.bumptech.glide.request.Request;
11
12import java.lang.ref.WeakReference;
13import java.util.ArrayList;
14import java.util.List;
15
16/**
17 * A base {@link Target} for loading {@link android.graphics.Bitmap}s into {@link View}s that provides default
18 * implementations for most most methods and can determine the size of views using a
19 * {@link android.view.ViewTreeObserver.OnDrawListener}.
20 *
21 * <p>
22 *     To detect {@link View} reuse in {@link android.widget.ListView} or any {@link ViewGroup} that reuses views, this
23 *     class uses the {@link View#setTag(Object)} method to store some metadata so that if a view is reused, any
24 *     previous loads or resources from previous loads can be cancelled or reused.
25 * </p>
26 *
27 * <p>
28 *     Any calls to {@link View#setTag(Object)}} on a View given to this class will result in excessive allocations and
29 *     and/or {@link IllegalArgumentException}s. If you must call {@link View#setTag(Object)} on a view, consider
30 *     using {@link BaseTarget} or {@link SimpleTarget} instead.
31 * </p>
32 *
33 * @param <T> The specific subclass of view wrapped by this target.
34 * @param <Z> The resource type this target will receive.
35 */
36public abstract class ViewTarget<T extends View, Z> extends BaseTarget<Z> {
37    private static final String TAG = "ViewTarget";
38
39    protected final T view;
40    private final SizeDeterminer sizeDeterminer;
41
42    public ViewTarget(T view) {
43        if (view == null) {
44            throw new NullPointerException("View must not be null!");
45        }
46        this.view = view;
47        sizeDeterminer = new SizeDeterminer(view);
48    }
49
50    /**
51     * Returns the wrapped {@link android.view.View}.
52     */
53    public T getView() {
54        return view;
55    }
56
57    /**
58     * Determines the size of the view by first checking {@link android.view.View#getWidth()} and
59     * {@link android.view.View#getHeight()}. If one or both are zero, it then checks the view's
60     * {@link android.view.ViewGroup.LayoutParams}. If one or both of the params width and height are less than or
61     * equal to zero, it then adds an {@link android.view.ViewTreeObserver.OnPreDrawListener} which waits until the view
62     * has been measured before calling the callback with the view's drawn width and height.
63     *
64     * @param cb {@inheritDoc}
65     */
66    @Override
67    public void getSize(SizeReadyCallback cb) {
68        sizeDeterminer.getSize(cb);
69    }
70
71    /**
72     * Stores the request using {@link View#setTag(Object)}.
73     *
74     * @param request {@inheritDoc}
75     */
76    @Override
77    public void setRequest(Request request) {
78        view.setTag(request);
79    }
80
81    /**
82     * Returns any stored request using {@link android.view.View#getTag()}.
83     *
84     * <p>
85     *     For Glide to function correctly, Glide must be the only thing that calls {@link View#setTag(Object)}. If the
86     *     tag is cleared or set to another object type, Glide will not be able to retrieve and cancel previous loads
87     *     which will not only prevent Glide from reusing resource, but will also result in incorrect images being
88     *     loaded and lots of flashing of images in lists. As a result, this will throw an
89     *     {@link java.lang.IllegalArgumentException} if {@link android.view.View#getTag()}} returns a non null object
90     *     that is not an {@link com.bumptech.glide.request.Request}.
91     * </p>
92     */
93    @Override
94    public Request getRequest() {
95        Object tag = view.getTag();
96        Request request = null;
97        if (tag != null) {
98            if (tag instanceof Request) {
99                request = (Request) tag;
100            } else {
101                throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting");
102            }
103        }
104        return request;
105    }
106
107    @Override
108    public String toString() {
109        return "Target for: " + view;
110    }
111
112    private static class SizeDeterminer {
113        private final View view;
114        private final List<SizeReadyCallback> cbs = new ArrayList<SizeReadyCallback>();
115        private SizeDeterminerLayoutListener layoutListener;
116
117        public SizeDeterminer(View view) {
118            this.view = view;
119        }
120
121        private void notifyCbs(int width, int height) {
122            for (SizeReadyCallback cb : cbs) {
123                cb.onSizeReady(width, height);
124            }
125            cbs.clear();
126        }
127
128        private void checkCurrentDimens() {
129            if (cbs.isEmpty()) {
130                return;
131            }
132
133            boolean calledCallback = true;
134            ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
135            if (isViewSizeValid()) {
136                notifyCbs(view.getWidth(), view.getHeight());
137            } else if (isLayoutParamsSizeValid()) {
138                notifyCbs(layoutParams.width, layoutParams.height);
139            } else {
140                calledCallback = false;
141            }
142
143            if (calledCallback) {
144                // Keep a reference to the layout listener and remove it here
145                // rather than having the observer remove itself because the observer
146                // we add the listener to will be almost immediately merged into
147                // another observer and will therefore never be alive. If we instead
148                // keep a reference to the listener and remove it here, we get the
149                // current view tree observer and should succeed.
150                ViewTreeObserver observer = view.getViewTreeObserver();
151                if (observer.isAlive()) {
152                    observer.removeOnPreDrawListener(layoutListener);
153                }
154            }
155        }
156
157        public void getSize(SizeReadyCallback cb) {
158            ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
159            if (isViewSizeValid()) {
160                cb.onSizeReady(view.getWidth(), view.getHeight());
161            } else if (isLayoutParamsSizeValid()) {
162                cb.onSizeReady(layoutParams.width, layoutParams.height);
163            } else if (isUsingWrapContent()) {
164                WindowManager windowManager =
165                        (WindowManager) view.getContext().getSystemService(Context.WINDOW_SERVICE);
166                Display display = windowManager.getDefaultDisplay();
167                @SuppressWarnings("deprecation") final int width = display.getWidth(), height = display.getHeight();
168                if (Log.isLoggable(TAG, Log.WARN)) {
169                    Log.w(TAG, "Trying to load image into ImageView using WRAP_CONTENT, defaulting to screen"
170                            + " dimensions: [" + width + "x" + height + "]. Give the view an actual width and height "
171                            + " for better performance.");
172                }
173                cb.onSizeReady(width, height);
174            } else {
175                if (cbs.contains(cb)) {
176                    throw new IllegalArgumentException("Cannot add a callback twice");
177                }
178                cbs.add(cb);
179                final ViewTreeObserver observer = view.getViewTreeObserver();
180                layoutListener = new SizeDeterminerLayoutListener(this);
181                observer.addOnPreDrawListener(layoutListener);
182            }
183        }
184
185        private boolean isViewSizeValid() {
186            return view.getWidth() > 0 && view.getHeight() > 0;
187        }
188
189        private boolean isUsingWrapContent() {
190            final ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
191            return layoutParams != null && (layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT
192                    || layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT);
193        }
194
195        private boolean isLayoutParamsSizeValid() {
196            final ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
197            return layoutParams != null && layoutParams.width > 0 && layoutParams.height > 0;
198        }
199
200        private static class SizeDeterminerLayoutListener implements ViewTreeObserver.OnPreDrawListener {
201            private final WeakReference<SizeDeterminer> sizeDeterminerRef;
202
203            public SizeDeterminerLayoutListener(SizeDeterminer sizeDeterminer) {
204                sizeDeterminerRef = new WeakReference<SizeDeterminer>(sizeDeterminer);
205            }
206
207            @Override
208            public boolean onPreDraw() {
209                if (Log.isLoggable(TAG, Log.VERBOSE)) {
210                    Log.v(TAG, "OnGlobalLayoutListener called listener=" + this);
211                }
212                SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
213                if (sizeDeterminer != null) {
214                    sizeDeterminer.checkCurrentDimens();
215                }
216                return true;
217            }
218        }
219    }
220}
221