ViewTarget.java 20.6 KB
Newer Older
S
Sam Judd 已提交
1
package com.bumptech.glide.request.target;
2

3 4
import android.content.Context;
import android.graphics.Point;
5
import android.graphics.drawable.Drawable;
6
import android.support.annotation.CallSuper;
A
Alex Saveau 已提交
7
import android.support.annotation.NonNull;
8
import android.support.annotation.Nullable;
9
import android.support.annotation.VisibleForTesting;
10
import android.util.Log;
11
import android.view.Display;
12
import android.view.View;
13
import android.view.View.OnAttachStateChangeListener;
S
Sam Judd 已提交
14
import android.view.ViewGroup.LayoutParams;
15
import android.view.ViewTreeObserver;
16
import android.view.WindowManager;
S
Sam Judd 已提交
17
import com.bumptech.glide.request.Request;
S
Sam Judd 已提交
18
import com.bumptech.glide.util.Preconditions;
19
import com.bumptech.glide.util.Synthetic;
20
import java.lang.ref.WeakReference;
21 22
import java.util.ArrayList;
import java.util.List;
23 24

/**
S
Sam Judd 已提交
25 26 27
 * A base {@link Target} for loading {@link android.graphics.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}.
28
 *
29
 * <p>To detect {@link View} reuse in {@link android.widget.ListView} or any {@link
S
Sam Judd 已提交
30 31
 * android.view.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
32
 * loads can be cancelled or reused.
33
 *
34
 * <p>Any calls to {@link View#setTag(Object)}} on a View given to this class will result in
S
Sam Judd 已提交
35
 * excessive allocations and and/or {@link IllegalArgumentException}s. If you must call {@link
S
Sam Judd 已提交
36
 * View#setTag(Object)} on a view, use {@link #setTagId(int)} to specify a custom tag for Glide to
37
 * use.
38
 *
39
 * <p>Subclasses must call super in {@link #onLoadCleared(Drawable)}
40
 *
41
 * @param <T> The specific subclass of view wrapped by this target.
S
Sam Judd 已提交
42
 * @param <Z> The resource type this target will receive.
43 44 45
 * @deprecated Use {@link CustomViewTarget}. Using this class is unsafe without implementing {@link
 *     #onLoadCleared} and results in recycled bitmaps being referenced from the UI and hard to
 *     root-cause crashes.
46
 */
