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