package com.bumptech.glide.request.target;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import com.bumptech.glide.request.Request;
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 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
* 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>
*
* <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>
*
* <p> Subclasses must call super in {@link #onLoadCleared(Drawable)} </p>
*
* @param <T> The specific subclass of view wrapped by this target.
* @param <Z> The resource type this target will receive.
*/
public abstract class ViewTarget<T extends View, Z> extends BaseTarget<Z> {
private static final String TAG = "ViewTarget";
private static boolean isTagUsedAtLeastOnce = false;
@Nullable private static Integer tagId = null;
protected final T view;
private final SizeDeterminer sizeDeterminer;
public ViewTarget(T view) {
this.view = Preconditions.checkNotNull(view);
sizeDeterminer = new SizeDeterminer(view);
}
/**
* Returns the wrapped {@link android.view.View}.
*/
public 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 void getSize(SizeReadyCallback cb) {
sizeDeterminer.getSize(cb);
}
@Override
public void onLoadCleared(Drawable placeholder) {
super.onLoadCleared(placeholder);
sizeDeterminer.clearCallbacksAndListener();
}
/**
* Stores the request using {@link View#setTag(Object)}.
*
* @param request {@inheritDoc}
*/
@Override
public void setRequest(@Nullable Request request) {
setTag(request);
}
/**
* 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
* View#setTag(Object)}. If the tag is cleared or put to another object type, Glide will not be
* 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
@Nullable
public Request getRequest() {
Object tag = 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 void setTag(@Nullable Object tag) {
if (tagId == null) {
isTagUsedAtLeastOnce = true;
view.setTag(tag);
} else {
view.setTag(tagId, tag);
}
}
@Nullable
private Object getTag() {
if (tagId == null) {
return view.getTag();
} else {
return view.getTag(tagId);
}
}
/**
* 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.
*/
public static void setTagId(int tagId) {
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;
}
private static class SizeDeterminer {
// Some negative sizes (Target.SIZE_ORIGINAL) are valid, 0 is never valid.
private static final int PENDING_SIZE = 0;
private final View view;
private final List<SizeReadyCallback> cbs = new ArrayList<>();
@Nullable private SizeDeterminerLayoutListener layoutListener;
SizeDeterminer(View view) {
this.view = view;
}
private void notifyCbs(int width, int height) {
for (SizeReadyCallback cb : 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(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);
}
}
void clearCallbacksAndListener() {
// 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);
}
layoutListener = null;
cbs.clear();
}
private boolean isViewStateAndSizeValid(int width, int height) {
return isViewStateValid() && isSizeValid(width) && isSizeValid(height);
}
private boolean isViewStateValid() {
// We consider the view state as valid if the view has
// non-null layout params and a non-zero layout width and height.
if (view.getLayoutParams() != null
&& view.getLayoutParams().width > 0
&& view.getLayoutParams().height > 0) {
return true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return view.isLaidOut();
}
return !view.isLayoutRequested();
}
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) {
int adjustedViewSize = viewSize - paddingSize;
if (isSizeValid(adjustedViewSize)) {
return adjustedViewSize;
}
if (paramSize == PENDING_SIZE) {
return PENDING_SIZE;
}
if (paramSize == LayoutParams.WRAP_CONTENT) {
return SIZE_ORIGINAL;
} else if (paramSize > 0) {
return paramSize - paddingSize;
} else {
return PENDING_SIZE;
}
}
private boolean isSizeValid(int size) {
return size > 0 || size == SIZE_ORIGINAL;
}
private static class SizeDeterminerLayoutListener implements ViewTreeObserver
.OnPreDrawListener {
private final WeakReference<SizeDeterminer> sizeDeterminerRef;
SizeDeterminerLayoutListener(SizeDeterminer sizeDeterminer) {
sizeDeterminerRef = new WeakReference<>(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;
}
}
}
}