47
@Deprecated
48
public abstract class ViewTarget<T extends View, Z> extends BaseTarget<Z> {
S
Sam Judd 已提交
49
  private static final String TAG = "ViewTarget";
R
Róbert Papp (TWiStErRob) 已提交
50 51
  private static boolean isTagUsedAtLeastOnce;
  @Nullable private static Integer tagId;
R
Robert Papp 已提交
52

S
Sam Judd 已提交
53 54
  protected final T view;
  private final SizeDeterminer sizeDeterminer;
55 56 57 58 59
  @Nullable
  private OnAttachStateChangeListener attachStateListener;
  private boolean isClearedByUs;
  private boolean isAttachStateListenerAdded;

60

61 62 63
  /**
   * Constructor that defaults {@code waitForLayout} to {@code false}.
   */
A
Alex Saveau 已提交
64
  public ViewTarget(@NonNull T view) {
65 66
    this.view = Preconditions.checkNotNull(view);
    sizeDeterminer = new SizeDeterminer(view);
67 68
  }

69 70 71 72 73 74 75 76 77
  /**
   * @param waitForLayout If set to {@code true}, Glide will always wait for any pending layout pass
   * before checking for the size a View. If set to {@code false} Glide will only wait for a pending
   * layout pass if it's unable to resolve the size from layout parameters or an existing View size.
   * Because setting this parameter to {@code true} forces Glide to wait for the layout pass to
   * occur before starting the load, setting this parameter to {@code true} can cause flashing in
   * some cases and should be used sparingly. If layout parameters are set to fixed sizes, they will
   * still be used instead of the View's dimensions even if this parameter is set to {@code true}.
   * This parameter is a fallback only.
78 79
   *
   * @deprecated Use {@link #waitForLayout()} instead.
80
   */
R
Róbert Papp (TWiStErRob) 已提交
81
  @SuppressWarnings("WeakerAccess") // Public API
82
  @Deprecated
A
Alex Saveau 已提交
83
  public ViewTarget(@NonNull T view, boolean waitForLayout) {
84 85 86 87
    this(view);
    if (waitForLayout) {
      waitForLayout();
    }
S
Sam Judd 已提交
88
  }
89

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
  /**
   * Clears the {@link View}'s {@link Request} when the {@link View} is detached from its
   * {@link android.view.Window} and restarts the {@link Request} when the {@link View} is
   * re-attached from its {@link android.view.Window}.
   *
   * <p>This is an experimental API that may be removed in a future version.
   *
   * <p>Using this method can save memory by allowing Glide to more eagerly clear resources when
   * transitioning screens or swapping adapters in scrolling views. However it also substantially
   * increases the odds that images will not be in memory if users subsequently return to a screen
   * where images were previously loaded. Whether or not this happens will depend on the number
   * of images loaded in the new screen and the size of the memory cache. Increasing the size of
   * the memory cache can improve this behavior but it largely negates the memory benefits of using
   * this method.
   *
   * <p>Use this method with caution and measure your memory usage to ensure that it's actually
   * improving your memory usage in the cases you care about.
   */
  // Public API.
A
Alex Saveau 已提交
109
  @NonNull
110 111 112 113 114 115 116 117
  @SuppressWarnings({"UnusedReturnValue", "WeakerAccess"})
  public final ViewTarget<T, Z> clearOnDetach() {
    if (attachStateListener != null) {
      return this;
    }
    attachStateListener = new OnAttachStateChangeListener() {
      @Override
      public void onViewAttachedToWindow(View v) {
118
        resumeMyRequest();
119 120 121 122
      }

      @Override
      public void onViewDetachedFromWindow(View v) {
123
        pauseMyRequest();
124 125 126 127 128 129
      }
    };
    maybeAddAttachStateListener();
    return this;
  }

130 131 132
  @SuppressWarnings("WeakerAccess")
  @Synthetic void resumeMyRequest() {
    Request request = getRequest();
133
    if (request != null && request.isCleared()) {
134 135 136 137 138 139 140
      request.begin();
    }
  }

  @SuppressWarnings("WeakerAccess")
  @Synthetic void pauseMyRequest() {
    Request request = getRequest();
141 142 143
    // If the Request were cleared by the developer, it would be null here. The only way it's
    // present is if the developer hasn't previously cleared this Target.
    if (request != null) {
144
      isClearedByUs = true;
145
      request.clear();
146 147 148 149
      isClearedByUs = false;
    }
  }

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
  /**
   * Indicates that Glide should always wait for any pending layout pass before checking
   * for the size an {@link View}.
   *
   * <p>By default, Glide will only wait for a pending layout pass if it's unable to resolve the
   * size from the {@link LayoutParams} or valid non-zero values for {@link View#getWidth()} and
   * {@link View#getHeight()}.
   *
   * <p>Because calling this method forces Glide to wait for the layout pass to occur before
   * starting loads, setting this parameter to {@code true} can cause Glide to asynchronous load
   * an image even if it's in the memory cache. The load will happen asynchronously because Glide
   * has to wait for a layout pass to occur, which won't necessarily happen in the same frame as
   * when the image is requested. As a result, using this method can resulting in flashing in some
   * cases and should be used sparingly.
   *
   * <p>If the {@link LayoutParams} of the wrapped {@link View} are set to fixed sizes, they will
   * still be used instead of the {@link View}'s dimensions even if this method is called. This
   * parameter is a fallback only.
   */
R
Róbert Papp (TWiStErRob) 已提交
169
  @SuppressWarnings("WeakerAccess") // Public API
A
Alex Saveau 已提交
170
  @NonNull
171 172 173 174 175
  public final ViewTarget<T, Z> waitForLayout() {
    sizeDeterminer.waitForLayout = true;
    return this;
  }

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
  @CallSuper
  @Override
  public void onLoadStarted(@Nullable Drawable placeholder) {
    super.onLoadStarted(placeholder);
    maybeAddAttachStateListener();
  }

