提交 3a706077 编写于 作者: A azlatin 提交者: Sam Judd

Create a CustomViewTarget to replace ViewTarget.

The two main differences are:
- It forces you to override the method where resources must be cleared. Not doing so results in recycled bitmaps being used and crashing apps. Not doing so was a common pattern among developers optimizing for lines of code instead of correctness.
- No more setTag(object) use. Glide now targets 14+ which can safely use the id tag variant and avoid another class of runtime bugs caused by developers optimizing for lines of code instead of correctness by calling setTag() and overwriting Glide's data.

Finally, we deprecate ViewTarget, SimpleTarget and BaseTarget. Apps should primarily be using Target, CustomViewTarget, ImageViewTarget and FutureTarget which either force the developer to implement all necessary methods, properly implement them themselves, or will not attempt to reclaim bitmaps. The deprecated classes continue to be used internally by some of the "correct" classes but can be merged down once the deprecated APIs are able to be removed.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=201729878
上级 b7c2b136
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.bumptech.glide">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bumptech.glide">
<uses-sdk android:minSdkVersion="14" />
<application/>
</manifest>
......@@ -8,16 +8,23 @@ import com.bumptech.glide.request.Request;
* A base {@link Target} for loading {@link com.bumptech.glide.load.engine.Resource}s that provides
* basic or empty implementations for most methods.
*
* <p> For maximum efficiency, clear this target when you have finished using or displaying the
* {@link com.bumptech.glide.load.engine.Resource} loaded into it using
* {@link com.bumptech.glide.RequestManager#clear(Target)}.</p>
* <p>For maximum efficiency, clear this target when you have finished using or displaying the
* {@link com.bumptech.glide.load.engine.Resource} loaded into it using {@link
* com.bumptech.glide.RequestManager#clear(Target)}.
*
* <p> For loading {@link com.bumptech.glide.load.engine.Resource}s into {@link android.view.View}s,
* {@link com.bumptech.glide.request.target.ViewTarget} or
* {@link com.bumptech.glide.request.target.ImageViewTarget} are preferable.</p>
* <p>For loading {@link com.bumptech.glide.load.engine.Resource}s into {@link android.view.View}s,
* {@link com.bumptech.glide.request.target.ViewTarget} or {@link
* com.bumptech.glide.request.target.ImageViewTarget} are preferable.
*
* @param <Z> The type of resource that will be received by this target.
* @deprecated Use {@link CustomViewTarget} if loading the content into a view, the download API if
* in the background
* (http://bumptech.github.io/glide/doc/getting-started.html#background-threads), or a a fully
* implemented {@link Target} for any specialized use-cases. Using BaseView is unsafe if the
* user does not implement {@link #onLoadCleared}, resulting in recycled bitmaps being
* referenced from the UI and hard to root-cause crashes.
*/
@Deprecated
public abstract class BaseTarget<Z> implements Target<Z> {
private Request request;
......
package com.bumptech.glide.request.target;
import android.content.Context;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import com.bumptech.glide.request.Request;
import com.bumptech.glide.request.transition.Transition;
import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Synthetic;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* A base {@link Target} for loading {@link android.graphics.Bitmap}s into {@link View}s that
* provides default implementations for most methods and can determine the size of views using a
* {@link android.view.ViewTreeObserver.OnDrawListener}.
*
* @param <T> The specific subclass of view wrapped by this target.
* @param <Z> The resource type this target will receive.
*/
public abstract class CustomViewTarget<T extends View, Z> implements Target<Z> {
private static final String TAG = "CustomViewTarget";
@IdRes private static final int VIEW_TAG_ID =
com.bumptech.glide.R.id.glide_custom_view_target_tag;
private final SizeDeterminer sizeDeterminer;
protected final T view;
@Nullable private OnAttachStateChangeListener attachStateListener;
private boolean isClearedByUs;
private boolean isAttachStateListenerAdded;
@IdRes private int overrideTag = 0;
/** Constructor that defaults {@code waitForLayout} to {@code false}. */
public CustomViewTarget(@NonNull T view) {
this.view = Preconditions.checkNotNull(view);
sizeDeterminer = new SizeDeterminer(view);
}
/**
* A required callback invoked when the resource is no longer valid and must be freed.
*
* <p>You must ensure that any current Drawable received in {@link #onResourceReady(Z,
* Transition)} is no longer used before redrawing the container (usually a View) or changing its
* visibility. <b>Not doing so will result in crashes in your app.</b>
*
* @param placeholder The placeholder drawable to optionally show, or null.
*/
protected abstract void onResourceCleared(@Nullable Drawable placeholder);
/**
* An optional callback invoked when a resource load is started.
*
* @see Target#onLoadStarted(Drawable)
* @param placeholder The placeholder drawable to optionally show, or null.
*/
protected void onResourceLoading(@Nullable Drawable placeholder) {}
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onDestroy() {}
/**
* 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.
*/
@SuppressWarnings("WeakerAccess") // Public API
@NonNull
public final CustomViewTarget<T, Z> waitForLayout() {
sizeDeterminer.waitForLayout = true;
return this;
}
/**
* 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.
@NonNull
@SuppressWarnings({"UnusedReturnValue", "WeakerAccess"})
public final CustomViewTarget<T, Z> clearOnDetach() {
if (attachStateListener != null) {
return this;
}
attachStateListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
resumeMyRequest();
}
@Override
public void onViewDetachedFromWindow(View v) {
pauseMyRequest();
}
};
maybeAddAttachStateListener();
return this;
}
/**
* Override the android resource id to store temporary state allowing loads to be automatically
* cancelled and resources re-used in scrolling lists.
*
* <p>Unlike {@link ViewTarget}, it is <b>not</b> necessary to set a custom tag id if your app
* uses {@link View#setTag(Object)}. It is only necessary if loading several Glide resources into
* the same view, for example one foreground and one background view.
*
* @param tagId The android resource id to use.
*/
// Public API.
@SuppressWarnings({"UnusedReturnValue", "WeakerAccess"})
public final CustomViewTarget<T, Z> useTagId(@IdRes int tagId) {
if (this.overrideTag != 0) {
throw new IllegalArgumentException("You cannot change the tag id once it has been set.");
}
this.overrideTag = tagId;
return this;
}
/** Returns the wrapped {@link android.view.View}. */
@NonNull
public final T getView() {
return view;
}
/**
* 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}
*/
@Override
public final void getSize(@NonNull SizeReadyCallback cb) {
sizeDeterminer.getSize(cb);
}
@Override
public final void removeCallback(@NonNull SizeReadyCallback cb) {
sizeDeterminer.removeCallback(cb);
}
@Override
public final void onLoadStarted(@Nullable Drawable placeholder) {
maybeAddAttachStateListener();
onResourceLoading(placeholder);
}
@Override
public final void onLoadCleared(@Nullable Drawable placeholder) {
sizeDeterminer.clearCallbacksAndListener();
onResourceCleared(placeholder);
if (!isClearedByUs) {
maybeRemoveAttachStateListener();
}
}
/**
* Stores the request using {@link View#setTag(Object)}.
*
* @param request {@inheritDoc}
*/
@Override
public final void setRequest(@Nullable Request request) {
setTag(request);
}
/** Returns any stored request using {@link android.view.View#getTag()}. */
@Override
@Nullable
public final Request getRequest() {
Object tag = getTag();
if (tag != null) {
if (tag instanceof Request) {
return (Request) tag;
} else {
throw new IllegalArgumentException("You must not pass non-R.id ids to setTag(id)");
}
}
return null;
}
@Override
public String toString() {
return "Target for: " + view;
}
@SuppressWarnings("WeakerAccess")
@Synthetic
final void resumeMyRequest() {
Request request = getRequest();
if (request != null && request.isCleared()) {
request.begin();
}
}
@SuppressWarnings("WeakerAccess")
@Synthetic
final void pauseMyRequest() {
Request request = getRequest();
if (request != null) {
isClearedByUs = true;
request.clear();
isClearedByUs = false;
}
}
private void setTag(@Nullable Object tag) {
view.setTag(overrideTag == 0 ? VIEW_TAG_ID : overrideTag, tag);
}
@Nullable
private Object getTag() {
return view.getTag(overrideTag == 0 ? VIEW_TAG_ID : overrideTag);
}
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;
}
@VisibleForTesting
static final class SizeDeterminer {
// Some negative sizes (Target.SIZE_ORIGINAL) are valid, 0 is never valid.
private static final int PENDING_SIZE = 0;
@VisibleForTesting
@Nullable
static Integer maxDisplayLength;
private final View view;
private final List<SizeReadyCallback> cbs = new ArrayList<>();
@Synthetic boolean waitForLayout;
@Nullable private SizeDeterminerLayoutListener layoutListener;
SizeDeterminer(@NonNull View view) {
this.view = view;
}
// Use the maximum to avoid depending on the device's current orientation.
private static int getMaxDisplayLength(@NonNull Context context) {
if (maxDisplayLength == null) {
WindowManager windowManager =
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = Preconditions.checkNotNull(windowManager).getDefaultDisplay();
Point displayDimensions = new Point();
display.getSize(displayDimensions);
maxDisplayLength = Math.max(displayDimensions.x, displayDimensions.y);
}
return maxDisplayLength;
}
private void notifyCbs(int width, int height) {
// 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)) {
cb.onSizeReady(width, height);
}
}
@Synthetic
void checkCurrentDimens() {
if (cbs.isEmpty()) {
return;
}
int currentWidth = getTargetWidth();
int currentHeight = getTargetHeight();
if (!isViewStateAndSizeValid(currentWidth, currentHeight)) {
return;
}
notifyCbs(currentWidth, currentHeight);
clearCallbacksAndListener();
}
void getSize(@NonNull SizeReadyCallback cb) {
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);
}
}
/**
* 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.
*/
void removeCallback(@NonNull SizeReadyCallback cb) {
cbs.remove(cb);
}
void clearCallbacksAndListener() {
// Keep a reference to the layout attachStateListener and remove it here
// rather than having the observer remove itself because the observer
// we add the attachStateListener to will be almost immediately merged into
// another observer and will therefore never be alive. If we instead
// keep a reference to the attachStateListener and remove it here, we get the
// current view tree observer and should succeed.
ViewTreeObserver observer = view.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnPreDrawListener(layoutListener);
}
layoutListener = null;
cbs.clear();
}
private boolean isViewStateAndSizeValid(int width, int height) {
return isDimensionValid(width) && isDimensionValid(height);
}
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);
}
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);
}
private int getTargetDimen(int viewSize, int paramSize, int paddingSize) {
// 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;
}
// 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;
}
// 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.
int adjustedViewSize = viewSize - paddingSize;
if (adjustedViewSize > 0) {
return adjustedViewSize;
}
// 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
// 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
// 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.
// 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) {
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;
}
private static final class SizeDeterminerLayoutListener
implements ViewTreeObserver.OnPreDrawListener {
private final WeakReference<SizeDeterminer> sizeDeterminerRef;
SizeDeterminerLayoutListener(@NonNull SizeDeterminer sizeDeterminer) {
sizeDeterminerRef = new WeakReference<>(sizeDeterminer);
}
@Override
public boolean onPreDraw() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "OnGlobalLayoutListener called attachStateListener=" + this);
}
SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
if (sizeDeterminer != null) {
sizeDeterminer.checkCurrentDimens();
}
return true;
}
}
}
}
......@@ -9,6 +9,7 @@ import com.bumptech.glide.util.Util;
* A simple {@link com.bumptech.glide.request.target.Target} base class with default (usually no-op)
* implementations of non essential methods that allows the caller to specify an exact width/height.
* Typically use cases look something like this:
*
* <pre>
* <code>
* Target<Bitmap> target =
......@@ -40,27 +41,32 @@ import com.bumptech.glide.util.Util;
* load into the same {@link View} or caller repeatedly using this class, always retain a reference
* to the previous instance and either call {@link com.bumptech.glide.RequestManager#clear(Target)}
* on the old instance before starting a new load or you must re-use the old instance for the new
* load. Glide's {@link com.bumptech.glide.RequestBuilder#into(Target)} method returns the
* {@link Target} instance you provided to make retaining a reference to the {@link Target} as easy
* as possible. That said, you must wait until you're completely finished with the resource before
* load. Glide's {@link com.bumptech.glide.RequestBuilder#into(Target)} method returns the {@link
* Target} instance you provided to make retaining a reference to the {@link Target} as easy as
* possible. That said, you must wait until you're completely finished with the resource before
* calling {@link com.bumptech.glide.RequestManager#clear(Target)} and you should always null out
* references to any loaded resources in {@link Target#onLoadCleared(Drawable)}.
*
* <p>Always try to provide a size when using this class. Use
* {@link SimpleTarget#SimpleTarget(int, int)} whenever possible with values that are <em>not</em>
* {@link Target#SIZE_ORIGINAL}. Using {@link Target#SIZE_ORIGINAL} is unsafe if you're loading
* large images or are running your application on older or memory constrained devices because it
* can cause Glide to load very large images into memory. In some cases those images may throw
* {@link OutOfMemoryError} and in others they may exceed the texture limit for the device, which
* will prevent them from being rendered. Providing a valid size allows Glide to downsample large
* images, which can avoid issues with texture size or memory limitations. You don't have to worry
* about providing a size in most cases if you use {@link ViewTarget} so prefer {@link ViewTarget}
* over this class whenver possible.
* <p>Always try to provide a size when using this class. Use {@link SimpleTarget#SimpleTarget(int,
* int)} whenever possible with values that are <em>not</em> {@link Target#SIZE_ORIGINAL}. Using
* {@link Target#SIZE_ORIGINAL} is unsafe if you're loading large images or are running your
* application on older or memory constrained devices because it can cause Glide to load very large
* images into memory. In some cases those images may throw {@link OutOfMemoryError} and in others
* they may exceed the texture limit for the device, which will prevent them from being rendered.
* Providing a valid size allows Glide to downsample large images, which can avoid issues with
* texture size or memory limitations. You don't have to worry about providing a size in most cases
* if you use {@link ViewTarget} so prefer {@link ViewTarget} over this class whenver possible.
*
* @see <a href="http://bumptech.github.io/glide/doc/targets.html">Glide's Target docs page</a>
*
* @param <Z> The type of resource that this target will receive.
* @deprecated Use {@link CustomViewTarget} if loading the content into a view, the download API if
* in the background
* (http://bumptech.github.io/glide/doc/getting-started.html#background-threads), or a a fully
* implemented {@link Target} for any specialized use-cases. Using BaseView is unsafe if the
* user does not implement {@link #onLoadCleared}, resulting in recycled bitmaps being
* referenced from the UI and hard to root-cause crashes.
*/
@Deprecated
public abstract class SimpleTarget<Z> extends BaseTarget<Z> {
private final int width;
private final int height;
......
......@@ -42,14 +42,14 @@ public interface Target<R> extends LifecycleListener {
void onLoadStarted(@Nullable Drawable placeholder);
/**
* A lifecycle callback that is called when a load fails.
* A <b>mandatory</b> lifecycle callback that is called when a load fails.
*
* <p> Note - This may be called before {@link #onLoadStarted(android.graphics.drawable.Drawable)
* } if the model object is null.
* <p>Note - This may be called before {@link #onLoadStarted(android.graphics.drawable.Drawable) }
* if the model object is null.
*
* <p>You must ensure that any current Drawable received in {@link #onResourceReady(Object,
* Transition)} is no longer displayed before redrawing the container (usually a View) or
* changing its visibility.
* <p>You <b>must</b> ensure that any current Drawable received in {@link #onResourceReady(Object,
* Transition)} is no longer used before redrawing the container (usually a View) or changing its
* visibility.
*
* @param errorDrawable The error drawable to optionally show, or null.
*/
......@@ -63,11 +63,12 @@ public interface Target<R> extends LifecycleListener {
void onResourceReady(@NonNull R resource, @Nullable Transition<? super R> transition);
/**
* A lifecycle callback that is called when a load is cancelled and its resources are freed.
* A <b>mandatory</b> lifecycle callback that is called when a load is cancelled and its resources
* are freed.
*
* <p>You must ensure that any current Drawable received in {@link #onResourceReady(Object,
* Transition)} is no longer displayed before redrawing the container (usually a View) or
* changing its visibility.
* <p>You <b>must</b> ensure that any current Drawable received in {@link #onResourceReady(Object,
* Transition)} is no longer used before redrawing the container (usually a View) or changing its
* visibility.
*
* @param placeholder The placeholder drawable to optionally show, or null.
*/
......
......@@ -26,21 +26,25 @@ import java.util.List;
* 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 android.widget.ListView} or any {@link
* <p>To detect {@link View} reuse in {@link android.widget.ListView} or any {@link
* 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
* loads can be cancelled or reused. </p>
* loads can be cancelled or reused.
*
* <p> Any calls to {@link View#setTag(Object)}} on a View given to this class will result in
* <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, use {@link #setTagId(int)} to specify a custom tag for Glide to
* use.
*
* <p> Subclasses must call super in {@link #onLoadCleared(Drawable)} </p>
* <p>Subclasses must call super in {@link #onLoadCleared(Drawable)}
*
* @param <T> The specific subclass of view wrapped by this target.
* @param <Z> The resource type this target will receive.
* @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.
*/
@Deprecated
public abstract class ViewTarget<T extends View, Z> extends BaseTarget<Z> {
private static final String TAG = "ViewTarget";
private static boolean isTagUsedAtLeastOnce;
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="glide_custom_view_target_tag" type="id"/>
</resources>
\ No newline at end of file
package com.bumptech.glide.request.target;
import static android.view.ViewGroup.LayoutParams;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.FrameLayout;
import com.bumptech.glide.request.Request;
import com.bumptech.glide.request.transition.Transition;
import com.bumptech.glide.tests.Util;
import com.bumptech.glide.util.Preconditions;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowDisplay;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 19)
public class CustomViewTargetTest {
private ActivityController<Activity> activity;
private View view;
private ViewGroup parent;
private CustomViewTarget<View, Object> target;
@Mock private SizeReadyCallback cb;
@Mock private Request request;
private int sdkVersion;
private AttachStateTarget attachStateTarget;
@Before
public void setUp() {
sdkVersion = Build.VERSION.SDK_INT;
MockitoAnnotations.initMocks(this);
activity = Robolectric.buildActivity(Activity.class).create().start().postCreate(null).resume();
view = new View(activity.get());
target = new TestViewTarget(view);
attachStateTarget = new AttachStateTarget(view);
activity.get().setContentView(view);
parent = (ViewGroup) view.getParent();
}
@After
public void tearDown() {
Util.setSdkVersionInt(sdkVersion);
CustomViewTarget.SizeDeterminer.maxDisplayLength = null;
}
@Test
public void testReturnsWrappedView() {
assertEquals(view, target.getView());
}
@Test
public void testReturnsNullFromGetRequestIfNoRequestSet() {
assertNull(target.getRequest());
}
@Test
public void testCanSetAndRetrieveRequest() {
target.setRequest(request);
assertEquals(request, target.getRequest());
}
@Test
public void testRetrievesRequestFromPreviousTargetForView() {
target.setRequest(request);
CustomViewTarget<View, Object> second = new TestViewTarget(view);
assertEquals(request, second.getRequest());
}
@Test
public void testSizeCallbackIsCalledSynchronouslyIfViewSizeSet() {
int dimens = 333;
activity.get().setContentView(view);
view.layout(0, 0, dimens, dimens);
target.getSize(cb);
verify(cb).onSizeReady(eq(dimens), eq(dimens));
}
@Test
public void testSizeCallbackIsCalledSynchronouslyIfLayoutParamsConcreteSizeSet() {
int dimens = 444;
LayoutParams layoutParams = new FrameLayout.LayoutParams(dimens, dimens);
view.setLayoutParams(layoutParams);
view.requestLayout();
target.getSize(cb);
verify(cb).onSizeReady(eq(dimens), eq(dimens));
}
@Test
public void getSize_withBothWrapContent_usesDisplayDimens() {
LayoutParams layoutParams =
new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
view.setLayoutParams(layoutParams);
setDisplayDimens(200, 300);
activity.visible();
view.layout(0, 0, 0, 0);
target.getSize(cb);
verify(cb).onSizeReady(300, 300);
}
@Test
public void getSize_withWrapContentWidthAndValidHeight_usesDisplayDimenAndValidHeight() {
int height = 100;
LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, height);
view.setLayoutParams(params);
setDisplayDimens(100, 200);
activity.visible();
view.setRight(0);
target.getSize(cb);
verify(cb).onSizeReady(200, height);
}
@Test
public void getSize_withWrapContentHeightAndValidWidth_returnsWidthAndDisplayDimen() {
int width = 100;
LayoutParams params = new FrameLayout.LayoutParams(width, LayoutParams.WRAP_CONTENT);
view.setLayoutParams(params);
setDisplayDimens(200, 100);
parent.getLayoutParams().height = 200;
activity.visible();
target.getSize(cb);
verify(cb).onSizeReady(width, 200);
}
@Test
public void getSize_withWrapContentWidthAndMatchParentHeight_usesDisplayDimenWidthAndHeight() {
LayoutParams params =
new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
view.setLayoutParams(params);
setDisplayDimens(500, 600);
target.getSize(cb);
verify(cb, never()).onSizeReady(anyInt(), anyInt());
int height = 32;
parent.getLayoutParams().height = height;
activity.visible();
view.getViewTreeObserver().dispatchOnPreDraw();
verify(cb).onSizeReady(600, height);
}
@Test
public void getSize_withMatchParentWidthAndWrapContentHeight_usesWidthAndDisplayDimenHeight() {
LayoutParams params =
new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
view.setLayoutParams(params);
setDisplayDimens(300, 400);
target.getSize(cb);
verify(cb, never()).onSizeReady(anyInt(), anyInt());
int width = 32;
parent.getLayoutParams().width = 32;
activity.visible();
view.getViewTreeObserver().dispatchOnPreDraw();
verify(cb).onSizeReady(width, 400);
}
@Test
public void testMatchParentWidthAndHeight() {
LayoutParams params =
new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
view.setLayoutParams(params);
target.getSize(cb);
verify(cb, never()).onSizeReady(anyInt(), anyInt());
int width = 32;
int height = 45;
parent.getLayoutParams().width = width;
parent.getLayoutParams().height = height;
activity.visible();
view.getViewTreeObserver().dispatchOnPreDraw();
verify(cb).onSizeReady(eq(width), eq(height));
}
@Test
public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParams() {
target.getSize(cb);
int width = 12;
int height = 32;
parent.getLayoutParams().width = width;
parent.getLayoutParams().height = height;
activity.visible();
view.getViewTreeObserver().dispatchOnPreDraw();
verify(cb).onSizeReady(eq(width), eq(height));
}
@Test
public void testSizeCallbacksAreCalledInOrderPreDraw() {
SizeReadyCallback[] cbs = new SizeReadyCallback[25];
for (int i = 0; i < cbs.length; i++) {
cbs[i] = mock(SizeReadyCallback.class);
target.getSize(cbs[i]);
}
int width = 100, height = 111;
parent.getLayoutParams().width = width;
parent.getLayoutParams().height = height;
activity.visible();
view.getViewTreeObserver().dispatchOnPreDraw();
InOrder order = inOrder((Object[]) cbs);
for (SizeReadyCallback cb : cbs) {
order.verify(cb).onSizeReady(eq(width), eq(height));
}
}
@Test
public void testDoesNotNotifyCallbackTwiceIfAddedTwice() {
target.getSize(cb);
target.getSize(cb);
view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
activity.visible();
view.getViewTreeObserver().dispatchOnPreDraw();
verify(cb, times(1)).onSizeReady(anyInt(), anyInt());
}
@Test
public void testDoesNotAddMultipleListenersIfMultipleCallbacksAreAdded() {
SizeReadyCallback cb1 = mock(SizeReadyCallback.class);
SizeReadyCallback cb2 = mock(SizeReadyCallback.class);
target.getSize(cb1);
target.getSize(cb2);
view.getViewTreeObserver().dispatchOnPreDraw();
// assertThat(shadowObserver.getPreDrawListeners()).hasSize(1);
}
@Test
public void testDoesAddSecondListenerIfFirstListenerIsRemovedBeforeSecondRequest() {
SizeReadyCallback cb1 = mock(SizeReadyCallback.class);
target.getSize(cb1);
view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
activity.visible();
view.getViewTreeObserver().dispatchOnPreDraw();
SizeReadyCallback cb2 = mock(SizeReadyCallback.class);
view.setLayoutParams(
new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
target.getSize(cb2);
view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
view.getViewTreeObserver().dispatchOnPreDraw();
verify(cb2).onSizeReady(anyInt(), anyInt());
}
@Test
public void testSizeCallbackIsNotCalledPreDrawIfNoDimensSetOnPreDraw() {
target.getSize(cb);
view.getViewTreeObserver().dispatchOnPreDraw();
verify(cb, never()).onSizeReady(anyInt(), anyInt());
activity.visible();
verify(cb).onSizeReady(anyInt(), anyInt());
}
@Test
public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParamsButLayoutParamsSetLater() {
target.getSize(cb);
int width = 689;
int height = 354;
LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
view.setLayoutParams(layoutParams);
view.requestLayout();
view.getViewTreeObserver().dispatchOnPreDraw();
verify(cb).onSizeReady(eq(width), eq(height));
}
@Test
public void testCallbackIsNotCalledTwiceIfPreDrawFiresTwice() {
activity.visible();
target.getSize(cb);
LayoutParams layoutParams = new FrameLayout.LayoutParams(1234, 4123);
view.setLayoutParams(layoutParams);
view.requestLayout();
view.getViewTreeObserver().dispatchOnPreDraw();
view.getViewTreeObserver().dispatchOnPreDraw();
verify(cb, times(1)).onSizeReady(anyInt(), anyInt());
}
@Test
public void testCallbacksFromMultipleRequestsAreNotifiedOnPreDraw() {
SizeReadyCallback firstCb = mock(SizeReadyCallback.class);
SizeReadyCallback secondCb = mock(SizeReadyCallback.class);
target.getSize(firstCb);
target.getSize(secondCb);
int width = 68;
int height = 875;
LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
view.setLayoutParams(layoutParams);
activity.visible();
view.getViewTreeObserver().dispatchOnPreDraw();
view.getViewTreeObserver().dispatchOnPreDraw();
verify(firstCb, times(1)).onSizeReady(eq(width), eq(height));
verify(secondCb, times(1)).onSizeReady(eq(width), eq(height));
}
@Test
public void testDoesNotThrowOnPreDrawIfViewTreeObserverIsDead() {
target.getSize(cb);
int width = 1;
int height = 2;
LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
view.setLayoutParams(layoutParams);
ViewTreeObserver vto = view.getViewTreeObserver();
view.requestLayout();
activity.visible();
assertFalse(vto.isAlive());
vto.dispatchOnPreDraw();
verify(cb).onSizeReady(eq(width), eq(height));
}
@Test(expected = NullPointerException.class)
public void testThrowsIfGivenNullView() {
new TestViewTarget(null);
}
@Test
public void testDecreasesDimensionsByViewPadding() {
activity.visible();
view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
view.setPadding(25, 25, 25, 25);
view.requestLayout();
target.getSize(cb);
verify(cb).onSizeReady(50, 50);
}
@Test
public void getSize_withValidWidthAndHeight_notLaidOut_notLayoutRequested_callsSizeReady() {
view.setRight(100);
view.setBottom(100);
target.getSize(cb);
verify(cb).onSizeReady(100, 100);
}
@Test
public void getSize_withLayoutParams_notLaidOut_doesCallSizeReady() {
view.setLayoutParams(new FrameLayout.LayoutParams(10, 10));
view.setRight(100);
view.setBottom(100);
target.getSize(cb);
verify(cb, times(1)).onSizeReady(anyInt(), anyInt());
}
@Test
public void getSize_withLayoutParams_emptyParams_notLaidOutOrLayoutRequested_callsSizeReady() {
view.setLayoutParams(new FrameLayout.LayoutParams(0, 0));
view.setRight(100);
view.setBottom(100);
target.getSize(cb);
verify(cb).onSizeReady(100, 100);
}
@Test
public void getSize_withValidWidthAndHeight_preV19_layoutRequested_callsSizeReady() {
Util.setSdkVersionInt(18);
view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
view.requestLayout();
target.getSize(cb);
verify(cb).onSizeReady(100, 100);
}
@Test
public void getSize_withWidthAndHeightEqualToPadding_doesNotCallSizeReady() {
view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
view.requestLayout();
view.setPadding(50, 50, 50, 50);
target.getSize(cb);
verify(cb, never()).onSizeReady(anyInt(), anyInt());
}
private void setDisplayDimens(Integer width, Integer height) {
WindowManager windowManager =
(WindowManager) RuntimeEnvironment.application.getSystemService(Context.WINDOW_SERVICE);
ShadowDisplay shadowDisplay =
Shadows.shadowOf(Preconditions.checkNotNull(windowManager).getDefaultDisplay());
if (width != null) {
shadowDisplay.setWidth(width);
}
if (height != null) {
shadowDisplay.setHeight(height);
}
}
@Test
public void clearOnDetach_onDetach_withNullRequest_doesNothing() {
attachStateTarget.clearOnDetach();
attachStateTarget.setRequest(null);
activity.visible();
}
// This behavior isn't clearly correct, but it doesn't seem like there's any harm to clear an
// already cleared request, so we might as well avoid the extra check/complexity in the code.
@Test
public void clearOnDetach_onDetach_withClearedRequest_clearsRequest() {
activity.visible();
attachStateTarget.clearOnDetach();
attachStateTarget.setRequest(request);
when(request.isCleared()).thenReturn(true);
parent.removeView(view);
verify(request).clear();
}
@Test
public void clearOnDetach_onDetach_withRunningRequest_pausesRequestOnce() {
activity.visible();
attachStateTarget.clearOnDetach();
attachStateTarget.setRequest(request);
parent.removeView(view);
verify(request).clear();
}
@Test
public void clearOnDetach_onDetach_afterOnLoadCleared_removesListener() {
activity.visible();
attachStateTarget.clearOnDetach();
attachStateTarget.onLoadCleared(/*placeholder=*/ null);
attachStateTarget.setRequest(request);
parent.removeView(view);
verify(request, never()).clear();
}
@Test
public void clearOnDetach_moreThanOnce_registersObserverOnce() {
activity.visible();
attachStateTarget.setRequest(request);
attachStateTarget
.clearOnDetach()
.clearOnDetach();
parent.removeView(view);
verify(request).clear();
}
@Test
public void clearOnDetach_onDetach_afterMultipleClearOnDetaches_removesListener() {
activity.visible();
attachStateTarget
.clearOnDetach()
.clearOnDetach()
.clearOnDetach();
attachStateTarget.onLoadCleared(/*placeholder=*/ null);
attachStateTarget.setRequest(request);
parent.removeView(view);
verify(request, never()).clear();
}
@Test
public void clearOnDetach_onDetach_afterLoadCleared_clearsRequest() {
activity.visible();
attachStateTarget.clearOnDetach();
attachStateTarget.setRequest(request);
when(request.isCleared()).thenReturn(true);
parent.removeView(view);
verify(request).clear();
}
@Test
public void clearOnDetach_onAttach_withNullRequest_doesNothing() {
attachStateTarget.clearOnDetach();
attachStateTarget.setRequest(null);
activity.visible();
}
@Test
public void clearOnDetach_onAttach_withRunningRequest_doesNotBeginRequest() {
attachStateTarget.clearOnDetach();
attachStateTarget.setRequest(request);
when(request.isCleared()).thenReturn(false);
activity.visible();
verify(request, never()).begin();
}
@Test
public void clearOnDetach_onAttach_withClearedRequest_beginsRequest() {
attachStateTarget.clearOnDetach();
attachStateTarget.setRequest(request);
when(request.isCleared()).thenReturn(true);
activity.visible();
verify(request).begin();
}
@Test
public void clearOnDetach_afterLoadClearedAndRestarted_onAttach_beingsREquest() {
attachStateTarget.clearOnDetach();
attachStateTarget.setRequest(request);
when(request.isCleared()).thenReturn(true);
attachStateTarget.onLoadCleared(/*placeholder=*/ null);
attachStateTarget.onLoadStarted(/*placeholder=*/ null);
activity.visible();
verify(request).begin();
}
@Test
public void clearOnDetach_onAttach_afterLoadCleared_doesNotBeingRequest() {
attachStateTarget.clearOnDetach();
attachStateTarget.setRequest(request);
when(request.isCleared()).thenReturn(true);
attachStateTarget.onLoadCleared(/*placeholder=*/ null);
activity.visible();
verify(request, never()).begin();
}
@Test
public void onLoadStarted_withoutClearOnDetach_doesNotAddListener() {
activity.visible();
target.setRequest(request);
attachStateTarget.onLoadStarted(/*placeholder=*/ null);
parent.removeView(view);
verify(request, never()).clear();
}
@Test
public void onLoadCleared_withoutClearOnDetach_doesNotRemoveListeners() {
AtomicInteger count = new AtomicInteger();
OnAttachStateChangeListener expected =
new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
count.incrementAndGet();
}
@Override
public void onViewDetachedFromWindow(View v) {}
};
view.addOnAttachStateChangeListener(expected);
attachStateTarget.onLoadCleared(/*placeholder=*/ null);
activity.visible();
assertThat(count.get()).isEqualTo(1);
}
private static final class AttachStateTarget extends CustomViewTarget<View, Object> {
AttachStateTarget(View view) {
super(view);
}
@Override
protected void onResourceCleared(@Nullable Drawable placeholder) {}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {}
@Override
public void onResourceReady(
@NonNull Object resource, @Nullable Transition<? super Object> transition) {}
}
private static final class TestViewTarget extends CustomViewTarget<View, Object> {
TestViewTarget(View view) {
super(view);
}
@Override
protected void onResourceCleared(@Nullable Drawable placeholder) {}
// We're intentionally avoiding the super call.
@SuppressWarnings("MissingSuperCall")
@Override
public void onResourceReady(
@NonNull Object resource, @Nullable Transition<? super Object> transition) {
// Avoid calling super.
}
// We're intentionally avoiding the super call.
@SuppressWarnings("MissingSuperCall")
@Override
public void onResourceLoading(@Nullable Drawable placeholder) {
// Avoid calling super.
}
// We're intentionally avoiding the super call.
@SuppressWarnings("MissingSuperCall")
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
// Avoid calling super.
}
}
}
......@@ -687,7 +687,7 @@ public class ViewTargetTest {
private LayoutParams layoutParams;
private boolean isLaidOut;
private boolean isLayoutRequested;
private final Set<OnAttachStateChangeListener> attachStateListeners = new HashSet<>();
final Set<OnAttachStateChangeListener> attachStateListeners = new HashSet<>();
public SizedShadowView setWidth(int width) {
this.width = width;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册