// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.widget.async_image;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
import org.chromium.base.Callback;
import org.chromium.components.browser_ui.widget.R;
import org.chromium.ui.UiUtils;
/**
* Helper class to handle asynchronously loading an image and displaying it when ready. This class
* supports both a 'waiting' drawable and an 'unavailable' drawable that will be used in the
* foreground when the async image isn't present yet.
*/
public class AsyncImageView extends ForegroundRoundedCornerImageView {
/** An interface that provides a way for this class to query for a {@link Drawable}. */
@FunctionalInterface
public interface Factory {
/**
* Called by {@link AsyncImageView} to start the process of asynchronously loading a
* {@link Drawable}.
*
* @param consumer The {@link Callback} to notify with the result.
* @param widthPx The desired width of the {@link Drawable} if applicable (not required to
* match).
* @param heightPx The desired height of the {@link Drawable} if applicable (not required to
* match).
* @return A {@link Runnable} that can be triggered to cancel the outstanding
* request.
*/
Runnable get(Callback<Drawable> consumer, int widthPx, int heightPx);
}
/** Provides the callers an opportunity to override the image size before it is drawn. */
public interface ImageResizer {
/**
* Called by the {@link AsyncImageView} before drawing to the screen.
* @param drawable The {@link Drawable} to be drawn.
*/
void maybeResizeImage(Drawable drawable);
}
private Drawable mUnavailableDrawable;
private Drawable mWaitingDrawable;
private Factory mFactory;
private ImageResizer mImageResizer;
private Runnable mCancelable;
private boolean mWaitingForResponse;
private @Nullable Object mIdentifier;
/** Creates an {@link AsyncImageDrawable instance. */
public AsyncImageView(Context context) {
this(context, null, 0);
}
/** Creates an {@link AsyncImageDrawable instance. */
public AsyncImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/** Creates an {@link AsyncImageDrawable instance. */
public AsyncImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray types =
attrs == null
? null
: context.obtainStyledAttributes(attrs, R.styleable.AsyncImageView, 0, 0);
mUnavailableDrawable =
AutoAnimatorDrawable.wrap(
UiUtils.getDrawable(
context, types, R.styleable.AsyncImageView_unavailableSrc));
mWaitingDrawable =
AutoAnimatorDrawable.wrap(
UiUtils.getDrawable(context, types, R.styleable.AsyncImageView_waitingSrc));
if (types != null) types.recycle();
}
/**
* Starts loading a {@link Drawable} from {@code factory}. This will automatically clear out
* any outstanding request state and start a new one.
*
* @param factory The {@link Factory} to use that will provide the {@link Drawable}.
* @param identifier An identification for this particular request. Subsequent calls with the
* same {@link Object} will be ignored until either {@code null} or a
* different {@link Object} are passed in. This lets us ignore redundant
* calls.
*/
public void setAsyncImageDrawable(Factory factory, @Nullable Object identifier) {
if (mIdentifier != null && identifier != null && mIdentifier.equals(identifier)) return;
// This will clear out any outstanding request.
setImageDrawable(null);
setForegroundDrawableCompat(mWaitingDrawable);
mIdentifier = identifier;
mFactory = factory;
retrieveDrawableIfNeeded();
}
/**
* @param unavailableDrawable Sets the {@link Drawable} to use when there is no thumbnail
* available.
*/
public void setUnavailableDrawable(Drawable unavailableDrawable) {
boolean showUnavailable =
getForegroundDrawableCompat() == mUnavailableDrawable && !mWaitingForResponse;
mUnavailableDrawable = AutoAnimatorDrawable.wrap(unavailableDrawable);
if (showUnavailable) setForegroundDrawableCompat(mUnavailableDrawable);
}
/**
* @param waitingDrawable Sets the {@link Drawable} to use when waiting for an outstanding
* asynchronous thumbnail request.
*/
public void setWaitingDrawable(Drawable waitingDrawable) {
mWaitingDrawable = AutoAnimatorDrawable.wrap(waitingDrawable);
if (mWaitingForResponse) setForegroundDrawableCompat(mWaitingDrawable);
}
/** @param resizer Sets a {@link ImageResizer} to use when drawing the image to the screen. */
public void setImageResizer(ImageResizer resizer) {
mImageResizer = resizer;
}
// RoundedCornerImageView implementation.
@Override
public void setImageDrawable(Drawable drawable) {
// If we had an outstanding async request, cancel it because we're now setting the drawable
// to something else.
cancelPreviousDrawableRequest();
if (mImageResizer != null) mImageResizer.maybeResizeImage(drawable);
setForegroundDrawableCompat(null);
super.setImageDrawable(drawable);
}
// View implementation.
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
retrieveDrawableIfNeeded();
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
if (width == oldWidth && height == oldHeight) return;
if (mImageResizer != null) mImageResizer.maybeResizeImage(getDrawable());
}
private void setAsyncImageDrawableResponse(Drawable drawable, Object identifier) {
// If we ended up swapping out the identifier and somehow this request didn't cancel ignore
// the response. This does a direct == comparison instead of .equals() because any new
// request should have canceled this one (we'll leave null alone though).
if (mIdentifier != identifier || !mWaitingForResponse) return;
mCancelable = null;
mWaitingForResponse = false;
setImageDrawable(drawable);
// Restore the identifier after calling setImageDrawable(), which will erase it.
mIdentifier = identifier;
setForegroundDrawableCompat(drawable == null ? mUnavailableDrawable : null);
}
private void cancelPreviousDrawableRequest() {
mFactory = null;
mIdentifier = null;
if (mWaitingForResponse) {
if (mCancelable != null) mCancelable.run();
mCancelable = null;
mWaitingForResponse = false;
}
}
private void retrieveDrawableIfNeeded() {
// If width or height are not valid, don't start to retrieve the drawable since the
// thumbnail may be scaled down to 0.
if (getWidth() <= 0 || getHeight() <= 0) return;
if (mFactory == null) return;
// Start to retrieve the drawable.
mWaitingForResponse = true;
Object localIdentifier = mIdentifier;
mCancelable =
mFactory.get(
drawable -> setAsyncImageDrawableResponse(drawable, localIdentifier),
getWidth(),
getHeight());
// If setAsyncImageDrawableResponse is called synchronously, clear mCancelable.
if (!mWaitingForResponse) mCancelable = null;
mFactory = null;
}
}