  private void maybeAddAttachStateListener() {
    if (attachStateListener == null || isAttachStateListenerAdded) {
      return;
    }

    view.addOnAttachStateChangeListener(attachStateListener);
    isAttachStateListenerAdded = true;
  }

  private void maybeRemoveAttachStateListener() {
    if (attachStateListener == null || !isAttachStateListenerAdded) {
      return;
    }

    view.removeOnAttachStateChangeListener(attachStateListener);
    isAttachStateListenerAdded = false;
  }

S
Sam Judd 已提交
201 202 203
  /**
   * Returns the wrapped {@link android.view.View}.
   */
A
Alex Saveau 已提交
204
  @NonNull
S
Sam Judd 已提交
205 206 207
  public T getView() {
    return view;
  }
208

S
Sam Judd 已提交
209 210 211 212 213 214 215 216 217
  /**
   * Determines the size of the view by first checking {@link android.view.View#getWidth()} and
   * {@link android.view.View#getHeight()}. If one or both are zero, it then checks the view's
   * {@link LayoutParams}. If one or both of the params width and height are less than or equal to
   * zero, it then adds an {@link android.view.ViewTreeObserver.OnPreDrawListener} which waits until
   * the view has been measured before calling the callback with the view's drawn width and height.
   *
   * @param cb {@inheritDoc}
   */
218
  @CallSuper
S
Sam Judd 已提交
219
  @Override
A
Alex Saveau 已提交
220
  public void getSize(@NonNull SizeReadyCallback cb) {
S
Sam Judd 已提交
221 222
    sizeDeterminer.getSize(cb);
  }
223

224
  @CallSuper
225
  @Override
A
Alex Saveau 已提交
226
  public void removeCallback(@NonNull SizeReadyCallback cb) {
227 228 229
    sizeDeterminer.removeCallback(cb);
  }

230
  @CallSuper
231
  @Override
A
Alex Saveau 已提交
232
  public void onLoadCleared(@Nullable Drawable placeholder) {
233 234
    super.onLoadCleared(placeholder);
    sizeDeterminer.clearCallbacksAndListener();
235 236 237 238

    if (!isClearedByUs) {
      maybeRemoveAttachStateListener();
    }
239 240
  }

S
Sam Judd 已提交
241 242 243 244 245 246
  /**
   * Stores the request using {@link View#setTag(Object)}.
   *
   * @param request {@inheritDoc}
   */
  @Override
247
  public void setRequest(@Nullable Request request) {
S
Sam Judd 已提交
248
    setTag(request);
S
Sam Judd 已提交
249
  }
250

S
Sam Judd 已提交
251 252 253 254
  /**
   * Returns any stored request using {@link android.view.View#getTag()}.
   *
   * <p> For Glide to function correctly, Glide must be the only thing that calls {@link
S
Sam Judd 已提交
255
   * View#setTag(Object)}. If the tag is cleared or put to another object type, Glide will not be
S
Sam Judd 已提交
256 257 258 259 260 261 262
   * able to retrieve and cancel previous loads which will not only prevent Glide from reusing
   * resource, but will also result in incorrect images being loaded and lots of flashing of images
   * in lists. As a result, this will throw an {@link java.lang.IllegalArgumentException} if {@link
   * android.view.View#getTag()}} returns a non null object that is not an {@link
   * com.bumptech.glide.request.Request}. </p>
   */
  @Override
263
  @Nullable
S
Sam Judd 已提交
264
  public Request getRequest() {
S
Sam Judd 已提交
265
    Object tag = getTag();
S
Sam Judd 已提交
266 267 268 269 270 271 272 273
    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");
      }
274
    }
S
Sam Judd 已提交
275 276
    return request;
  }
277

S
Sam Judd 已提交
278 279 280 281
  @Override
  public String toString() {
    return "Target for: " + view;
  }
