aboutsummaryrefslogtreecommitdiff
path: root/library/src/main/java/com/bumptech/glide/request/target/ViewTarget.java
blob: 5f77d534c3218478e7aeaf6bf0fd1dbce68d0d69 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package com.bumptech.glide.request.target;

import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.ListView;
import com.bumptech.glide.request.Request;

import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Set;

/**
 * A base {@link Target} for loading {@link Bitmap}s into {@link View}s that provides default implementations for most
 * most methods and can determine the size of views using a
 * {@link android.view.ViewTreeObserver.OnDrawListener}.
 *
 * <p>
 *     To detect {@link View} reuse in {@link ListView} or any {@link ViewGroup} that reuses views, this class uses the
 *     {@link View#setTag(Object)} method to store some metadata so that if a view is reused, any previous loads or
 *     resources from previous loads can be cancelled or reused.
 * </p>
 *
 * <p>
 *     Any calls to {@link View#setTag(Object)}} on a View given to this class will result in excessive allocations and
 *     and/or {@link IllegalArgumentException}s. If you must call {@link View#setTag(Object)} on a view, consider
 *     using {@link BaseTarget} or {@link SimpleTarget} instead.
 * </p>
 *
 * @param <T> The specific subclass of view wrapped by this target.
 */
public abstract class ViewTarget<T extends View, Z> implements Target<Z> {
    private static final String TAG = "ViewTarget";
    private final T view;
    private final SizeDeterminer sizeDeterminer;

    public ViewTarget(T view) {
        this.view = view;
        sizeDeterminer = new SizeDeterminer(view);
    }

    public T getView() {
        return view;
    }

    @Override
    public void getSize(SizeReadyCallback cb) {
        sizeDeterminer.getSize(cb);
    }

    @Override
    public void setRequest(Request request) {
        view.setTag(request);
    }

    @Override
    public Request getRequest() {
        Object tag = view.getTag();
        Request request = null;
        if (tag != null) {
            if (tag instanceof Request) {
                request = (Request) tag;
            } else {
                throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting");
            }
        }
        return request;
    }

    @Override
    public String toString() {
        return "Target for: " + view;
    }

    private static class SizeDeterminer {
        private final View view;
        private Set<SizeReadyCallback> cbs = new HashSet<SizeReadyCallback>();
        private SizeDeterminerLayoutListener layoutListener;

        public SizeDeterminer(View view) {
            this.view = view;
        }

        private void notifyCbs(int width, int height) {
            for (SizeReadyCallback cb : cbs) {
                cb.onSizeReady(width, height);
            }
            cbs.clear();
        }

        private void checkCurrentDimens() {
            if (cbs.isEmpty()) {
                return;
            }

            boolean calledCallback = true;
            ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
            if (isViewSizeValid()) {
                notifyCbs(view.getWidth(), view.getHeight());
            } else if (isLayoutParamsSizeValid()) {
                notifyCbs(layoutParams.width, layoutParams.height);
            } else {
                calledCallback = false;
            }

            if (calledCallback) {
                // Keep a reference to the layout listener and remove it here
                // rather than having the observer remove itself because the observer
                // we add the listener to will be almost immediately merged into
                // another observer and will therefore never be alive. If we instead
                // keep a reference to the listener and remove it here, we get the
                // current view tree observer and should succeed.
                ViewTreeObserver observer = view.getViewTreeObserver();
                if (observer.isAlive()) {
                    observer.removeOnPreDrawListener(layoutListener);
                }
            }
        }

        public void getSize(SizeReadyCallback cb) {
            ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
            if (isViewSizeValid()) {
                cb.onSizeReady(view.getWidth(), view.getHeight());
            } else if (isLayoutParamsSizeValid()) {
                cb.onSizeReady(layoutParams.width, layoutParams.height);
            } else if (isUsingWrapContent()) {
                WindowManager windowManager =
                        (WindowManager) view.getContext().getSystemService(Context.WINDOW_SERVICE);
                Display display = windowManager.getDefaultDisplay();
                final int width = display.getWidth();
                final int height = display.getHeight();
                if (Log.isLoggable(TAG, Log.WARN)) {
                    Log.w(TAG, "Trying to load image into ImageView using WRAP_CONTENT, defaulting to screen" +
                            " dimensions: [" + width + "x" + height + "]. Give the view an actual width and height " +
                            " for better performance.");
                }
                cb.onSizeReady(display.getWidth(), display.getHeight());
            } else {
                cbs.add(cb);
                final ViewTreeObserver observer = view.getViewTreeObserver();
                layoutListener = new SizeDeterminerLayoutListener(this);
                observer.addOnPreDrawListener(layoutListener);
            }
        }

        private boolean isViewSizeValid() {
            return view.getWidth() > 0 && view.getHeight() > 0;
        }

        private boolean isUsingWrapContent() {
            final ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
            return layoutParams != null && (layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT);
        }

        private boolean isLayoutParamsSizeValid() {
            final ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
            return layoutParams != null && (layoutParams.width > 0 && layoutParams.height > 0);
        }

        private static class SizeDeterminerLayoutListener implements ViewTreeObserver.OnPreDrawListener {
            private final WeakReference<SizeDeterminer> sizeDeterminerRef;

            public SizeDeterminerLayoutListener(SizeDeterminer sizeDeterminer) {
                sizeDeterminerRef = new WeakReference<SizeDeterminer>(sizeDeterminer);
            }

            @Override
            public boolean onPreDraw() {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "OnGlobalLayoutListener called listener=" + this);
                }
                SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
                if (sizeDeterminer != null) {
                    sizeDeterminer.checkCurrentDimens();
                }
                return true;
            }
        }
    }
}