chromium/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/async_image/AsyncImageView.java

// 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;
    }
}