282

283
  private void setTag(@Nullable Object tag) {
S
Sam Judd 已提交
284 285 286 287 288
    if (tagId == null) {
      isTagUsedAtLeastOnce = true;
      view.setTag(tag);
    } else {
      view.setTag(tagId, tag);
289
    }
S
Sam Judd 已提交
290
  }
291

292
  @Nullable
S
Sam Judd 已提交
293 294 295 296 297
  private Object getTag() {
    if (tagId == null) {
      return view.getTag();
    } else {
      return view.getTag(tagId);
298
    }
S
Sam Judd 已提交
299
  }
300

S
Sam Judd 已提交
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
  /**
   * Sets the android resource id to use in conjunction with {@link View#setTag(int, Object)}
   * to store temporary state allowing loads to be automatically cancelled and resources re-used
   * in scrolling lists.
   *
   * <p>
   *   If no tag id is set, Glide will use {@link View#setTag(Object)}.
   * </p>
   *
   * <p>
   *   Warning: prior to Android 4.0 tags were stored in a static map. Using this method prior
   *   to Android 4.0 may cause memory leaks and isn't recommended. If you do use this method
   *   on older versions, be sure to call {@link com.bumptech.glide.RequestManager#clear(View)} on
   *   any view you start a load into to ensure that the static state is removed.
   * </p>
   *
   * @param tagId The android resource to use.
   */
S
Sam Judd 已提交
319 320
  // Public API.
  @SuppressWarnings("unused")
S
Sam Judd 已提交
321
  public static void setTagId(int tagId) {
A
Alex Saveau 已提交
322 323 324 325 326
    if (ViewTarget.tagId != null || isTagUsedAtLeastOnce) {
      throw new IllegalArgumentException("You cannot set the tag id more than once or change"
          + " the tag id after the first request has been made");
    }
    ViewTarget.tagId = tagId;
S
Sam Judd 已提交
327
  }
S
Sam Judd 已提交
328

329 330
  @VisibleForTesting
  static final class SizeDeterminer {
331
    // Some negative sizes (Target.SIZE_ORIGINAL) are valid, 0 is never valid.
S
Sam Judd 已提交
332
    private static final int PENDING_SIZE = 0;
333 334 335
    @VisibleForTesting
    @Nullable
    static Integer maxDisplayLength;
S
Sam Judd 已提交
336 337
    private final View view;
    private final List<SizeReadyCallback> cbs = new ArrayList<>();
338
    @Synthetic boolean waitForLayout;
S
Sam Judd 已提交
339

340
    @Nullable private SizeDeterminerLayoutListener layoutListener;
S
Sam Judd 已提交
341

A
Alex Saveau 已提交
342
    SizeDeterminer(@NonNull View view) {
S
Sam Judd 已提交
343
      this.view = view;
344 345 346
    }

    // Use the maximum to avoid depending on the device's current orientation.
A
Alex Saveau 已提交
347
    private static int getMaxDisplayLength(@NonNull Context context) {
348 349 350
      if (maxDisplayLength == null) {
        WindowManager windowManager =
            (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
S
Sam Judd 已提交
351
        Display display = Preconditions.checkNotNull(windowManager).getDefaultDisplay();
352 353 354 355 356
        Point displayDimensions = new Point();
        display.getSize(displayDimensions);
        maxDisplayLength = Math.max(displayDimensions.x, displayDimensions.y);
      }
      return maxDisplayLength;
357 358
    }

S
Sam Judd 已提交
359
    private void notifyCbs(int width, int height) {
360 361 362 363 364
      // One or more callbacks may trigger the removal of one or more additional callbacks, so we
      // need a copy of the list to avoid a concurrent modification exception. One place this
      // happens is when a full request completes from the in memory cache while its thumbnail is
      // still being loaded asynchronously. See #2237.
      for (SizeReadyCallback cb : new ArrayList<>(cbs)) {
S
Sam Judd 已提交
365 366
        cb.onSizeReady(width, height);
      }
367
    }
368

369 370
    @Synthetic
    void checkCurrentDimens() {
S
Sam Judd 已提交
371 372 373
      if (cbs.isEmpty()) {
        return;
      }
S
Sam Judd 已提交
374

375 376 377
      int currentWidth = getTargetWidth();
      int currentHeight = getTargetHeight();
      if (!isViewStateAndSizeValid(currentWidth, currentHeight)) {
S
Sam Judd 已提交
378 379
        return;
      }
380

S
Sam Judd 已提交
381
      notifyCbs(currentWidth, currentHeight);
382
      clearCallbacksAndListener();
S
Sam Judd 已提交
383
    }
384

A
Alex Saveau 已提交
385
    void getSize(@NonNull SizeReadyCallback cb) {
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
      int currentWidth = getTargetWidth();
      int currentHeight = getTargetHeight();
      if (isViewStateAndSizeValid(currentWidth, currentHeight)) {
        cb.onSizeReady(currentWidth, currentHeight);
        return;
      }

      // We want to notify callbacks in the order they were added and we only expect one or two
      // callbacks to be added a time, so a List is a reasonable choice.
      if (!cbs.contains(cb)) {
        cbs.add(cb);
      }
      if (layoutListener == null) {
        ViewTreeObserver observer = view.getViewTreeObserver();
        layoutListener = new SizeDeterminerLayoutListener(this);
        observer.addOnPreDrawListener(layoutListener);
S
Sam Judd 已提交
402 403
      }
    }
404

405 406 407 408 409 410
    /**
     * The callback may be called anyway if it is removed by another {@link SizeReadyCallback} or
     * otherwise removed while we're notifying the list of callbacks.
     *
     * <p>See #2237.
     */
A
Alex Saveau 已提交
411
    void removeCallback(@NonNull SizeReadyCallback cb) {
412 413 414
      cbs.remove(cb);
    }

415
    void clearCallbacksAndListener() {
416
      // Keep a reference to the layout attachStateListener and remove it here
417
      // rather than having the observer remove itself because the observer
418
      // we add the attachStateListener to will be almost immediately merged into
419
      // another observer and will therefore never be alive. If we instead
420
      // keep a reference to the attachStateListener and remove it here, we get the
421 422 423 424 425 426 427 428 429
      // current view tree observer and should succeed.
      ViewTreeObserver observer = view.getViewTreeObserver();
      if (observer.isAlive()) {
        observer.removeOnPreDrawListener(layoutListener);
      }
      layoutListener = null;
      cbs.clear();
    }

430 431
    private boolean isViewStateAndSizeValid(int width, int height) {
      return isDimensionValid(width) && isDimensionValid(height);
432 433
    }

434 435 436 437 438
    private int getTargetHeight() {
      int verticalPadding = view.getPaddingTop() + view.getPaddingBottom();
      LayoutParams layoutParams = view.getLayoutParams();
      int layoutParamSize = layoutParams != null ? layoutParams.height : PENDING_SIZE;
      return getTargetDimen(view.getHeight(), layoutParamSize, verticalPadding);
S
Sam Judd 已提交
439
    }
S
Sam Judd 已提交
440

441 442 443 444 445
    private int getTargetWidth() {
      int horizontalPadding = view.getPaddingLeft() + view.getPaddingRight();
      LayoutParams layoutParams = view.getLayoutParams();
      int layoutParamSize = layoutParams != null ? layoutParams.width : PENDING_SIZE;
      return getTargetDimen(view.getWidth(), layoutParamSize, horizontalPadding);
S
Sam Judd 已提交
446
    }
S
Sam Judd 已提交
447

448
    private int getTargetDimen(int viewSize, int paramSize, int paddingSize) {
449 450 451 452 453 454 455 456
      // We consider the View state as valid if the View has non-null layout params and a non-zero
      // layout params width and height. This is imperfect. We're making an assumption that View
      // parents will obey their child's layout parameters, which isn't always the case.
      int adjustedParamSize = paramSize - paddingSize;
      if (adjustedParamSize > 0) {
        return adjustedParamSize;
      }

457 458 459 460 461 462 463
      // Since we always prefer layout parameters with fixed sizes, even if waitForLayout is true,
      // we might as well ignore it and just return the layout parameters above if we have them.
      // Otherwise we should wait for a layout pass before checking the View's dimensions.
      if (waitForLayout && view.isLayoutRequested()) {
        return PENDING_SIZE;
      }

464 465 466 467 468 469 470
      // We also consider the View state valid if the View has a non-zero width and height. This
      // means that the View has gone through at least one layout pass. It does not mean the Views
      // width and height are from the current layout pass. For example, if a View is re-used in
      // RecyclerView or ListView, this width/height may be from an old position. In some cases
      // the dimensions of the View at the old position may be different than the dimensions of the
      // View in the new position because the LayoutManager/ViewParent can arbitrarily decide to
      // change them. Nevertheless, in most cases this should be a reasonable choice.
471
      int adjustedViewSize = viewSize - paddingSize;
472
      if (adjustedViewSize > 0) {
473
        return adjustedViewSize;
S
Sam Judd 已提交
474
      }
475 476 477

      // Finally we consider the view valid if the layout parameter size is set to wrap_content.
      // It's difficult for Glide to figure out what to do here. Although Target.SIZE_ORIGINAL is a
478 479 480
      // coherent choice, it's extremely dangerous because original images may be much too large to
      // fit in memory or so large that only a couple can fit in memory, causing OOMs. If users want
      // the original image, they can always use .override(Target.SIZE_ORIGINAL). Since wrap_content
481 482 483 484
      // may never resolve to a real size unless we load something, we aim for a square whose length
      // is the largest screen size. That way we're loading something and that something has some
      // hope of being downsampled to a size that the device can support. We also log a warning that
      // tries to explain what Glide is doing and why some alternatives are preferable.
485 486 487
      // Since WRAP_CONTENT is sometimes used as a default layout parameter, we always wait for
      // layout to complete before using this fallback parameter (ConstraintLayout among others).
      if (!view.isLayoutRequested() && paramSize == LayoutParams.WRAP_CONTENT) {
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
        if (Log.isLoggable(TAG, Log.INFO)) {
          Log.i(TAG, "Glide treats LayoutParams.WRAP_CONTENT as a request for an image the size of"
              + " this device's screen dimensions. If you want to load the original image and are"
              + " ok with the corresponding memory cost and OOMs (depending on the input size), use"
              + " .override(Target.SIZE_ORIGINAL). Otherwise, use LayoutParams.MATCH_PARENT, set"
              + " layout_width and layout_height to fixed dimension, or use .override() with fixed"
              + " dimensions.");
        }
        return getMaxDisplayLength(view.getContext());
      }

      // If the layout parameters are < padding, the view size is < padding, or the layout
      // parameters are set to match_parent or wrap_content and no layout has occurred, we should
      // wait for layout and repeat.
      return PENDING_SIZE;
    }

    private boolean isDimensionValid(int size) {
      return size > 0 || size == SIZE_ORIGINAL;
S
Sam Judd 已提交
507
    }
508

509 510
    private static final class SizeDeterminerLayoutListener
        implements ViewTreeObserver.OnPreDrawListener {
S
Sam Judd 已提交
511
      private final WeakReference<SizeDeterminer> sizeDeterminerRef;
512

A
Alex Saveau 已提交
513
      SizeDeterminerLayoutListener(@NonNull SizeDeterminer sizeDeterminer) {
S
Sam Judd 已提交
514 515
        sizeDeterminerRef = new WeakReference<>(sizeDeterminer);
      }
516

S
Sam Judd 已提交
517 518 519
      @Override
      public boolean onPreDraw() {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
520
          Log.v(TAG, "OnGlobalLayoutListener called attachStateListener=" + this);
S
Sam Judd 已提交
521 522 523 524
        }
        SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
        if (sizeDeterminer != null) {
          sizeDeterminer.checkCurrentDimens();
525
        }
S
Sam Judd 已提交
526 527
        return true;
      }
528
    }
S
Sam Judd 已提交
529
  }
530
}